Repository navigation
logkeeper: the file is a consumer of the replay, written in batches on its own thread - #1232
Conversation
A spawn's log lines cost the `/log` file server, its driver and logkeeper CPU on other CPUs, off the spawn's own path, so its latency cannot show them. The row now prints the machine's busy CPU over its repetitions, a spawn's share, from the sysinfo header every process may read: the figure an A/B of the log's write path, or of anything else a spawn reaches, is read from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n its own thread Measured on both benches (agents/spawnperf), each spawn's two log lines cost 370-475 us of CPU in /log's file server, nvme and logkeeper together on the T14 and 1.14-1.23 ms on the AMD bench, one spawn at a time: a fixed cost per write, because every round was written and synced. And the loop made every /log call itself, so a /log server that took a call and never answered left every console channel and reader silent. - The file is a thread with an offset into the replay, as the readers and carriers are (`file.rs`). It opens the volume (the opener thread goes), writes, syncs, rotates and asks a refusing volume again. The loop appends a round to the replay and says so; it makes no call on /log. - A batch is written and synced once 64 KiB are owed, once its oldest line has waited 1 s, at once for a line at Error or above (a program's panic report is its stderr, a kernel fault an Alert), and at the stop's flush. Both bounds are justified at their site. A machine that dies without a stop loses at most that second of lines below Error from the file; the console carried them. - The stop's flush and mark are asks the loop waits on within STOP_FILE, three quarters of the supervisor's bound, and gives up on with a line. From the flush until a refused stop the file thread makes no call but the mark, which it writes only over a file the flush reached and only before its deadline, so the mark stays the last write before the drivers stop. - A caught-up reader waits on a bell the replay's growth rings beside its pipe's other end leaving, so a reader that leaves while the log is quiet gives back its thread and slot at once. - `policy::Step` goes with the per-round write it named. - The manual's diagnostics page says what the file holds and what a crash costs it; the ABI's severity note says Error rather than Alert. Closes issues/the-console-waits-on-logkeepers-file-writes.md and issues/logkeeper-hears-a-reader-leave-only-at-its-next-write.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ching left them A reader of the served log is handed each round as logkeeper reads it, before the file has it, and the stop's flush answers within logkeeper's own bound even where the volume does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h costs is stated as far as it holds The loop told the file's thread of every round with a message on an unbounded channel, so a `/log` call that never returns left a queue growing by a message a round behind it. Growth is now one atomic beside the channel: the loop sends a `Grew` only when none is unread, and the thread takes the growth, urgency included, whole. The manual and the file's header said the console never loses what a crash costs the file; the console is a sink that drops under load like any other. They now say the console is not batched, and the header bounds the loss for a volume that answers. `BATCH_BYTES` states its policy. The batch test takes its instant by a checked subtraction (clippy's adopted `unchecked_time_subtraction`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7754c1b to
ab336eb
Compare
…l request staged at A fd5d2628f / B ab336ebb3
Review of #1232 at ab336eb (stacked on #1228)ArchitectureThe file becomes the replay's third consumer, on its own thread. That is the exit the issue record itself proposed, and the ownership is right:
The reader wake replaces the condvar with one bell per reader, because a reader must also wait on its pipe's other end leaving, and a condvar cannot. The kernel already provides that primitive for pipe ends, and Size: logkeeper production grows by about 190 lines ( Durability under crashAcceptable. On main, the manual's logging page already said the keeper "syncs on an alert or an interval". So losing lines up to an interval on a death without a stop was already within the requirement.
The stop mark under batchingThe design holds:
So the mark follows the flush's sync and is the last write, except for B2 below. PR description (for the orchestrator to correct): "A flush the file did not finish in time leaves FindingsBLOCKER B1 —
BLOCKER B2 —
BLOCKER B3 — SUGGESTION (optional):
The reader wakeCorrect on inspection:
I'm not asking for a row that asserts a slot coming back. To tell the new code from the old, it needs a quiet log, which a QEMU boot does not reliably give. Note: no QEMU row reads the served log; all of its readers are metal-only jobs. So the new The coalescing testIt is a good test:
Evidence
What each metal readback must show (queued at ab336eb)
B1 and B2 change only the idle wait and the stop mark's bookkeeping, not the per-round write path. If the fix diff confirms that, the testcases A/B readbacks can stand for the cost measurement. SEND BACK (B1, B2, B3) |
…already written starts no wait A round appended while a write caught up was written by it, and the ask that round had sent, taken after the write, started the batch's age with nothing owed: `next_due` then answered a time that passed while `due` stayed false, and the file thread woke at once, over and over, until the next line. A refused stop that held nothing did the same. The file is now owed a batch only where the replay holds bytes past it. The loop recorded whether the mark was written from the file thread's answer, which is no answer once the loop stops waiting, while the mark's create and sync could still land after it. A refused stop then left the mark on the volume, and a later death of the boot would read as clean and raise the anti-rollback floor. The file thread now records any mark it asked the volume for and takes it back at a refused stop; the loop keeps no copy and no longer says, when it stops waiting, that the boot will not count as clean. The mark is the volume's own: it goes in the directory of the file it vouches for. `Hub::append`'s header says what the replay is for now. The manual bounds a crash's loss for a volume that keeps pace, the cadence's header gives the file's latency to its batch, the write's text no longer shares the file's name, and the ABI's severity note leaves logkeeper's policy to logkeeper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 2 at 9108a46 (one commit on ab336eb):
Checks at 9108a46: 33 tests pass; both controls exit 101; clippy has no finding in the change. Guest rows dispatched (43067018392). Metal: the AMD A/B is judged (all pass), but busy CPU fell 70 µs a spawn (430 → 360), not 1.1–1.2 ms: the 50 spawns fit in one or two 10 ms rounds, and the old cost is one write and sync per round with lines in it (~3.5 ms on this bench, which is what the drop is). Details in the body's Checks. |
Re-review of #1232 at 9108a46 (round 2)ArchitectureThe stop mark's record now lives with the only code that calls
Previous blockers
Suggestions
FindingsBLOCKER B1′ —
BLOCKER B4 — the round's evidence is not published.
The AMD A/B explanationIt holds from the code.
It is honest about the miss. It says plainly that the drop was 70 µs, not the expected 1.1–1.2 ms. SUGGESTION, for the body:
PR text (for the orchestrator)"It writes the mark … only before the mark's deadline" should say it begins the mark only before its deadline. The next bullet already says the mark can land after the loop stops waiting. LAND-pending, once B1′ and B4 are answered
SEND BACK (B1′, B4) |
…2 controls; the AMD A/B judged; machine_reboot staged at the fixed head
…nst the replay's end), its store test and control
|
Round 3 at 9356faa (one commit on 9108a46; first pushed as 03ce0c8, its message since reworded, tree unchanged):
Evidence: |
Re-review of #1232 at 03ce0c8 (round 3)Scope: round 2's two blockers (B1′, B4), the round-3 diff, and whether the queued runs at 9108a46 still stand for the head. ArchitectureNo change.
Nothing else in the tree changed. The rest of the round is PR text and evidence. Previous blockersB1′: resolved in the code.
How each caller fares:
No other return path leaves an age with nothing owed. I traced every path that sets or keeps
The test.
The control. I applied the published
B4: resolved.
FindingsBLOCKER B5. Commit 03ce0c8's message says "a stop's catch-up did the same until its deadline". That is false, and with this repository's merge commits the message lands on
SUGGESTION. PR text (for the orchestrator)
The queued runs at 9108a46 stand for 03ce0c8The only behavioural difference between the two is one boundary: a
The landing preconditions are as round 2 listed them:
SEND BACK (B5 only: a commit-message clause; no code change is needed, and once it is corrected this is LAND pending the runs above) |
…e is caught up and synced `Volume::catch_up` returned at its budget before asking the replay for more, so a budget the replay's last line used up left the file unsynced and reported not caught up with nothing owed. The file thread then kept a batch's age that no write would end and woke at once, over and over, until the next line. The replay is now asked first, and a file with every byte is synced whatever the budget left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
03ce0c8 to
9356faa
Compare
|
Orchestrator: verified 9356faa. Its tree (2314a39a1) is identical to 03ce0c8's; only the commit message changed, and the false clause about the stop's catch-up is gone. Round 3's B5 is resolved; the code was accepted in round 3. Lands when guest 43067018392 is green on both rows, |
…machine_reboot readback at 9108a464a
…are down The T14's `machine_reboot` boot and its `spawn_cost` A/B were staged and never ran. They are owed on metal against main, in the log subsystem's record, each with what main must show; the AMD bench's A/B and `machine_reboot` passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Orchestrator: landing under the benches-down rule. Review r3 LAND (code accepted; commit message fix verified). Guest run 43092231506 at head 79b15d3: success (re-dispatched after 9356faa's catch_up change). Owed on metal recorded in |
Built on #1228, which has landed; the diff against
mainis this branch's own. Status: round 3 (9356faa) answers B1′ on round 2 (9108a46), which answered B1–B3; the head 79b15d3 adds only the owed-on-metal record. Host checks done; guest rows green at 9108a46 and queued at the head; metal is no landing condition while the benches are down, and what is owed on metal is listed below. The speed-up is not yet measured on the T14, and on the AMD bench by one boot per arm.Problem
/log's file server,nvmeandlogkeeperon the T14, and 1.14–1.23 ms on the AMD bench, one spawn at a time (about 40% of the child's own CPU there). The cause:logkeeperwrote and synced every round. That the cost is fixed per write is an inference: it falls per line as concurrency rises.issues/the-console-waits-on-logkeepers-file-writes.md). The loop made every/logcall itself. A/logserver that took a call and never answered left every console channel and reader silent, with no bound.issues/logkeeper-hears-a-reader-leave-only-at-its-next-write.md). A caught-up reader learned that its reader had gone only at the next write.Resulting behaviour
The file is a consumer of the replay on its own thread (
system/logkeeper/src/file.rs), as the readers and carriers already were./log: itsLogholds noVolume, and its only calls into the file are a send on an unbounded channel and the stop's two waits, each bounded.Grewis sent only when none is unread. A thread held in a/logcall has at most that one queued, rather than one a round for as long as the call lasts.Batches. A batch is written and synced when one of these holds:
BATCH_BYTES);BATCH_AGE);Erroror above: a program's panic report is its stderr, and a kernel fault is anAlert;A batch's age starts only for bytes the file is owed: a round a write has already taken, with the round before it, starts no wait. A catch-up that spends its 1 MiB budget on the replay's last line is caught up and synced, so that end leaves no age behind either.
Both bounds are justified at their site.
Stop. The flush and the stop mark are requests the loop waits on for at most
STOP_FILE, three quarters of the supervisor'sFLUSH_MS. If the file does not answer, the loop gives up, says so and goes on.Readers. A caught-up reader waits on two events: a bell the replay's growth rings, and its pipe's other end going. A reader that leaves while the log is quiet gives back its thread and its
MAX_READERSslot at once.spawn_costprints the machine's busy CPU a spawn, from the sysinfo header every process may read, beside its latency. The log's cost runs on other CPUs, off the spawn's path, so latency cannot show it; this figure is what the A/B below reads. It is the stack's first commit, so the A image carries it too.What a crash can cost, against #1196 and the manual
/logat most the unwritten batch.Error. Lines atErroror above are written at once.FLUSHED, as before.finishedfalse, so the mark is withheld, as it was before for a volume that was not open. A flush that begins in time and is cut short by the deadline says the shortfall in the file and syncs it, and the mark goes over that file, as on main.docs/src/architecture/diagnostics.mdnow says the file is written in batches and what a crash costs it while the volume keeps pace; the boot page's account of the mark is unchanged and still true. The ABI's severity note no longer restates logkeeper's write policy, which isfile::urgent's.Decisions
/logholds only its own thread.Error, not onlyAlert, writes at once. A Rust program's panic message is stderr, which isError. A program that writes heavily to stderr is therefore synced every round, as before this change. Spawn lines areInfo.FLUSHis the explicit sync and writes at once, as doError/Alertlines and a refused stop's held lines. A protocol message can be added when a caller needs one.policy::Stepis deleted with the per-round write it named.degradedis timed over a whole write (review S4, declined): that includes a catch-up of up to 1 MiB and a rotation, andLOG_WRITE_BUDGETis 5 s, many times what a stick takes for a mebibyte (an estimate, not measured here).Checks
All on a macOS arm64 host unless named. Under the owner's testing policy the host checks are logkeeper's own; the merge queue runs the full host check.
cargo test --manifest-path system/logkeeper/Cargo.tomlat 9108a46: exit 0, 33 passed (b-test.log). New:a_batch_falls_due_at_its_size_its_age_or_an_urgent_line,the_file_takes_the_replay_a_batch_at_a_time,nothing_reaches_the_file_between_the_stops_flush_and_its_refusal,rounds_the_file_has_not_taken_are_one_ask,a_growth_already_written_starts_no_wait(B1),a_refused_stop_takes_back_the_mark_it_asked_for(B2).cargo test --manifest-path system/logkeeper/Cargo.tomlat 9356faa: exit 0, 34 passed (r3-test.log). New:store::tests::a_budget_spent_on_the_replays_last_line_is_caught_up_and_synced(B1′): four lines in the replay, a budget of exactly their bytes; the catch-up answers caught up and the file is synced. Its control, the budget's return put back before the replay is asked, fails it at "the file has every byte" (exit 101;r3-b1p-control.log, patch). Clippy at 9356faa: the same four findings onmain, none in the change (r3-clippy.log).owewithout its early return, which is the oldtook_growthandresume(failsa_growth_already_written_starts_no_wait:b1-control.log, patch), andresumethat does not take the mark back (failsa_refused_stop_takes_back_the_mark_it_asked_for:b2-control.log, patch);File::grewasking at every round, 100 asks where 1 is owed, at ab336eb (coalesce-control.log, patch); at a7ed602, the batch policy without its bounds (batch-control-1.log) anddue()without the stop guard (batch-control-2.log). The code those two controls cut is unchanged since a7ed602.cargo clippy --manifest-path system/logkeeper/Cargo.toml --all-targetswith the workspace's adopted lints at 9108a46: no finding in this change (b-clippy.log). The four left are in code this branch does not touch and are onmainas they are here. No gate lints a guest program (issues/userland-programs-are-never-linted.md).lib/logstreamandsdk/abi, whose edits are doc comments with no code block.git diffhasha7f24befabeain each round-2 log's header and of the commit) and 9356faa (a915dacfe05b; first pushed as 03ce0c8, whose message was then reworded with the tree unchanged).a_clean_stop_proves_its_image the_log_lands_on_the_boot_stick: the stop mark through the file thread, and the file under a load through the stop's flush on usbd's disk.guest-43041114433.log).guest-43051944401.log). The mark proved the image on the next loader pass; the stick held 2,462,470 bytes of the boot's log in 6 files.guest-43067018392.log).Volume::catch_up's end at its budget, so 43067018392 is not at a code-identical head; added here when it ends.spawn_costfigure, no batching), B = ab336eb. Every one of these boots ends with a stop, and its verdict refuses a boot with no stop mark or whose log lacks the supervisor's stop line.spawn_cost(A, B) andmachine_reboot(B) pass the harness's judge (judge-a-amd.log,judge-b-amd.log, lines inamd-ab-extract.txt).spawn_cost: A min 224 µs, median 288 µs, the machine busy 430 µs a spawn; B min 228 µs, median 276 µs, busy 360 µs a spawn. Latency did not move beyond the two runs' spread./logserver,nvmeandlogkeeperCPU over the roughly 83 rounds of an 830 ms build at j1). On that figure the saving is about 3.5 ms of CPU per busy 10 ms round, down to a write a second or a 64 KiB batch; while the log is busy every round, that projects to about a third of one CPU (a projection, not measured).spawn_cost, which packs its spawns into a round or two, shows it once.test_rs_ps2server_holdexited 0, so the served-log reader got its line through the new wait; no reader was refused.machine_rebootat 9108a46: passed, withtest_rs_ps2server_holdexit 0, the reset-register line, and none of the stop or slowness lines (amd-machine-reboot-9108a464a.txt).issues/redesign-the-log-subsystem.md, to be run against main once a bench returns):machine_rebooton the T14: main passes the boot verdict with the stop mark and the supervisor's stop line in the file,machine_reboot's reset-register line, andtest_rs_ps2server_holdexit 0, with no "had not answered the stop's flush", "stop mark was asked … after its time", "answers but slowly" or "refusing reader" line.spawn_cost's A/B on the T14, main before this PR against main after it, back to back: "the machine busy … a spawn" lower after, by about one write and sync per logkeeper round the loop spans; min and median no worse than the two runs' spread; both boots passing with the stop mark.Evidence: ToyOSOrg/evidence
pr-1232: the guest run at 9108a46 and the AMDmachine_rebootat 36ba2576; round 3 at 8886efce (its message names a wrong ToyOS hash; 5f48598d corrects it to 03ce0c8, and 247ce08f records that 03ce0c8 is now 9356faa, the same tree), round 2 at f2b0acee, each linked above per check; earlier rounds' files are on the same branch.Risks
Errorfrom the file. The one metal row of that kind (the_disk_survives_a_reset_under_load) needs a mebibyte of its load's lines on the disk, which batches of 64 KiB reach; not run here.OTHER_END_GONEand the event bell. Host tests cannot reach either; the metalmachine_rebootboot runs a reader, and no row asserts a slot coming back.spawn_cost's figure counts the machine's busy CPU over its 50 repetitions, which span a round or two of logkeeper's, so it shows the per-round write once rather than per spawn. A paced variant, a spawn a round, would show the per-round cost directly; not built here.sdk/abicomment edit rebuilds the sysroot and republishestoyos-abi.🤖 Generated with Claude Code