chore(model): optimise fsync calls (#10831)
This improves performance when running with fsync enabled. The key observation is that the OS may coalesce multiple concurrent fsyncs, so we gain some performance by issuing them in parallell. Since they run from the finisher routine, a simple fix is to run multiple finisher routines. That's the `after` line in the graph below. The other step is to do the same for the directory fsyncs, issuing them concurrently with a limiter. In both cases I used the Copiers value as the concurrency factor. Additionally, add some buffering to the channels between routines to minimise stalls where a routine needs to wait for another. This is the `after2` line. All in all, this speeds up syncing 25k tiny files from 190s to 130s, a 30% improvement. <img width="821" height="540" alt="Screenshot 2026-07-25 at 22 58 34" src="https://github.com/user-attachments/assets/18272c89-a99e-4ee4-9b5e-b278967ea7c6" /> --------- Signed-off-by: Jakob Borg <jakob@kastelo.net>
This commit is contained in:
@@ -143,6 +143,9 @@ func newSendReceiveFolder(model *model, ignores *ignore.Matcher, cfg config.Fold
|
||||
f.puller = f
|
||||
|
||||
if f.Copiers == 0 {
|
||||
// "Copiers" is effectively the concurrency level for a number of
|
||||
// different processes in the folder runner, not only the specific
|
||||
// copy step. TODO: Rename this config option at some point.
|
||||
f.Copiers = defaultCopiers
|
||||
}
|
||||
|
||||
@@ -243,10 +246,10 @@ func (f *sendReceiveFolder) pullerIteration(ctx context.Context, scanChan chan<-
|
||||
f.tempPullErrors = make(map[string]string)
|
||||
f.errorsMut.Unlock()
|
||||
|
||||
pullChan := make(chan pullBlockState)
|
||||
copyChan := make(chan copyBlocksState)
|
||||
finisherChan := make(chan *sharedPullerState)
|
||||
dbUpdateChan := make(chan dbUpdateJob)
|
||||
pullChan := make(chan pullBlockState, f.Copiers)
|
||||
copyChan := make(chan copyBlocksState, f.Copiers)
|
||||
finisherChan := make(chan *sharedPullerState, f.Copiers)
|
||||
dbUpdateChan := make(chan dbUpdateJob, f.Copiers)
|
||||
|
||||
var pullWg sync.WaitGroup
|
||||
var copyWg sync.WaitGroup
|
||||
@@ -274,9 +277,11 @@ func (f *sendReceiveFolder) pullerIteration(ctx context.Context, scanChan chan<-
|
||||
})
|
||||
|
||||
// finisherRoutine finishes when finisherChan is closed
|
||||
doneWg.Go(func() {
|
||||
f.finisherRoutine(ctx, finisherChan, dbUpdateChan, scanChan)
|
||||
})
|
||||
for range f.Copiers {
|
||||
doneWg.Go(func() {
|
||||
f.finisherRoutine(ctx, finisherChan, dbUpdateChan, scanChan)
|
||||
})
|
||||
}
|
||||
|
||||
fileDeletions, dirDeletions, err := f.processNeeded(ctx, dbUpdateChan, copyChan, scanChan)
|
||||
|
||||
@@ -1772,21 +1777,10 @@ func (f *sendReceiveFolder) dbUpdaterRoutine(dbUpdateChan <-chan dbUpdateJob) in
|
||||
tick := time.NewTicker(maxBatchTime)
|
||||
defer tick.Stop()
|
||||
batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
|
||||
// sync directories
|
||||
for dir := range changedDirs {
|
||||
delete(changedDirs, dir)
|
||||
if !f.DisableFsync {
|
||||
fd, err := f.mtimefs.Open(dir)
|
||||
if err != nil {
|
||||
f.sl.Debug("Fsync failed", slogutil.FilePath(dir), slogutil.Error(err))
|
||||
continue
|
||||
}
|
||||
if err := fd.Sync(); err != nil {
|
||||
f.sl.Debug("Fsync failed", slogutil.FilePath(dir), slogutil.Error(err))
|
||||
}
|
||||
fd.Close()
|
||||
}
|
||||
if !f.DisableFsync {
|
||||
f.fsyncDirs(changedDirs)
|
||||
}
|
||||
clear(changedDirs)
|
||||
|
||||
// All updates to file/folder objects that originated remotely
|
||||
// (across the network) use this call to updateLocals
|
||||
@@ -1840,6 +1834,27 @@ loop:
|
||||
return changed
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) fsyncDirs(changedDirs map[string]struct{}) {
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, f.Copiers)
|
||||
for dir := range changedDirs {
|
||||
sem <- struct{}{}
|
||||
wg.Go(func() {
|
||||
defer func() { <-sem }()
|
||||
fd, err := f.mtimefs.Open(dir)
|
||||
if err != nil {
|
||||
f.sl.Debug("Fsync failed", slogutil.FilePath(dir), slogutil.Error(err))
|
||||
return
|
||||
}
|
||||
if err := fd.Sync(); err != nil {
|
||||
f.sl.Debug("Fsync failed", slogutil.FilePath(dir), slogutil.Error(err))
|
||||
}
|
||||
fd.Close()
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// pullScannerRoutine aggregates paths to be scanned after pulling. The scan is
|
||||
// scheduled once when scanChan is closed (scanning can not happen during pulling).
|
||||
func (f *sendReceiveFolder) pullScannerRoutine(ctx context.Context, scanChan <-chan string) {
|
||||
|
||||
+6
-2
@@ -1,5 +1,5 @@
|
||||
<configuration version="51">
|
||||
<folder id="default" label="" path="s1" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" fsWatcherTimeoutS="0" ignorePerms="false" autoNormalize="true">
|
||||
<configuration version="52">
|
||||
<folder id="default" label="" path="~/tmp/src" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" fsWatcherTimeoutS="0" ignorePerms="false" autoNormalize="true">
|
||||
<filesystemType>basic</filesystemType>
|
||||
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy="">
|
||||
<encryptionPassword></encryptionPassword>
|
||||
@@ -37,6 +37,7 @@
|
||||
<sendOwnership>false</sendOwnership>
|
||||
<syncXattrs>false</syncXattrs>
|
||||
<sendXattrs>false</sendXattrs>
|
||||
<blockIndexing>true</blockIndexing>
|
||||
<xattrFilter>
|
||||
<maxSingleEntrySize>0</maxSingleEntrySize>
|
||||
<maxTotalSize>0</maxTotalSize>
|
||||
@@ -72,6 +73,8 @@
|
||||
<metricsWithoutAuth>false</metricsWithoutAuth>
|
||||
<apikey>abc123</apikey>
|
||||
<theme>default</theme>
|
||||
<sessionCookieDurationS>604800</sessionCookieDurationS>
|
||||
<sessionCookiePath>/</sessionCookiePath>
|
||||
</gui>
|
||||
<ldap></ldap>
|
||||
<options>
|
||||
@@ -165,6 +168,7 @@
|
||||
<sendOwnership>false</sendOwnership>
|
||||
<syncXattrs>false</syncXattrs>
|
||||
<sendXattrs>false</sendXattrs>
|
||||
<blockIndexing>true</blockIndexing>
|
||||
<xattrFilter>
|
||||
<maxSingleEntrySize>1024</maxSingleEntrySize>
|
||||
<maxTotalSize>4096</maxTotalSize>
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<configuration version="52">
|
||||
<folder id="default" label="" path="s2" type="sendreceive" rescanIntervalS="60" fsWatcherEnabled="false" fsWatcherDelayS="10" fsWatcherTimeoutS="0" ignorePerms="false" autoNormalize="true">
|
||||
<folder id="default" label="" path="~/tmp/dst" type="sendreceive" rescanIntervalS="60" fsWatcherEnabled="false" fsWatcherDelayS="10" fsWatcherTimeoutS="0" ignorePerms="false" autoNormalize="true">
|
||||
<filesystemType>basic</filesystemType>
|
||||
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy="">
|
||||
<encryptionPassword></encryptionPassword>
|
||||
@@ -31,7 +31,7 @@
|
||||
<disableFsync>false</disableFsync>
|
||||
<blockPullOrder>standard</blockPullOrder>
|
||||
<copyRangeMethod>standard</copyRangeMethod>
|
||||
<caseSensitiveFS>false</caseSensitiveFS>
|
||||
<caseSensitiveFS>true</caseSensitiveFS>
|
||||
<junctionsAsDirs>true</junctionsAsDirs>
|
||||
<syncOwnership>false</syncOwnership>
|
||||
<sendOwnership>false</sendOwnership>
|
||||
|
||||
Reference in New Issue
Block a user