Faster File Provider Log Rotation - #10741
Conversation
There was a problem hiding this comment.
While I personally consider the practical performance impact neglectable (I see the substantial bottlenecks elsewhere), this optimization still appears reasonable and justified to scale better.
I will definitely remember this pattern in the future. It makes much more sense than the repeated calls.
|
@juliusvaart Oh, oops… I just noticed: you opened the pull request to be merged into |
|
/backport to master |
|
Uh… I just tried whether our backport bot can actually do the reverse of what it usually does… Maybe, maybe, maybe… Otherwise: We need another PR to |
Head branch was pushed to by a user without write access
07e6ea2 to
1e19b16
Compare
|
Now based on |
|
Same as in #10734: the failed DCO check can be ignored. When the other checks succeed, we can merge. |
claucambra
left a comment
There was a problem hiding this comment.
Just some complaints about claudeisms (bad commenting practices)
1e19b16 to
d698e93
Compare
`rotateLogFileIfNeeded()` runs before every single line and used to `stat` the log file each time to decide whether to rotate. `write(...)` then called `synchronize()` after every line. Both are syscalls, and both sit on an actor that every hot path in the extension awaits, so a burst of log lines became that many serialized disk operations in front of unrelated work. A bulk materialisation emits tens of thousands of lines: one measured pass produced 28,334 of them in seven minutes. Track the bytes written instead of asking the file system. The counter is exact because this actor is the only writer and each file is created empty. Drop the per-line fsync: `FileHandle.write(contentsOf:)` is an unbuffered `write(2)`, so the line is in the file and readable the moment it returns, and the fsync only bought durability against power loss, which a diagnostic log does not need. Rotation still flushes before closing a file. Both substitutions are safe only because of properties that are easy to break later and invisible when broken, so each gets a test: the counter agrees with the file byte for byte, an unsynchronised line is readable as soon as the write returns, and rotation restarts the counter rather than carrying the closed file's total over. Reaching those from a test needs somewhere to write, and a test bundle has no application group container. `init` therefore takes an optional logs directory and a maximum file size, mirroring `FilesDatabaseManager(databaseDirectory:)`. Both default to today's behaviour. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Julius van der Vaart <julius@vanderva.art>
d698e93 to
770a16e
Compare
|
Artifact containing the AppImage: nextcloud-appimage-pr-10741.zip Digest: To test this change/fix you can download the above artifact file, unzip it, and run it. Please make sure to quit your existing Nextcloud app and backup your data. |
|
The backport to # Switch to the target branch and update it
git checkout master
git pull origin master
# Create the new backport branch
git checkout -b backport/10741/master
# Cherry pick the change from the commit sha1 of the change against the default branch
# This might cause conflicts, resolve them
git cherry-pick 770a16e1
# Push the cherry pick commit to the remote repository and open a pull request
git push origin backport/10741/masterError: Failed to check for changes with origin/master: No changes found in backport branch Learn more about backports at https://docs.nextcloud.com/server/stable/go.php?to=developer-backports. |
|
/backport to stable-34.0 |
rotateLogFileIfNeeded()runs before every single line and used tostatthe log file each time to decide whether to rotate.write(...)then calledsynchronize()after every line. Both are syscalls, and both sit on an actor that every hot path in the extension awaits, so a burst of log lines became that many serialized disk operations in front of unrelated work. A bulk materialisation emits tens of thousands of lines: one measured pass produced 28,334 of them in seven minutes.Track the bytes written instead of asking the file system. The counter is exact because this actor is the only writer and each file is created empty. Drop the per-line fsync:
FileHandle.write(contentsOf:)is an unbufferedwrite(2), so the line is in the file and readable the moment it returns, and the fsync only bought durability against power loss, which a diagnostic log does not need. Rotation still flushes before closing a file.Both substitutions are safe only because of properties that are easy to break later and invisible when broken, so each gets a test: the counter agrees with the file byte for byte, an unsynchronised line is readable as soon as the write returns, and rotation restarts the counter rather than carrying the closed file's total over.
Reaching those from a test needs somewhere to write, and a test bundle has no application group container.
inittherefore takes an optional logs directory and a maximum file size, mirroringFilesDatabaseManager(databaseDirectory:). Both default to today's behaviour.Assisted-by: Claude Opus 5
Resolves
#
Summary
FileProviderLogis an actor, and every hot path in the File Provider extension awaitsit. Two syscalls per line sat on that actor:
rotateLogFileIfNeeded()runs before every single line, and itstat-ed the log fileeach time to decide whether to rotate.
write(...)calledsynchronize()after every line.That is fine at a handful of lines a second and expensive in bulk. A materialisation pass
on a large synced folder emitted 28,334 lines in seven minutes, each one putting a
statand an
fsyncin front of unrelated extension work.This replaces the
statwith a byte counter and removes the per-line fsync:empty.
FileHandle.write(contentsOf:)is an unbufferedwrite(2), so a line is in the fileand readable the moment the call returns. The fsync only added durability against power
loss, which a diagnostic log does not need. Rotation still flushes before closing a file.
Net effect is two syscalls removed per line, one of them a disk flush.
Before / after
20,000 log lines written through the actor, three runs each, same machine. Measured by
building
stable-34.0with only this PR'sinitparameters applied (so both sides can bedriven from a test bundle) and running the identical probe against both:
stable-34.07.0x, median 1.393 s -> 0.199 s. Against the 28,334-line pass above that is roughly
1.97 s of actor time down to 0.28 s.
To reproduce, drop this into
Tests/NextcloudFileProviderKitTests/on either side and runswift test --filter ThroughputProbe:ThroughputProbe.swift
The probe itself is deliberately not committed. A wall-clock assertion is flaky on
hosted runners, so the tests that ship assert correctness properties instead.
Tests
Both substitutions are safe only because of properties that are easy to break later and
invisible when they break, so each gets a guard in FileProviderLogWriteTests:
the byte counter agrees with the file on disk exactly — drift compounds silently, too high and the log rotates early forever, too low and it never rotates
a line is readable as soon as write returns — the sole justification for dropping the fsync; if it stops holding, the log loses its newest lines, which are the ones a crash investigation wants
rotation restarts the counter rather than carrying the closed file's total over, which would leave it above the limit permanently and rotate on every subsequent line
Note on the API change
FileProviderLog.init gains an optional logs directory and maximum file size. A test
bundle has no application group container, so without a directory to write to none of the
above is reachable from a test. This mirrors FilesDatabaseManager(databaseDirectory:),
which exists for the same reason. Both parameters default to current behaviour.
Checklist
AI (if applicable)