Open and answer this issue on GitHub.
ready · opened 2026-10-19 · updated 2026-10-20 · 1 comment
CI’s slowest tests (sysroot 94 s, language 87 s, map_trust 71 s) all lower the tools package (crates/nikaia-std/src/tools, 209 files, 94,500 lines, 3.5 MB of Rust). A cold nikaia lower-std (NIKAIA_CACHE_DIR empty, NIKAIA_LOWER_THREADS=1) takes 58.6 s of CPU, about 1,600 lines a second; a front end usually checks 50-200 k lines a second.
perf record --call-graph dwarf of a cold lower-std at d4f932d, 8,570 samples. The figures are shares of all samples, inclusive.
A. The same analysis runs several times (about 45 %)
| Analysis | Runs | Total | Once would cost |
|---|---|---|---|
check::walked (the whole checker): ledger inference (check_for_the_ledger), the file’s check, and again in the emitter (Emitter::with_contracts -> check::propagation) |
3x | 50 % | 17 % |
contracts::keep::buffer_plans (in walked three times, in keep::infer, and in with_contracts) |
5x | 21 % | 4 % |
nonnegative_locals / nn_every (bounds::proven and Emitter::function_body) |
2x | 12 % | 6 % |
Ledger::clone in emit_std_items_against_at, once per file |
209x | 3 % | 0 |
B. Method lookups scan every function of the ledger (about 24 %, mostly inside A). For each method call these functions walk ledger.functions (about 5,000 keys) with key.ends_with("::name"), which is where memcmp (21 % of all samples) comes from:
| Function | Share (memcmp + self) |
|---|---|
keeping_method_key (keep.nika) |
10.4 % |
owned_by_every_method (buffers.nika) |
7.5 % |
ledger_candidates (check_values.nika) |
4.3 % |
candidate_keys / keeps_at_in (keeps.nika) |
1.8 % |
d4f932d already did this for view_changes.rs: -1 s.
C. Clones. Ty::clone takes 5.7 %, mostly in cleanup_within and collect_copies. FnContract::clone takes 3 %. malloc and free take about 12 % in all.
D. InternerContext::resolve takes 4.5 %: a DashMap with a lock, read by every check.
A cold lower of the tools package in a tenth of 58.6 s of CPU, with byte-identical package.rs.
benches/lowering/ (in Nikaia or as a lower.sh modelled on benches/brc/brc.sh). It does a cold lower of crates/rs2nika/convert (5,000 lines; callgrind instruction count) and of the tools package. It checks that the output is byte-identical to the committed one and prints a table: instructions, plus the NIKAIA_LOWER_TIMES phases for orientation. Count, never time (CLAUDE.md). This VM has no hardware counters, so callgrind is the count.Emitter::with_contracts. First establish whether the ledger pass’s check sees the final ledger. If it does not, re-check only the units whose callees’ contracts changed, and say so in the commit. Store buffer_plans and nonnegative_locals once per function. Share the ledger instead of cloning it per file. Test: sysroot (package byte-identical, one thread equals every core) plus the touched tests.:: segment) to its ledger keys, once per ledger, so the lookups never go stale. Use it in the four functions of B (and in described_key).RodeoReader.cleanup_within, collect_copies, FnContract in BTreeMap clones): borrow instead of clone, guided by a new profile.benches/lowering/README.md.State after eight agent rounds (versions 0.0.1099–0.0.1120), counted with benches/lowering/lower.sh (cold nikaia lower --no-cache crates/rs2nika/convert/src/main.nika, release, output byte-identical):
16 410 M → 3 097 M Ir (−81 %). The goal (a tenth, ~1 640 M) is not reached yet.
Built: method index; each analysis once (emitter reuses the check’s facts, Needs read off items, program read once, trust join reuses the ledger pass); solver without arena/bound copies, ranked sorts, repeat questions answered from the book; line/column by binary search; ledger pass skips report-only walks; keep plans reused when the library is std’s; text-tiers worklist; result sources stop when unchanged; CheckResolved.contract as a view. Lowering defects found on the way: #733, #735 (fixed), and one checker gap filed separately.
Current profile (inclusive, 3 096 M): check::walked 1 111 M (ledger walk 390 M); infer_package_noted 735 M; bounds::proven 577 M; parse 467 M; keep::buffer_plans 217 M (twice, different ledgers). malloc/free ~27 % of self.
What is left is no longer local, and needs a decision:
computes settings): an architecture change.winnow-grammar git dependency (InternerContext::resolve 97 M over a DashMap; parsing ~2 500 Ir per byte).lg_verify re-checks certificates the search just made (60 M): skipping it is a trust decision.Generated by Claude Code