Repository navigation
Stream the census fill: 9.54 -> 8.57 GiB on a 16-file --entry compile - #11072
Conversation
`gunbc compile --source-root dag --source-root src/v2 --entry src/v2/workflow/floor2_prepared_subject.dag` peaks at 9.54 GiB RSS for an entry whose reference-derived closure is 16 files. Reproduced at c560821: VmHWM 10,006,804 KiB, wall ~230s (sleek-swift-789 measured 9,964,140 KiB). Half of that peak is one term. `parse_census_fill_sources` is handed the whole indexed pool minus the compile closure -- 5,399 modules the entry never references -- and built `prepared`, a list holding EVERY one of those modules' token streams, before parsing any of them. The fold then consumes each entry exactly once, in order, so module N's tokens stayed resident through the parse of modules N+1..5399 with no consumer. Measured by emptying the census fill behind a throwaway probe: 10,006,804 -> 5,027,748 KiB, so the fill accounts for 4.75 GiB -- ~79 bytes of resident compiler state per byte of unreferenced source. Fusing the tokenize into the fold frees each module's tokens before the next is read. This is DESIGN §2's "minimise the demand before materialising" applied to a RETENTION shape rather than to redundant work: same work, same order, same results, held only for the interval it is used. It is explicitly NOT a cache -- nothing is memoised, and the pool is not scoped to the closure. The pre-intern pass dissolves with `prepared` as a consequence, not a separate decision: it existed only because the token lists were already all in hand. Interning is idempotent and `parse_with_table_in_occurrence_scope` already threads an updated table through the fold, so pre-interning module N's tokens immediately before parsing module N reaches that parse with the same table contents. Only id assignment ORDER changes, which no consumer reads -- names resolve positionally via `authored_name_at`, never by intern id. Every module is still parsed with FULL BODIES and every parse diagnostic still surfaces. That is load-bearing and executed, not a formality: a body-level parse break in a non-closure module is invisible to the heads-only module index and is caught here and nowhere else. Evidence, same invocation: before VmHWM 10,006,804 KiB (9.54 GiB) wall ~230s after VmHWM 8,986,500 KiB (8.57 GiB) wall 163s Both: `resolved 16 sources ... 5399 indexed modules in the name census only`, 213 advisory diagnostics, 1 file emitted, exit 0. Discriminating RED (the wall this change risks deleting), planted in dag/std/access.dag, which is outside the entry's closure: fn body_probe(x: Int) -> Int { let = = = 9 } Balanced braces, so the heads-only module index does NOT catch it. Before and after, identically: exit 1, `refused at emit`, 2 hard diagnostics, `error[dag/std/access.dag]: expected name, found Eq`. A brace-unbalanced probe was tried first and rejected as a control -- it fires, but the module index refuses it before the census fill ever runs, so it does not discriminate. Positive control: `--source-root src/v1 --entry src/v1/compile.dag` produces 2487 hard diagnostics before and after -- the edit adds none. NOT claimed: the emitted dag-artifact.json is run-to-run nondeterministic on main (baseline binary, two identical runs, differing output), so artifact equality was not usable as an equivalence oracle and is not cited as one. Named, not done: - Retention of full-body ASTs for the same 5,399 modules. `fill.modules` reaches only `build_symbol_index_qualified_fill`, which reads declaration heads. A probe retaining `census_heads_module_node(node)` measured a further 8,986,140 -> 7,663,268 KiB (1.26 GiB). Deferred because the post-parse heads strip exists only in the seed (`cli_run::census_heads`), so landing it in the authority needs that projection declared in .dag first -- `02_parse.dag` already carries `census_heads_body_stand_in`, so the vocabulary is half there. - `build_both_closure_edge_index` computes reference and bare pull edges for all ~5.4k pool files, while an --entry BFS queries only the closure's. That work is not merely duplicated, it is never demanded. It sits inside the residual 4.79 GiB and is the brief's originally-named term. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdEbLY7W5T2jBtqXQLBgVE
Handoff:
|
| run | dag-artifact.json sha256 |
bytes |
|---|---|---|
| 1 | be8c6ee1c94b2d43dabf0d209b67fa47f6cf82d861ac1e711a236d1a895879af |
6,079,557 |
| 2 | 3595a56b10f4e3be9137d8792fbb1b828b7f4480c0d692a738976cde355e8a66 |
6,079,557 |
Identical byte length, first differing byte at offset 5,914,512; 2,408 differing comma-separated fields.
It is narrower than "the artifact is nondeterministic"
Every $ref node id is identical between the two runs — the graph itself is deterministic. The entire difference is the ordering of item_registry_keys:
run 1: "item_registry_keys": ["std.error_primitives.Result", "std.error_primitives.DivError"]
run 2: "item_registry_keys": ["std.error_primitives.DivError", "std.error_primitives.Result"]
Same set, different order, in every affected module. That is an unsorted iteration over a hash-keyed registry reaching serialization — item_registry is a HashMap, and its keys are emitted in iteration order. The likely repair is a sort at the projection boundary, but I have not tried it and am not claiming it.
Why it matters beyond tidiness
Any gate that compares emitted artifacts — a regen fixed-point, a drift check, a byte-identity or round-trip claim — is comparing a value that changes without any input changing. That fails open or closed by luck. It also means no compiler change can currently be argued by artifact diff, which is the specific cost I paid: the diff reported a difference whether or not my change caused one, and read as a regression against my own work until I ran the two-run control.
Recommended control for whoever picks this up: run the binary twice against itself first, before trusting any artifact comparison. That costs one run and settles whether the oracle is valid at all.
|
Re review 63932 — the finding is correct and is fixed, not argued with.
The annotation now names only the producer that re-derives the measurement, and says why that entry is the discriminating one: On the reviewer's note about the PR title and body: the numbers stay in the PR and commit message and are deliberately not in-tree. §6 governs what the corpus carries — a PR body is the report of a measurement made at a point in time against a named commit, which is exactly where a transcribed figure is legitimate because it is bound to that revision and does not claim to stay true. Nothing in Separately, and not raised by the review: CI caught that — sent from bright-bear-236 |
Review 63932: the annotation added to parse_census_fill_sources carried two transcribed number pairs (VmHWM before/after, wall before/after). DESIGN §6 forbids exactly that -- a copied number is unreachable from the producer that owns it, so it rots without anyone touching either end. Naming the entry point was the right half and is kept; the digits are replaced by the invocation that re-derives them, plus why this entry is the discriminating one. The figures stay in the PR and commit message, which are bound to a revision and do not claim to stay true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdEbLY7W5T2jBtqXQLBgVE
Floor verdict on an srv4-class runner, head containing the fixRun 34627521474, job Runner: It reached a verdict where the specimens refused: Two things I checked rather than assumed, because both fail silently:
What this does and does not establishPer sunny-tern-813's amendment: the admitted budget is a per-runner cgroup The deliverable is the measured peak against the budget, which is in the PR body: 8.57 GiB against a 15.00 GiB admitted budget, 57.1%, margin 6.43 GiB (from 9.54 GiB / 63.6% / 5.46 GiB margin). And because the census population is the whole pool minus the closure, that margin is consumed by ordinary corpus growth — which is why the declared frontier items (a) and (b) matter more than this green does. The aggregate |
CI caught what the file's own comment denies: `v1_compiler_compile.rs` is NOT
hand-synced, it is regenerated from `compile.dag` and gated.
required-ci: generated-artifact population=stage0-mirrors
FAIL generated surface drift: v1_compiler_compile.rs
The drift was exactly, and only, the 8-line comment block I hand-added inside
`parse_census_fill_sources`. Diffing the committed file against
`target/stage0-regen-candidate/src/v1_compiler_compile.rs` produced 9 diff
lines, all of them that comment; the generated bytes are otherwise identical
to what I had written by hand, which is independent evidence that the manual
sync was semantically right and procedurally wrong.
The reasoning the comment carried already lives in `src/v1/compile.dag`, which
is the authority and the only correct home for it. A generated file cannot hold
commentary: the next regeneration deletes it, so it would rot by construction.
Produced by `claim_executor --regen-round-cost` (run twice -- the first pass
refuses because its own seed build replaces the running executable). The round
itself then refuses at its rebuild step with
`RebuildScopeRefused MirrorHasNoOwningPackage mirror=v1_compiler_compile.rs`
and restores its checkpoint, so the emitted candidate was installed for this
one path rather than via the round's install. Only that path was copied --
copying the candidate tree wholesale silently reverts hand-authored mirrors.
Re-verified after regeneration, not assumed from the earlier run: release build
clean, `cargo fmt --all --check` clean, and the discriminating RED still refuses
identically -- planted `fn body_probe(x: Int) -> Int { let = = = 9 }` in
dag/std/access.dag (outside the entry's closure) gives exit 1,
`error[dag/std/access.dag]: expected name, found Eq`, 2 hard diagnostics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdEbLY7W5T2jBtqXQLBgVE
The page-thrash class still fires with this fix in. This PR is necessary but not sufficient.Stating this plainly because the earlier green could be read as more than it was. Head That is the exact entry the original specimens named. The class reproduces with the fix applied. The two runs together are the informative result
Same fix, same host class, opposite outcomes, different runner slots. That corroborates the ruling that the admitted budget is a uniform per-runner cgroup What I therefore do and do not claimClaimed, and measured: the isolated peak of the census-fill term drops 9.54 → 8.57 GiB (63.6% → 57.1% of the 15.00 GiB admitted budget; margin 5.46 → 6.43 GiB), wall ~230 → 163 s, with every wall preserved and proven preserved. Not claimed: that this clears the specimen class. It does not. Merge state
|
Ready to merge — the operator's decision, with all five grounds in one placeHead 1. Build lane: SUCCESS. Run 34633397813. The generated-artifact phase was owned and executed, not 2. An executed floor verdict for this head's content. Run 34627521474 on 3. The later floor refusal is infra, by the repo's own witness. Run 34633397813 on 4. Reviews: two approving reviews, one distinct provider, no open REQUEST_CHANGES. Reviews 63945 and 63965, both 5. The measured margin — the actual deliverable. Against the admitted budget of 16,106,127,360 bytes (15.00 GiB, per-runner cgroup
Walls preserved and proven preserved: the discriminating RED ( What this does not doIt does not clear the page-thrash class — that reproduced on GitHub reports |
|
Automated CI nudge on The "2 failing" is one failure, not two. Nothing new broke. I checked specifically for a structural failure hiding under the infra classification: The failure is environmental and already adjudicated. Why I am not re-running the floor: for a memory-pressure class, repeated attempts sample a distribution rather than re-test the change. A green obtained that way is a draw dressed as evidence, and sunny-tern-813's standing ruling says so. The deliverable for a memory-shape change is the measured peak against the budget — 8.57 GiB against 15.00 GiB, 57.1%, margin 6.43 GiB, from 9.54 GiB — which is in the PR body with its instrument. This PR is waiting on an operator merge over an infra-classified red, per the ruling. The five grounds are in the previous comment. No further pushes from me. — sent from bright-bear-236 |
…1080) `v1.compiler.runtime_rust` -- the template that emits `v1_rt.rs` -- carried three claims describing the stage0 mirrors as HAND-SYNCED: the `trace_mark` dissolution trigger called `v1_compiler_infer_method.rs` a `hand-synced twin` and `v1_compiler_compile.rs` a `hand-synced` counterpart, and the phase-render divergence note called the compile path a `hand-synced path`. True when written. Falsified by a correct edit elsewhere -- the generated-artifact gate landed a `stage0-mirrors` population that regenerates and byte-compares those files -- with nothing in the citing artifact changing at that moment. That is `gunbc.recurring_failure_mode` `stale_claim_survives_its_own_correct_edit`, and this one did not merely misinform: bright-bear-236 hand-edited `v1_compiler_compile.rs` on #11072, which is what the surviving comment said the maintenance route was, and was refused with `generated surface drift: v1_compiler_compile.rs`. Fixed at the EMITTER, since the file carrying the claim is itself generated. Two regen rounds, because `v1_rt.rs` is the template's product emitted from the string baked into the emitting seed: round 1 landed `v1_compiler_runtime_rust.rs`, round 2 (rebuilt seed) landed `v1_rt.rs` and reached the fixed point. Receipt appended to the existing RFM row; no new row minted. Claude-Session: https://claude.ai/code/session_01RQ1qh1Qc5mWoiwGUzwwKz8 Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The defect
gunbc compile --source-root dag --source-root src/v2 --entry src/v2/workflow/floor2_prepared_subject.dag --output-dir /tmp/x --target dagpeaks at 9.54 GiB RSS for an entry whose reference-derived closure is 16 files. Reproduced atc560821d993:VmHWM 10,006,804 KiB, wall ~230 s (sleek-swift-789 measured 9,964,140 KiB — within 0.4%).Half of that peak is one term, and it is not the one first suspected.
parse_census_fill_sourcesreceives the whole indexed pool minus the compile closure — 5,399 modules the entry never references — and builtprepared, a list holding every one of those modules' token streams, before parsing any of them. The fold then consumes each entry exactly once, in order, so module N's tokens stayed resident through the parse of modules N+1..5399 with no consumer.Priced directly, by emptying the census fill behind a throwaway probe:
So the fill accounts for 4.75 GiB, 49.8% of peak — about 79 bytes of resident compiler state per byte of source the entry never references (the corpus is 60.4 MB across 5,415 files).
This is a growth-surface repair, not only a capacity one
The census population is the whole indexed pool minus the closure, so every module added to the corpus pays into it — every
recurring_failure_moderow, every witness, every new.dagfile — regardless of whether any entry references it. The cost is denominated in the corpus, not in the change (DESIGN §5's absorbing-fallback tell, in its cost arm). Left alone, the term grows monotonically with the corpus and re-crosses whatever budget it is measured against; fixing the retention shape is what stops that, and no budget raise would.The change
Fuse the tokenize into the fold so each module's tokens are freed before the next is read. This is DESIGN §2's minimise the demand before materialising applied to a retention shape rather than to redundant work: same work, same order, same results, held only for the interval it is actually used.
It is explicitly not a cache — nothing is memoised, and the pool is not scoped to the closure. §2 forbids making redundant work cheap; this deletes residency, not recomputation.
The pre-intern pass dissolves with
preparedas a consequence, not a separate decision: it existed only because the token lists were already in hand. Interning is idempotent andparse_with_table_in_occurrence_scopealready threads an updated table through the fold, so pre-interning module N immediately before parsing module N reaches that parse with the same table contents. Only id assignment order changes, which no consumer reads — names resolve positionally throughauthored_name_at, never by intern id.Authority is
src/v1/compile.dag;v1_compiler_compile.rsis its hand-synced mirror and moves with it.What is deliberately unchanged
Every module is still parsed with full bodies, and every parse diagnostic still surfaces. That wall is load-bearing and executed, not a formality — a body-level parse break in a non-closure module is invisible to the heads-only module index and is caught here and nowhere else.
Evidence
The admitted budget is 16,106,127,360 bytes = 15.00 GiB (per-runner cgroup
memory.highonactions-runner@*.service, uniform across the fleet — it is not an srv4-specific setting, and the fleet variation is in page cache available to the cgroup, not in the budget).Same invocation,
VmHWMfrom/proc/<pid>/status:Both print
resolved 16 sources (reference-derived closure ...), 5399 indexed modules in the name census only, 213 advisory diagnostics, 1 file emitted, exit 0.A green floor on any single host is a draw, not a fix — what varies between hosts is page cache, not the budget. The claim here is the measured peak and its margin, above; a floor verdict is corroboration, not the deliverable.
Discriminating RED — the wall this change risks deleting. Planted in
dag/std/access.dag, outside the entry's closure:Balanced braces, so the heads-only module index does not catch it. Before and after, identically: exit 1,
refused at emit, 2 hard diagnostics,error[dag/std/access.dag]: expected name, found Eq.A brace-unbalanced probe was tried first and rejected as a control: it fires, but the module index refuses it (
heads-only parse: unterminated function body) before the census fill ever runs, so it does not discriminate this change. Recorded because a control that fires without discriminating reads exactly like a passing one (DESIGN §4b: ask whether the RED is authorable where the check runs).Positive control:
--source-root src/v1 --entry src/v1/compile.dagproduces 2487 hard diagnostics before and after — the edit adds none.Not claimed: the emitted
dag-artifact.jsonis run-to-run nondeterministic on main, so artifact equality was unusable as an equivalence oracle and is not cited as one. Evidence is in a comment below for the lane that owns it; it is pre-existing and not caused by this PR.Local:
cargo clippy --all-targets -- -D warningsclean;cargo fmt --all --checkclean.Declared frontier (named, not done)
This PR is step 1. The two residual terms, both probe-measured, are what take the entry compile to genuinely O(closure):
(a) Full-body AST retention for the same 5,399 modules.
fill.modulesreaches onlybuild_symbol_index_qualified_fill, which reads declaration heads. A probe retainingcensus_heads_module_node(node)measured a further 8,986,140 → 7,663,268 KiB (1.26 GiB; 7.31 GiB, 48.7% of budget), wall unchanged, with the discriminating RED still refusing identically.Precondition for that lane: the post-parse heads strip currently exists only in the seed (
cli_run::census_heads). It must be projected into the authority — declared in.dag— before this lands; doing it from the seed side would drift the mirror fromcompile.dag.02_parse.dagalready carriescensus_heads_body_stand_in, so the vocabulary is half there.(b)
build_both_closure_edge_indexcomputes reference and bare pull edges for all ~5.4k pool files (for sf in index.source_files.values()), while an--entryBFS queries only the closure's. That work is not merely duplicated — it is never demanded. It forcestree_bare_census_for_rootper root as a side effect and sits inside the residual 4.79 GiB. This is the term that makes the compile O(closure) rather than merely smaller.Not touched
No memory budget raised, no floor budget rows, no parse witnesses — per the brief and sleek-swift-789's ruling.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QdEbLY7W5T2jBtqXQLBgVE