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:
Jakob Borg
2026-07-27 12:13:05 +02:00
committed by GitHub
parent b2092b188f
commit 952da4224b
3 changed files with 44 additions and 25 deletions
+36 -21
View File
@@ -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
View File
@@ -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
View File
@@ -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>