Working state — a note taken while the work happens, not a specification. The system as it is meant to be is in Architecture.
B3 — what the builder live-set diet cut, and where the floor is
Lane arc/builder-diet, base dev @ 8791b2c91, 2026-08-05.
Measurement instrument: tools/builder_diet_probe.ce, an in-process harness
that use()s pit-compiler::compiler directly. Anything realized through the
daemon runs the SEEDED compiler, so a daemon-lane reading describes the
compiler the daemon was built from, not the working tree
(plans/carried.md, “the seeded-compiler observation gotcha”).
Live figures are max(new_used_bytes) over the probe actor’s gc ladder
(tools/diet_gc_report.py over .pit/log/observe.jsonl). The copying
collector reports exactly the surviving bytes there, so it is the only honest
live reading a running actor produces. The probe ends with a settle step —
churning short-lived garbage until one more collection fires — because the
finished unit is assembled after the last natural GC and was otherwise never
sampled.
Calibration unit: pit-compiler/streamline.cm, ~192 KB, 109 functions,
~105k instructions, 6.8 MB canonical mcode.
The numbers
Full builder shape (compile + canonical.encode, which is what
shop_store.stage_mcode_unit does):
dev 8791b2c91 | arc/builder-diet | |
|---|---|---|
| peak live | 72.2 MB | 45.7 MB |
| peak GC capacity | 256 MB | 128 MB |
| peak physical heap block | 512 MB | 256 MB |
| bytes allocated | 1,420 MB | 842 MB |
Compile only, no canonical encode:
| dev | branch | |
|---|---|---|
| peak live | 72.2 MB | 37.7 MB |
| GC time | 632 ms | 179 ms |
What was cut
The token stream and the AST stopped riding every stage envelope.
analyze_result_from/mcode_result_from/compile_result_fromcarriedtokens,astandanalyzedforward from parse to portable emission. No reader in this tree ever read them off a stage result — every in-tree caller that wants raw tokens or a pre-fold AST callsparse_resultdirectly. Each stage now releases the form it consumed: the ~6 MB token stream goes at fold, the AST goes at emission (BEFORE streamline, which is 87% of a compile), andmcode_unit_resultrenders its claims up front so the analysis result dies with the AST.portable_body(code, consume)drops each semantic function’s canonical rows once its portable function exists. Every positioned instruction becomes a fresh stripped row plus a site row, so without this the located rows and their portable descendants were both live for the whole unit.consumeis opt-in; link-time re-encoding throughportable_unit_from_codeis unchanged.stream_ir.decode()no longer populates the row cache. Decode is a bulk walk — the caller already holds every row it built — so caching pinned a complete canonical copy of the function to the stream itself.row()still caches, which is where a cache pays.panic_outlinestopped holding the whole unit decoded. It scanned every function up front and retained each one’s rows until the splice pass at the end — even for the common unit with no panic site at all. Combined with (3) that was the “canonical rows and interned stream coexist” finding, literally. The scan now returns sites only.panic_outline.replace_siteswas O(sites x rows). It rebuilt the entire row array once per site, three fresh arrays each, with the intermediates live inside the nested calls — the largest single allocator in a compile per the gc allocation-site records. Sites are ascending and disjoint by construction, so the splices apply in one forward pass.The interned IR’s dead
source/dirtypassthrough is deleted. CP1’s review flagged it;sourceis never assigned anywhere, sorow()’s first branch was unreachable and thedirtyarray guarding it was write-only — one 8-byte parallel array per instruction, doubled by array growth.
Byte identity
tools/diet_bytes_diff.py over a 144-unit corpus (pit-compiler, shoplib,
pit-linker, pit-shop, lang, lang-endowments, logger, std, net,
http, shop_tools), compiled in-process at both revisions and compared by
canonical.hash of the pit.mcode.unit@3:
140/144 byte-identical. The 4 that move are the 4 files this lane edited, whose canonical mcode legitimately changes because their source changed. Zero unexpected moves, zero compile failures, at every intermediate commit as well as at the tip.
Where the floor is — the target is not reachable this way
The proposal’s target was ≤ 15–20 MB live. It is arithmetically out of reach
while mcode_unit_result returns the unit as one pit value:
$vm.value_footprintof the finished unit: 32.1 MB in 349,250 objects, for a 6.8 MB artifact — a 4.7x expansion that is pure record/array/text header overhead.sitesalone is ~71k records plus ~105k id texts.- The canonical encode adds the output blob on top.
blob.make(1024)starts at 128 bytes and doubles, so the final buffer holds up to 2x the used bytes, andstone(out)copies alongside it.
45.7 MB peak = 32.1 MB unit tree + 6.8 MB stoned blob + ~6.8 MB of blob over-allocation. There is essentially no pipeline residue left in the peak — what remains is the artifact and its encoding. Getting under 20 MB requires either a smaller in-memory artifact shape or a streaming encoder, and E4/E9 settled the encoder (byte identity is law), so neither is B3’s to take.
Remaining holders, with evidence
- The unit value tree, 32.1 MB. Irreducible without changing what a builder produces or how it hands it over.
- The canonical encode buffer, up to ~6.8 MB of over-allocation.
blob_growdoubles (pitlib/blob.c:29). A growth policy change would halve it, but it is a shared primitive and belongs with whoever owns blob, not here. - Encode churn: 168 MB allocated to produce 6.8 MB.
write_valuecallsnota.encodeper scalar, one fresh blob each, ~350k of them. Churn, not live — it drives GC frequency during the encode, not the block size. - The 2.5x-rounded-to-a-power-of-two capacity policy and the one-GC physical shrink lag are why 45.7 MB live still holds a 256 MB block. That is B4, and after this lane it is the larger of the two remaining factors: a 45.7 MB live set justifies 128 MB of capacity and is holding twice that.
B5’s prerequisite, restated with the new number
plans/proposal-notes/builder-memory.md §4.4 computed that an unbounded fleet
on a 4 GB budget over the 77-unit pit-shop/clerk plan needs ~53 MB physical
per builder, i.e. an 11–21 MB live peak. This lane reaches 45.7 MB live / 256 MB
physical on the WIDEST unit in the tree. B5 therefore still needs B4, and it
needs the bound to be bytes rather than a count — a 500-unit closure blows any
fixed per-actor number regardless.
Source: plans/proposal-notes/builder-diet-b3.md