Repository navigation
A DATA listing reads its inodes in a pass, not one from the root each - #1236
Conversation
A listing entry carries its file's size and kind, so `DataVolume::list` read each entry's inode with `Mount::inode`: one search of the inodes btree from its root per name, after the dirents in batches of 256, each batch reading the directory's own inode again. On the host model of the T14's DATA those lookups were 416 us of a 1 000-entry listing's 534 us (#1152's evidence). The engine now answers a listing itself. `Source::listing` reads a directory's names in one walk of the dirents and their inodes with `Source::inodes`, which takes the numbers in ascending order and keeps one walk of the inodes btree going while the next number is within `NEAR` (32) of the last: the keys between are stepped over where they lie, far fewer than a leaf holds, and a number further on is found from the root as before, so a directory whose inodes are scattered costs what it did. Two names of one inode are answered from one read, and a name whose inode is absent refuses the listing `NotFound`, as the lookup alone did. `DataVolume::list` maps and sorts what the engine answers. Tests, on the host: - `lib/bcachefs`'s `a_listing_reads_its_inodes_in_a_pass_over_each_run_of_them` wraps the mount in a `Source` that counts walks, each one search from a root. A directory of 1 000 files made in a row, a second name for one and a file made after 40 elsewhere lists in 4: the directory's inode, its names, and the two runs of inodes. Every answer is compared with `Mount::inode`'s for the same number. With `NEAR` set to 0, every inode from the root, the count is 1 003 and the test is red. - `inodes_read_together_are_those_read_alone`: any order, a number twice, none, and an absent number first, in the middle and last. - `system/fileserver`'s `a_listing_says_of_each_entry_what_its_path_does` holds each listed entry to `lstat` and `identity` of its own path, after a remount. The record's other half stands: every READDIR request still lists the directory whole. The record is renamed to say that, and keeps the exit on both benches' `fs.disk.readdir.1000`, which this change has not been measured against yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1236 at 7d00a1fArchitectural assessment
RequiredNone in the code. The metal A/B's rules
What the A/B must show to land
Suggestions
LAND, pending the metal A/B above, run in a counterbalanced order. |
…unmeasured The walk-counting source said each walk is one search from the root; a walk crossing a leaf finds the next leaf from the root again, so it starts from the root and may search more than once. NEAR's comment says its cost argument is reasoned, not measured, as the PR does. Comments only: the staged A/B images, built at 7d00a1f, measure the same code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four boots a side, counterbalanced (#1236's evidence): readdir.1000 from 2.87-2.91 ms to 2.38-2.40 ms, 0.83 times, well short of the host model's promise and of the record's exit, which stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Orchestrator decision on the A/B (8 of 8 boots valid, B-1 H-1 H-2 B-2 H-3 B-3 B-4 H-4):
|
#1236 merged before its owed list reached its branch's record. The osbench_disk A/B of main before and after it, on both benches, goes into the DATA rows' record, which this branch and main both carry, with the readdir.1000 and open_close/stat conditions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
A DATA listing entry carries its file's size and kind, so
DataVolume::listread each entry's inode withMount::inode: one search of the inodes btree from its root per name, after the dirents in batches of 256, each batch reading the directory's own inode again. On the host model of the T14's DATA those lookups were 416 us of a 1 000-entry listing's 534 us (#1152's evidence);fs.disk.readdir.1000stood at about 3x main's on both benches.Resulting behaviour
The engine answers a listing itself.
Source::listingreads a directory's names in one walk of the dirents and their inodes withSource::inodes, which takes the numbers in ascending order and keeps one walk of the inodes btree going while the next number is withinNEAR(32) of the last. The keys between are stepped over where they lie, far fewer than a leaf holds; a number further on is found from the root as before, so a directory whose inodes are scattered costs what it did. Two names of one inode are answered from one read, and a name whose inode is absent refuses the listingNotFound, as the lookup alone did.DataVolume::listmaps and sorts what the engine answers.The record's other half stands: every READDIR request still lists the directory whole. The record is renamed to say that (
issues/a-data-directory-listing-is-read-whole-for-every-readdir-request.md) and keeps its exit on both benches'readdir.1000.Checks
Host, macOS arm64, at 7d00a1f (this change on 422915e; review r1's 506f6f8 changes two comments only, and
cargo clippy -p bcachefs --all-targetsis clean there, exit 0; it merges with main b566931 without conflict, and nothing since toucheslib/bcachefsorsystem/fileserver):cargo test -p bcachefs -p fileserver: exit 0 (bcachefs 201, fileserver 122 andosbench_replay3).a_listing_reads_its_inodes_in_a_pass_over_each_run_of_themwraps the mount in aSourcethat counts walks, each of which starts from a root. A directory of 1 000 files made in a row, a second name for one and a file made after 40 elsewhere lists in 4 walks: the directory's inode, its names and the two runs of inodes. Every answer is compared withMount::inode's. Negative control: withNEAR0 the count is 1 003 and the test is red.inodes_read_together_are_those_read_alone: any order, a number twice, none, and an absent number first, in the middle and last.system/fileserver'sa_listing_says_of_each_entry_what_its_path_doesholds each listed entry tolstatandidentityof its own path after a remount.cargo clippy -p bcachefs --all-targetswith the gate's adopted lints and-D warnings: exit 0. The file server is a program, which the gate does not lint; run anyway, it finds four things in files this PR does not touch (listing.rs,pages/tests.rs,writeback.rs) and none indata.rs.The full
--ci hostwas not run: the change is two packages' code, and their tests and lint are above.Metal is no longer a landing condition: both benches are down. The speed-up is measured on the T14 alone; on the AMD bench it is not yet measured. Owed on metal (recorded in
issues/a-data-directory-listing-is-read-whole-for-every-readdir-request.md): both benches,osbench_disk'sfs.disk.*rows in one A/B of main before and after this PR; on the AMD bench main after must showreaddir.1000below main before on every boot, and on both no other row may resolve against main after, which is whereopen_closeandstat(below) recur or not.The T14 A/B, done before the benches went down: a one-bench A/B of
osbench_disk(thefs.disk.*rows) on the T14, H = 7d00a1f against its base B = 422915e, four boots a side in the counterbalanced order B, H, H, B, H, B, B, H set before any boot. r1's comments leave the measured code as it is.osbench_diskrow:PASS, which holdsdata_on_the_nvme(DATA ours, mounted, not formatted by the boot); no boot formatted DATA;readdir.1000reported on every boot. This was also the listing's first run on a real drive.readdir.1000resolves, H faster: B 2.869 to 2.914 ms, H 2.384 to 2.400 ms, every H below every B; H/B 0.826 (exact two-sided Mann-Whitney, p = 2/70). Far less than the host model promised; the record says so and stays open.open_close(B 21.73 to 21.89 us, H 22.05 to 22.29 us; H/B 1.018) andstat(B 12.21 to 12.36 us, H 12.50, 14.84, 14.13, 12.52 us; H/B 1.081, carried by two boots). Neither path lists anything. Disassembling both builds' fileserver, function by function with addresses removed:DataVolume::lstat,statandopenare the same code in H as in B, and so are 3 589 other functions, every one at another address in H. The one change on their path isMount::inode's future, which in H calls the out-of-linedecode_inode(withtimeinlined into it) where B decoded inline (callingtimefour times): the same calls intoInodeValue, one call and return more per inode read. That is nanoseconds against the 0.3 to 1 us the rows moved, so the change adds no work there that would explain them. Code placement and boot-to-boot variance would; that is not shown by a control.Evidence: https://github.com/ToyOSOrg/evidence/tree/9124a882ece9c3441d9894a5101d78221401134b (host logs, the control patch, and under
metal/the A/B: rules, every boot'sfs.diskrows and verdict, the offline judgments, the table, and the code comparison).Risks
NEARis a reasoned constant (keys stepped over against a root-to-leaf search), not a measured one, as its comment now says.readdir.1000within the record's 1.5x of main's (2.39 ms against about 1.37 ms); the record stays open with the measurement, and where the remaining time goes is not measured.open_closeandstatresolve against H by 1.8 % and 8 %; the compiled code says the change does no more work on those paths, but no layout control has shown the cause.🤖 Generated with Claude Code