Repository navigation
fileserver and bcachefs: a served stat, listing and close search no tree, and CRC-32C is the CPU's instruction - #1060
Conversation
Measured on the host against DATA over memory, with each osbench files row's volume calls in the order the server makes them, the served path's time was the interim format's tree: every node read checks its CRC a byte at a time and parses every entry, and the DATA volume searched twice per stat (`file_extents`, then `file_mtime`), again for each path component the resolver looks up, and twice per file in a listing. A stat was eight node reads, a listing of 1,000 files 4,000. DATA's in-memory name index now holds what each entry records, its kind, length and time, kept as the entry changes (persist, truncate, create, rename, symlink, removal) and read whole at mount from the format's one listing walk, which now answers those too; the mount's search per name for its kind goes. A stat, a lookup and a listing are map queries; an open searches once, for the extents. A listing passes over the subtree of each child directory instead of walking every name beneath it. `CLOSE` is answered with nothing, so a close costs its client a send and not a round trip. A refused close keeps its node for the next sync, as before, and the volume says so; the client always ignored the answer. The server keeps its watches armed and re-watches only what fired: a watch replaces its handle's poll by a pass over every poll the ring holds, so re-watching every client per request made each request cost the kernel O(clients^2). Host, release, volume calls only (no IPC), before -> after: stat 181,810 -> 673 ns; open_close 298,944 -> 27,390 ns; readdir.1000 128.8 ms -> 0.19 ms; create_write_unlink 701 -> 259 us. rename (419 -> 308 us) and mkdir_rmdir (305 -> 316 us) are the format's writes, whose CRC dominates them too, and are the engine's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every checksum this crate takes, the interim format's per-node CRC on every read and write and the engine's node, journal, superblock and extent checksums, went through a table walked a byte at a time: a serial dependency of about five cycles a byte, and after the DATA server's own searches went (the previous commit) the larger part of what each of DATA's mutations costs. It is now SSE4.2's `CRC32` on x86-64 and FEAT_CRC32's `CRC32C` on AArch64, eight bytes an instruction, from `core::arch` under `target_feature` in an architecture file each (`src/crc32c/`). Every machine ToyOS supports has the instruction (x86-64 parts of 2020 on; FEAT_CRC32 is mandatory from Armv8.1), as do the hosts that build and test it, and the QEMU models (`build/arch.rs` gives TCG's x86-64 guest `+sse4.2`); no other way of computing it is kept. Any other architecture fails the build by name. The tests hold both instructions to a bit-at-a-time reference over RFC 3720's examples (Appendix B.4), every length to 24 bytes at every word alignment and whole nodes, the uninverted register's included. Measured on the aarch64 host, interleaved in one run, per 4,088-byte node: the table 12.4 us, the instruction 0.65 us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s nothing The file server now watches a connection again only after its watch was answered, and reads one frame an answer. That is sound because a poll nothing replaced stays standing across submits, and a watch looks at its object when it is made, so a second frame queued behind the one read is answered with no post behind it. The model holds `polls.rs` to both in the server's own sequence. Made edge-triggered (an arm that never fires for bytes already there), it reds at the queued frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 of #1060 at
|
The rename collected every record beneath the directory, then wrote the entries of its open files, which updates their records, and moved the copies collected before: an open, written file under a renamed directory kept the record it had before the write until a remount. It now moves each record as it stands once the entries are written. The index differential gains such a file and a clock that advances, so a stale time differs too; with the old move it reds. The acceptor is watched only below the handshake bound, and only its firing adds a waiting client, so the bound checked again at its firing was always met: gone. The loom case added in fcd8099 is gone too: what told it apart was the test's own model of `arm`, not the kernel's. A `CLOSE` is never outstanding, said where the bound is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review r1, answered at
Suggestions taken: the Host CI at |
Review r2 of #1060 at
|
The hang review r1's suite run met at test_rs_socket_export's race is `issues/two-holders-racing-on-an-exported-socket-can-both-stall.md`, which PR #1060 filed: its own record goes, and its observation (two of three at this branch, one of one on main) joins that one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
On the T14, files the DATA file server serves (
/home, in memory) are 20–430× slower than Linux's/dev/shm(osbenchfs.served.*, PR #1053's tables): a stat 59.7 µs, an open and close 95 µs, a 1,000-entry listing 76 ms, a rename 292 µs.Where the time went
Measured on the host, against DATA over memory, with each osbench files row's volume calls in the order
src/main.rsmakes them and no IPC (harness, before, sampler call graphs). Nearly all of it was the format's tree:file_extents, thenfile_mtime), once more for each path component the resolver looks up, and twice per file in a listing. A stat cost 8 node reads; a 1,000-file listing cost 4,000.The IPC round trips explain what is left.
What changes
system/fileserver/src/data.rs).Mounted::listnow also answers each entry's symlink flag and time (bcachefs::Recorded), which also drops the mount's search per name.CLOSEis answered with nothing (sdk/toyos/src/fs.rs). A close costs its client a send, not a round trip; the client already ignored the answer.Polls::admit), so re-watching every client per wait cost the kernel O(clients²) per request.lib/bcachefs/src/crc32c/): SSE4.2'sCRC32on x86-64 and FEAT_CRC32'sCRC32Con AArch64, throughcore::archundertarget_feature, in one architecture file each.build/arch.rsgives TCG's x86-64 guest+sse4.2, and the aarch64 models aremaxandhost.Host, release, volume calls only (before; after changes 1–3 only, log):
The CRC itself, timed directly on the aarch64 host, interleaved in one run, per 4,088-byte node (log, source): the old table 12.4 µs, the instruction 0.65 µs. The whole-profile rerun with the instruction (log) ran at host load 11–14, and its rows that take no checksum (stat, readdir) came out slower than at the earlier run. So it is not evidence of the mutation rows' gain; the T14 A/B is.
Checks and evidence
cargo run -- --ci hostata0806ca07(the head): exit 0, 103 steps (log); ata0aba9cc8also exit 0 (log). Earlier, at04ec3f6f4, also green (log).cargo run -- --build-onlyatb0c014009: exit 0 (log).x86_64-apple-darwinunder Rosetta, which has SSE4.2 (log). Every target that links the crate compiles the intrinsics with no warning (probe). In the built x86-64 loader, everycrc32is in one function of 62 instructions with no%xmm/%ymmoperand; the built aarch64 loader contains no CRC32C encoding, as no CRC is reachable there (command, output and scan).data.rs). One is a differential: every closed name's index record against the format's own entry, under a clock that advances, across writes, a truncate, renames (one of a directory holding an open, written file), an unlink and a remount. The other is a listing whose children sort around/.fcd809961(log):virt_jobs_at_el2,virt_el1_smp,stream_holders,a_launch_gives_each_unsent_stdio_slot_a_null,a_row_reaches_its_own_state_and_no_otherandsession_lifecyclepassed.netstack_holders_exported_to_childrenstalled; see below. Earlier, at04ec3f6f4:libc_childrentoo (logs).The
session_lifecyclestallIt stalled once, at
04ec3f6f4with the host at 5.82×. Asked whether the new loop can lose a client's wakeup, I checked it against the kernel's poll semantics (kernel/src/inbox/polls.rs,mod.rs):FrameRxreads no byte past a frame, so a second queued frame keeps the handle ready.CLOSEgoing one-way reorders nothing within a connection.The loom case that was here tested its own model of
armrather than the kernel's and is removed (review r1); the argument above stands on readingpolls.rsandmod.rs, which the reviewer repeated independently.Since that stall,
session_lifecyclehas passed 9 times at the head: six alone, at 2.45× to 7.03× load (summary), and three beside other tests. The mechanism this change could have introduced is ruled out; the one stall itself is not explained.The
netstack_holders_exported_to_childrenstall (independent, recorded)It stalls at the step where the parent and a second child race on exported sockets, after the racer's spawn has completed. That step touches no served file. It reproduces on main: 1 of 6 runs at
32046264b(summary), against 2 of 6 at the head (logs). It is recorded asissues/two-holders-racing-on-an-exported-socket-can-both-stall.md.T14
Run by the coordinator at
a0aba9cc8: all 4 head boots exit 0, the judge 14 passed and 0 failed. The base is #1061's two osbench runs at the merge base32046264b. Served ratios, head against base:The 4 KiB sequential rows are within the base's spread.
mem.anon.mapread 1.48× in the one head run. Nothing here touches anonymous mapping: no kernel code changed, and the file server's resident memory grows by the index's 23 more bytes a name. #1061 reads that row flat at the same base.Since
a0aba9cc8, review r1's fixes change no measured path's cost. The rename fix changes behaviour thetestcasesmembers cover, so only that image was booted again ata0806ca07: T14, 1 boot, rc=0, the orchestrator's judge 11 passed, 0 failed (request, words).Unresolved
swapacross this change leaves clients built with the oldCLOSEwaiting for a reply. Every earlier wire change carried the same exposure.Mounted::is_symlinkandfile_mtimein the interim format now have only its own tests as callers. They stay until C3 removes the format.Polls::admitscans every poll for each watch, anddiskserverre-watches every connection on each wait.🤖 Generated with Claude Code