Repository navigation
issues: the all-CPU build waits on the frame allocator's lock, for the 2 MiB windows - #1265
Conversation
…e 2 MiB windows A diagnosis boot a bench split each build-shaped child's CPU by system call, fault, lock spin and shootdown wait, at j1 and one child a CPU. The growth in the children's CPU is 59 % (T14 j8) and 72 % (AMD j16) their four 2 MiB directory windows' SHM_CREATE, and 12 and 14 % their faults; the frame allocator's one lock is 99 % of every contended lock wait, 1.06 and 7.1 CPUs spinning for the whole round, and no shootdown was waited on. The spawn record carries the attribution in place of "unmeasured", and names the frame allocator and the window as the parallel build's owners. The frame allocator's record and the window's record carry their parts and a proposal each: batched draws under one hold; a window as large as what it still carries, or a region's frames drawn at first touch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1265 at a11cab8 (base main 45935f4), records onlyArchitectureThe records' direction is sound:
None of this needs a ruling from the owner. The manual already says pages are populated lazily ("Reservation, commitment and population are separate … pages are populated lazily"), so today's eager shared memory is the divergence. The Evidence checkedI ran
I checked every figure in the three records against those outputs. These all match:
The code facts also hold: FindingsBLOCKER 1. The decisions are presented as open proposals.
Record these as decisions, in that order, with their real decider: the orchestrator, 2026-11-11, not the owner unless the owner made them. Make the exits match. The window record's exit says "read again after the change" and should name which change. The decided lazy-draw work also needs a home whose kind fits it. The window record is BLOCKER 2. Figures that are false as written.
BLOCKER 3. Inferences without a label.
SUGGESTION. Lock spin across the whole machine is about a third of the children's CPU growth on the T14 (244 of 733 ms) but most of it on the AMD bench. The records' "the build waits on the frame allocator's lock" fits the AMD bench far better. One sentence would keep the batched draw's expected gain on the T14 from being overstated. The growth in user time and exit (+43 % and +85 %) is not lock spin either. SUGGESTION. The spawn record does not carry the probe's overhead (the SUGGESTION. Break SEND BACK. |
…e figures as measured The three follow-ups are decided, in order: a draw of many frames takes the allocator's lock once, with per-CPU caches held until it is measured; a shared-memory region's frames are drawn at first touch, promised when it is made, with a device's run drawn whole, which gets a record of its own (a defect, assigned); and the window's size waits for the file-call work. Figures corrected: on the T14 `SHM_CREATE`'s growth is 47 % lock spin (83 % on the AMD bench), not most; 95 us is one window, and 17 and 19 % are a child's four; the allocator lock's own wait is 1.05 CPUs on the T14. The 2 MiB size and "most of what the build loses" are labelled as inferences, the probes' slowing of the round is stated, and the T14's lock spin is a third of its children's growth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2 of #1265 at 9cbcbf6 (records only, focused)ArchitectureThe direction is unchanged and sound. The batched draw comes first, then a region's frames at first touch, and then the window's size after the file-call work. The lazy draw now has its own Round-1 blockers
FindingsBLOCKER 1.
BLOCKER 2 (conditional on the orchestrator's answer). The same record,
The orchestrator settles both points. This blocks only where the record misstates the decision. SUGGESTION. The records link the evidence at 36117dda5, whose SUGGESTION. Once #1268 lands, the allocator record's first decision is built. Whichever of #1265 and #1268 lands second should say so in that record and leave only the measurement as owed. SEND BACK for BLOCKER 1, a one-phrase fix, and for BLOCKER 2 if any part of it misstates the decision. |
…res as SHM_CREATE's The first-touch record states the orchestrator's decision as made: one draw, `pmm::alloc_promised`, for a region's hole (`SharedMemObject::page()`, into the region's slot) and a MEMORY volume's hole (into the pager's index under its lock), two stores because one is revoked and the other never; and an inbox's rings stay drawn whole beside a device's run, since the kernel writes them in interrupt context, in the decision and its exit. The build plan (on #1259) leaves the decision. Its figures are `SHM_CREATE`'s, and that those are the windows is labelled an inference. Every record links the evidence whose script sorts deterministically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 3 of #1265 at 3e79317 (records only, focused on round 2's points)ArchitectureThe direction is unchanged. This round is +22/−16 lines of markdown in four records. As a records-only change it needs a consistency review, not a build. Round-2 points
FindingsNone. Note on landing order#1250's and #1268's new records-only commits each add an Owed on metal: bullet at the same place where this PR adds its paragraphs:
Whichever lands second resolves the conflicts. As the PR body says, the second of this PR and #1268 also records the batched draw as built. LAND |
The frame allocator's record says the first decision is built here, #1265 having landed first, and names what main must show on metal for it: the benches are down, so the A/B and the lock census are owed rather than run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SharedMemObject::create` drew and zeroed every frame of a region before it returned, so a region cost its whole size however little of it a process touched: a served directory's 2 MiB window, four of them a build child, was 95 us a window on the T14 and 133 us on the AMD bench with one child at a time, and most of what the all-CPU build's children add in CPU, by the spawn diagnosis. The orchestrator decided (recorded in #1265): promised whole at creation, drawn at first touch, with a device's run and an inbox's rings drawn whole. A region's pages are slots (`shm::slot::Slot`), each a hole until its first touch. `SharedMemObject::page()`, which every consumer of a page's frame goes through (an object range's fault, the kernel's reads and writes, an inbox's words), draws a hole's zeroed frame with `pmm::alloc_promised()` against the promise `create` made, and installs it with one compare-exchange; a toucher that loses the race gives its frame and its promise back (`pmm::discard`) and is handed the winner's. That draw is the one map7b's N4 calls for a MEMORY volume's hole, into its pager's index. `page()` states who may call it: never in interrupt context, never holding the allocator's lock. An inbox's rings are made with `create_whole`, which touches every page at once, since the kernel writes them in interrupt context; the clock page's one page is drawn by the boot's own `page(0)`. A region's end frees what was drawn, 512 at a time, and gives back the promises of the pages never touched. `pmm::alloc_promised_many` and `pmm::free_all` lose their only callers and go. The slot is compiled a second time by `kernel-loom`: `shm_slot` models two touches racing for one page (one frame each is handed, every other draw goes back once, a frame comes with its zeros), and `slot-install-relaxed` is its negative control. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frame allocator's record says the first decision is built here, #1265 having landed first, and names what main must show on metal for it: the benches are down, so the A/B and the lock census are owed rather than run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SharedMemObject::create` drew and zeroed every frame of a region before it returned, so a region cost its whole size however little of it a process touched: a served directory's 2 MiB window, four of them a build child, was 95 us a window on the T14 and 133 us on the AMD bench with one child at a time, and most of what the all-CPU build's children add in CPU, by the spawn diagnosis. The orchestrator decided (recorded in #1265): promised whole at creation, drawn at first touch, with a device's run and an inbox's rings drawn whole. A region's pages are slots (`shm::slot::Slot`), each a hole until its first touch. `SharedMemObject::page()`, which every consumer of a page's frame goes through (an object range's fault, the kernel's reads and writes, an inbox's words), draws a hole's zeroed frame with `pmm::alloc_promised()` against the promise `create` made, and installs it with one compare-exchange; a toucher that loses the race gives its frame and its promise back (`pmm::discard`) and is handed the winner's. That draw is the one map7b's N4 calls for a MEMORY volume's hole, into its pager's index. `page()` states who may call it: never in interrupt context, never holding the allocator's lock. An inbox's rings are made with `create_whole`, which touches every page at once, since the kernel writes them in interrupt context; the clock page's one page is drawn by the boot's own `page(0)`. A region's end frees what was drawn, 512 at a time, and gives back the promises of the pages never touched. `pmm::alloc_promised_many` and `pmm::free_all` lose their only callers and go. The slot is compiled a second time by `kernel-loom`: `shm_slot` models two touches racing for one page (one frame each is handed, every other draw goes back once, a frame comes with its zeros), and `slot-install-relaxed` is its negative control. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frame allocator's record says the first decision is built here, #1265 having landed first, and names what main must show on metal for it: the benches are down, so the A/B and the lock census are owed rather than run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SharedMemObject::create` drew and zeroed every frame of a region before it returned, so a region cost its whole size however little of it a process touched: a served directory's 2 MiB window, four of them a build child, was 95 us a window on the T14 and 133 us on the AMD bench with one child at a time, and most of what the all-CPU build's children add in CPU, by the spawn diagnosis. The orchestrator decided (recorded in #1265): promised whole at creation, drawn at first touch, with a device's run and an inbox's rings drawn whole. A region's pages are slots (`shm::slot::Slot`), each a hole until its first touch. `SharedMemObject::page()`, which every consumer of a page's frame goes through (an object range's fault, the kernel's reads and writes, an inbox's words), draws a hole's zeroed frame with `pmm::alloc_promised()` against the promise `create` made, and installs it with one compare-exchange; a toucher that loses the race gives its frame and its promise back (`pmm::discard`) and is handed the winner's. That draw is the one map7b's N4 calls for a MEMORY volume's hole, into its pager's index. `page()` states who may call it: never in interrupt context, never holding the allocator's lock. An inbox's rings are made with `create_whole`, which touches every page at once, since the kernel writes them in interrupt context; the clock page's one page is drawn by the boot's own `page(0)`. A region's end frees what was drawn, 512 at a time, and gives back the promises of the pages never touched. `pmm::alloc_promised_many` and `pmm::free_all` lose their only callers and go. The slot is compiled a second time by `kernel-loom`: `shm_slot` models two touches racing for one page (one frame each is handed, every other draw goes back once, a frame comes with its zeros), and `slot-install-relaxed` is its negative control. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frame allocator's record says the first decision is built here, #1265 having landed first, and names what main must show on metal for it: the benches are down, so the A/B and the lock census are owed rather than run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SharedMemObject::create` drew and zeroed every frame of a region before it returned, so a region cost its whole size however little of it a process touched: a served directory's 2 MiB window, four of them a build child, was 95 us a window on the T14 and 133 us on the AMD bench with one child at a time, and most of what the all-CPU build's children add in CPU, by the spawn diagnosis. The orchestrator decided (recorded in #1265): promised whole at creation, drawn at first touch, with a device's run and an inbox's rings drawn whole. A region's pages are slots (`shm::slot::Slot`), each a hole until its first touch. `SharedMemObject::page()`, which every consumer of a page's frame goes through (an object range's fault, the kernel's reads and writes, an inbox's words), draws a hole's zeroed frame with `pmm::alloc_promised()` against the promise `create` made, and installs it with one compare-exchange; a toucher that loses the race gives its frame and its promise back (`pmm::discard`) and is handed the winner's. That draw is the one map7b's N4 calls for a MEMORY volume's hole, into its pager's index. `page()` states who may call it: never in interrupt context, never holding the allocator's lock. An inbox's rings are made with `create_whole`, which touches every page at once, since the kernel writes them in interrupt context; the clock page's one page is drawn by the boot's own `page(0)`. A region's end frees what was drawn, 512 at a time, and gives back the promises of the pages never touched. `pmm::alloc_promised_many` and `pmm::free_all` lose their only callers and go. The slot is compiled a second time by `kernel-loom`: `shm_slot` models two touches racing for one page (one frame each is handed, every other draw goes back once, a frame comes with its zeros), and `slot-install-relaxed` is its negative control. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frame allocator's record says the first decision is built here, #1265 having landed first, and names what main must show on metal for it: the benches are down, so the A/B and the lock census are owed rather than run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`SharedMemObject::create` drew and zeroed every frame of a region before it returned, so a region cost its whole size however little of it a process touched: a served directory's 2 MiB window, four of them a build child, was 95 us a window on the T14 and 133 us on the AMD bench with one child at a time, and most of what the all-CPU build's children add in CPU, by the spawn diagnosis. The orchestrator decided (recorded in #1265): promised whole at creation, drawn at first touch, with a device's run and an inbox's rings drawn whole. A region's pages are slots (`shm::slot::Slot`), each a hole until its first touch. `SharedMemObject::page()`, which every consumer of a page's frame goes through (an object range's fault, the kernel's reads and writes, an inbox's words), draws a hole's zeroed frame with `pmm::alloc_promised()` against the promise `create` made, and installs it with one compare-exchange; a toucher that loses the race gives its frame and its promise back (`pmm::discard`) and is handed the winner's. That draw is the one map7b's N4 calls for a MEMORY volume's hole, into its pager's index. `page()` states who may call it: never in interrupt context, never holding the allocator's lock. An inbox's rings are made with `create_whole`, which touches every page at once, since the kernel writes them in interrupt context; the clock page's one page is drawn by the boot's own `page(0)`. A region's end frees what was drawn, 512 at a time, and gives back the promises of the pages never touched. `pmm::alloc_promised_many` and `pmm::free_all` lose their only callers and go. The slot is compiled a second time by `kernel-loom`: `shm_slot` models two touches racing for one page (one frame each is handed, every other draw goes back once, a frame comes with its zeros), and `slot-install-relaxed` is its negative control. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
The all-CPU build (
work.build_shaped.j<cpus>) is 2.22x Linux on the T14 and 2.57x on the AMD bench while theone-at-a-time build is ahead on both, and the spawn record called the loss unmeasured.
Result
One diagnosis boot a bench (branch
spawnperf-split, c2f345b: main 34cb4c9 with MEASUREMENT ONLY commits, neverto land) split each child's CPU by system call, fault, lock spin and shootdown wait, at
j1and one child a CPU,three rounds each, and logged every contended
sync::Lockwait by site.j8) and from 2.83 to 15.5 ms (AMDj16) for the same 53system calls and 90 faults.
SHM_CREATEis 59 % and 72 % of that growth, faults 12 % and 14 %, every other system call under 2 %.of 16 (AMD) for the whole round. The next lock (the process table in
process_data) waits under a hundredth as long.SHM_CREATEs are, by inference from the SDK, the 2 MiB windows of the child's four directoryconnections, each 512 frames drawn one at a time under that lock and zeroed: 95 us (T14) and 133 us (AMD) a
window even at
j1, where a child's four are 17 and 19 % of its CPU.The orchestrator decided the follow-ups (2026-11-11), in this order, and the records carry them as decisions:
(the frame allocator's record).
rings (which the kernel writes in interrupt context) stay drawn whole. One draw,
pmm::alloc_promised, serves aregion's hole (
SharedMemObject::page(), into the region's slot) and a MEMORY volume's hole (map7b's N4, into thepager's index under its lock): two stores, since a pager's page is revocable and a region's is not. A record of its
own,
a-shared-memory-region-draws-and-zeroes-every-frame-when-it-is-made.md(defect, assigned), since thewindow's record is
tooling.The spawn record carries the attribution and names the frame allocator, the shared-memory object and the window as
the parallel build's owners. Lock spin is a third of the children's growth on the T14 and 72 % on the AMD bench, so
the lock bounds the AMD build far more than the T14's.
Evidence
https://github.com/ToyOSOrg/evidence/tree/a22307591cfdefcf9fa1fef2ae5d1de79b926e9e/spawnperf-diag, which the records
link.
The scripts regenerate the tables from the published log lines byte for byte (
attrib.py,split.py, exit 0).The instrumentation adds a probe to every system call once on; the
j16round's wall reads 331 ms against boot 2's313 to 316 ms without it, so the shares are of a slightly slowed machine.
Checks
Records only. No code changes. Whichever of this and #1268 lands second updates the frame allocator's record to
say the batched draw is built, leaving its measurement owed.
🤖 Generated with Claude Code