Repository navigation
fileserver: an fsync of a clean file asks the disk for nothing - #1067
Conversation
Review r1: PR #1067 at 991f08dArchitectureThe change sits in the right owners. Durability: is every acknowledged fsync covered by an answered flush?I traced this myself, and the answer is yes in every path I could build.
Dirty tracking (truncate, rename, mtime-only, size-only)
EvidenceI checked the evidence branch at 558a5282. Host CI exited 0 at the head. The disk-request probe shows base 1w+1f → change 0/0 for a clean sync, with the overwrite and append counts unchanged. Each of m1–m4 is a single-line mutation of the claim it targets, and each turns the named test red (exit 101; the panics are at the asserted lines). The FAT test exercises FindingsBLOCKER
SUGGESTION S1. S2. S3. The durability argument holds in every path above. The only blocker is the stale header in finding 1, which is a one-line fix. SEND BACK |
An fsync on the FAT log volume of a file nothing had changed cost a block write and a device flush, about 2 ms over the T14's USB 2.0 stick, where Linux on NVMe takes 30 us (osbench persist.fsync_clean, 67x). Two redundancies made it: - The FAT volume rewrote every open file's directory entry at each sync and last close, stamped with the time of that sync, whether or not the file had been written. A file only read, or fsynced twice, got a new modification time, and the entry block went back to the disk. An entry is now levelled only when its file was written or resized since the entry was last written, as the DATA volume's already was. - The cache asked the disk to flush at every flush, even with nothing written since a flush the disk had answered. A flush makes durable every write completed before it, so that request bought nothing. The cache now owes the disk a flush from each write it issues, and from mount, since a previous server may have left writes in the device's cache, until a flush the disk answers. Only the running flush writes the disk, so nothing is written between that flush and the debt's clearing. A refused flush leaves the debt. An acknowledged fsync still means the file's data and entry are on the medium past a flush the disk answered. Counted on a host disk, a clean sync goes from one write and one flush to none. An overwrite's sync (two writes, one flush) and an append's (five writes, two flushes) are unchanged: those are the ordering FAT needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… stamps The module header said every open file is brought level at each close, fsync and sync; only a file changed since its last level is. A level is also taken whenever the handle and its entry disagree (`needs_reconcile`), so a size or chain change no mutator marked is never skipped. An open with O_TRUNC marks the file changed even when it was already empty, since POSIX's open() updates its timestamps there. The clock is read only for a file that is levelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
r1 addressed in 2118299, rebased onto main 8348c2a.
Rerun at the head: fileserver tests, host CI (exit 0, 103 green) and Please review the r1 commit. |
Review r2: PR #1067 at 2118299Scope: the r1 commit 2118299 and the rebase onto main 8348c2a. I did not reopen the durability argument settled in r1. Rebase
r1 commit (fat.rs, +15/-11)
EvidenceThe evidence branch at ccda61f8, section "Review r1: head 2118299 on main 8348c2a", records reruns at the head:
No test covers S2's empty-file FindingsNo blockers. No new suggestions. LAND |
|
T14 benchcase at 2118299 (build line confirms the head; flashed with main's 8348c2a tool onto the ToyOS stick): persist.fsync_clean p50 2.00 ms -> 39 µs (Linux ext4/NVMe: 67 µs); write_4k_fsync 2.16 -> 1.01 ms; append/create/seq_write rows remain stick-bound. Readback: orchestrator scratch persist-fsync/t14/benchcase. Earlier m1067r1 run was invalid (pre-#1064 tool flashed the spare stick) and is not evidence. Review r2 LAND. Landing. |
#1097's AMD validation reads persist.fsync_clean at 1.23 ms under ToyOS against 57-70 us under Linux on the same NVMe. Mounted::sync writes the superblock on every commit, so the cache owes a Flush even where nothing changed. #1067's handoff named this open and unfiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1097's AMD validation reads persist.fsync_clean at 1.23 ms under ToyOS against 57-70 us under Linux on the same NVMe. Mounted::sync writes the superblock on every commit, so the cache owes a Flush even where nothing changed. #1067's handoff named this open and unfiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
osbench
persiston the T14 at aeba59f had ToyOS 67× slower than Linux onfsync_clean(2.0 ms p50 against 30 µs), the worst ratio in the group.What the row measures. On a benchcase boot, ToyOS's
persistfiles are on the boot stick'sTOYOS-LOGpartition: FAT32 with 512-byte clusters, served by the LOG file server through usbd. The stick is a SanDisk Ultra on a high-speed (USB 2.0) link. The run'spersist.mediumcondition records this, and so doeskernel.log: boot lines 270, 279 and 286 and line 14349. That boot has no DATA partition: DATA is bcachefs in memory (lines 298–301). Linux ran on ext4 on the internal NVMe. bcachefs and NVMe are not on the measured ToyOS path, so the whole group compares a USB 2.0 stick with NVMe.Where a clean fsync went. I counted disk requests on a host disk at the base revision. An fsync of a file with no change since its last sync cost one block write and one device flush. Over usbd that is a WRITE(10) and a SYNCHRONIZE CACHE. It came from two redundancies:
FatVolume::levelrewrote each open file's directory entry at every sync and last close, stamped with the time, even when nothing had written the file. It also changed the mtime of a file that was only read, or fsynced twice.Cache::flushasked the disk to flush even when nothing had been written since a flush the disk answered.Change
system/fileserver/src/fat.rs): an open file isdirtyonce written, resized or opened withO_TRUNC.O_TRUNCcounts even on an empty file, because POSIX'sopenstamps it.levelwrites the entry only when the file is dirty or its handle disagrees with its entry (needs_reconcile), and clears the flag only when the level succeeds. This matches the DATA volume'sdirty/persist.system/fileserver/src/cache.rs): the cache owes the disk a flush from each write it issues, until a flush the disk answers. The debt starts set, because a previous server (restart = true) may have left writes in the device's volatile cache. A refused flush keeps it. Only the running flush writes the disk, so no write falls between the disk's flush and the debt being cleared.Durability. An acknowledged fsync still means the file's data and entry reached the medium before a flush the disk answered. Skipping the flush is safe because every write the cache ever issued was already covered by an answered flush. This sits in the cache, not in usbd or nvme, because the cache is the one place that knows what this server wrote. A flush owed across a device loss still goes out, and blockhold judges it as before.
Disk requests per sync, host disk, base → this branch
fsync_clean)write_4k_fsync)append_4k_fsync)Inference, not measured per command. The T14 times fit about 1 ms per USB mass-storage command: 2 commands ≈ 2.0 ms, 3 ≈ 2.16 ms, 7 ≈ 7.4 ms. So
fsync_cleanshould fall to about the cost of the IPC round trip. The other rows send the requests FAT's ordering needs on a USB 2.0 stick: the chain and data are flushed before the entry that names them.seq_write_fsync's 14 MB/s is in the range of a USB 2.0 stick's writes. The T14 readback below is the timing evidence. It has not been run yet.Checks
The head is 2118299, rebased onto main 8348c2a. All checks ran on a macOS aarch64 host. Index: evidence pr-1067.
cargo run -- --ci host: exit 0, 103 steps green, including the bcachefs crash-model suites and the fileserver's kill and refusal tests (log).cargo test --test toyos-build -- the_log_lands_on_the_boot_stick, x86-64 QEMU: exit 0. The boot's log is written through usbd onto the stick and read back on the host after the stop (log).cargo run -- --build-only: exit 0 at 991f08d (log). Since then the image has also been built at the head by the guest test and the readback staging.cache::a_flush_asks_the_disk_to_flush_only_what_it_wrote_since_its_lastandfat::a_sync_of_files_nothing_changed_asks_the_disk_nothing.osbenchrow's single benchcase boot, withpersistas its last group.Not addressed
FSYNCstill syncs the whole volume (issues/an-fsync-of-a-served-file-syncs-its-whole-volume.md). Now that unchanged files cost nothing, this matters only when another file is dirty.Mounted::sync(lib/bcachefs/src/fs.rs) still writes the superblock and flushes on a clean commit. With this change a clean DATA fsync costs one flush instead of two. The measured row does not reach this code, and the clean-room engine replaces it.seq_read_cached(955 MB/s against 8.7 GB/s) is a read-path copy cost, not a durable write. I did not investigate it.🤖 Generated with Claude Code