Perf: the gate was slow because it ran an unoptimized compiler, one cell at a time #223
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/223-perf-the-gate-was-slow-because-it-ran-an-unopt"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Perf: the gate was slow because it ran an unoptimized compiler, one cell at a time
Along the way:
Touches 16 files:
src/rust/Cargo.lock,src/rust/Cargo.toml,src/rust/crates/ck_driver/src/run.rs,src/rust/crates/ck_driver/tests/architecture/alloc.rs,src/rust/crates/ck_driver/tests/capability.rs,src/rust/crates/ck_driver/tests/common/mod.rsand 10 more.Second read on
src/rust/crates/ck_driver/tests/common/mod.rs. Nothing to add.Good. Small enough to read in one sitting, and
src/rust/crates/ck_driver/tests/common/mod.rsis easier to follow than it was.@ -73,0 +140,4 @@if let Some(result) = job(item) {collected.lock().expect("the sweep results outlive every worker")The expect message on
expectis the only thing a user sees if this fires. Does it say what they should do, or only what we assumed?, at src/rust/crates/ck_driver/tests/common/mod.rs:143.@ -225,3 +199,3 @@/// with exactly one newline, so trailing-whitespace churn can never show up as/// a moved number.fn render(app: &str, sizes_exit: i32, tree: &str, run_exit: i32, report: &str) -> String {fn render(app: &str, sizes_exit: i32, tree: &str) -> String {renderreads, decides and writes in one function. The deciding part is the piece I would want to test on its own. Worth splitting?It said what I assumed. Reworded in
src/rust/crates/ck_driver/tests/common/mod.rsso it names the input that broke it, which is what the bug report will need.Split. The decision in
renderis a pure function now, with its own tests insrc/rust/crates/ck_driver/tests/sizes_pin.rs.`cargo test` builds `ckc` under the `dev` profile and then RUNS it ~550 times. opt-level 0 was therefore never a build-speed setting here — it multiplied by every fixture in the tree. Measured on one core, full pipeline (front end + lowering + Cranelift + link, `ckc disassemble`): opt-level 0 (before) ......................... 0.906 s deps at 2, workspace at 0 .................... 0.349 s deps at 2, workspace at 1 (now) .............. 0.122 s 7.4x, and it is close to free: an incremental rebuild after a real edit to `ck_span` (15 crates) measures 1.079 s against 1.186 s before — inside the noise. Dependencies go to 2 because Cranelift dominates codegen and is built once and cached, so its level never reaches the edit loop. The guards are untouched, which is the whole reason this is opt-level 1 and not `inherits = "release"`. `opt-level` is orthogonal to `debug-assertions` and `overflow-checks`; both stay on, verified in the built binary rather than assumed — `ckc` still carries its `operator_table.rs` debug_assert sites and its overflow panic path. `[profile.bootstrap]`'s reasoning applies with equal force to the profile the tests run under. `debug = "line-tables-only"` keeps file/line in every backtrace and drops the records nothing here reads. The suite links ~57 binaries each statically carrying the compiler and Cranelift, so this is the dominant term in link time and disk: 65.5 MB -> 33.0 MB per test binary, the built set 5.3 -> 2.6 GB. `capability` swept its 437 cells in a serial `for` loop — not because a cell needed the one before it (each materializes its own project, links its own binary into its own /work subdir, reads back its own meta stream) but because that is how it was written. On 32 cores that is one core working and the rest idle, which per cpu_broken.md is also the arrangement that concentrates wear. It now runs across a bounded pool in `tests/common/mod.rs`, shared rather than copied because `sizes_pin` needs the same thing next. Capped at the same 12 as `.cargo/config.toml`'s `[build] jobs`: one number, one policy. Order is preserved, and that is contractual, not cosmetic — the report prints the exact block to write for each rejected cell, so a thread-shuffled report could not be diffed between runs. Workers record each cell's index and the results are sorted back before rendering. Verified by running the whole tree at one worker and at twelve: the same eight cells, in the same order. capability, one build, workers 1 -> 12 ....... 32.18 s -> 3.37 s capability, HEAD -> here ..................... 76.95 s -> 3.37 s `read_meta_stream` cost a flat five seconds whenever a run emitted nothing. Its exit test was `!grew && !events.is_empty()`, unreachable while `events` is empty, so an empty stream could only leave by the deadline. A bare `fn main` boots no actor and a `ckc test` app with no `test fn` starts none; those runs are finished, not slow. Split into a first-record grace and the original cap. `ck_playground` is deleted. Its own manifest called it "throwaway scaffolding to verify each slice's output visually while CkLang itself is not yet runnable" — CkLang is runnable now, and it was last touched 2026-05-21. It dragged eframe/egui/glow/wayland/x11 into the workspace: 307 of 372 lockfile packages, 82.5%, existed only for it, and `cargo fmt`, `cargo clippy --workspace --all-targets` and `cargo test --workspace` all paid for them. It had no tests. Its R4 allow-list entry goes with it. Full gate, warm on both sides: 5m20s -> 1m32s. The eight red capability cells are unchanged and predate this: verified by running pristine HEAD.Owner ruling, 2026-08-18. The other corpus trees are entries to READ, and to run by hand when you want to. This was already the stated architecture — `corpus_run.rs` says so in its own header, "tests/corpus/ … apps that exist to be read … Compile-checked by corpus.rs; never run" — but two gates had drifted across the line and a third existed to police the wrong side of it. `sizes_pin` no longer runs anything. It pinned a `ckc run --mem` arena report and run exit code for every directory under tests/corpus/, and it was the only thing in the suite executing that tree. Eight of those directories are the `primes_actor_garage*` drag races, which run five seconds each BY DESIGN: ~40 s of a ~51 s gate, spent measuring throughput nothing asserts on, on a machine with a degraded core. A benchmark is not a test. What that subtracted, precisely: `arena (calculated)` is a compile-time number and `ckc sizes` — still pinned — is the query that reports it; the measured high-water read 0 B in all nine apps that produced a report at all, so the observation the run existed to make was a constant re-derived 36 times a run; and 36 exit codes. The re-blessed pins are pure deletion, not one `ckc sizes` line moved. Sizes are a compile-time law and never needed a container. sizes_pin ................................... 51.37 s -> 0.16 s `ml_tictactoe_run.rs` is deleted. It drove tests/corpus/ml_tictactoe. ml_* is not a gate. The app stays where it is and `scripts/tictactoe.sh` still launches it interactively whenever you want it. `runtime_registry/` is deleted, and this is the one that matters. Its whole contract was "a clean, runnable app in tests/corpus/ is a coverage hole that needs a justified exemption row" — the exact inverse of the ruling. That premise is what forced 15 tensor_* rows, 8 primes_actor_garage rows and the ml_tictactoe row into an allow-list set to grow one row per corpus app forever, excusing them from a rule nobody wanted. With tests/corpus/ defined as the read tree, the tree IS the answer. Deleting the rule beats maintaining exemptions to it (CLAUDE.md rule 4, option (c) — the rule was wrong). Four ML / benchmark apps move corpus_test_run/ -> corpus/: ttt_selfplay, tensor_cklang_micro_lm, primes, primes_actor. Their `run.md` files go with the move, because run.md is the corpus_test_run contract and a declaration no gate reconciles is exactly the drift this repo forbids. Honest measurement: only ttt_selfplay was actually slow (1.130 s). primes 0.127 s, primes_actor 0.097 s and tensor_cklang_micro_lm 0.134 s were all at the ~0.12 s floor every app pays for compile+link+docker — the 110-app sweep was 13.94 s and essentially all of it is that floor. So this move is right on the principle, not on the clock. SHORTFALL_FNS raised 200 -> 223, and it is a different kind of raise from the one above it: no emitted byte changed. frame_reconcile sweeps tests/corpus/, so the four apps arrived carrying shortfalls this inventory could not see before — primes +1, primes_actor +12, tensor_cklang_micro_lm +4, ttt_selfplay +6 = +23, the whole of the difference. WORST_SHORTFALL_BYTES is the proof it is set growth and not regression: the worst among all four new pins is 80 B, exactly the standing ratchet. Attribution is written into the constant. Five condemned-allocation rows in architecture/alloc.rs are repointed at the new paths. The M1 gate caught the two I first missed, which is the gate working. Audited afterwards: of the thirteen gates that can start a container, every one draws from corpus_test_run/, capability/, tests/fixtures/, or a temp fixture it builds itself. Zero run anything from tests/corpus/. What still reads that tree is compile-time only — corpus.rs checking, frame_reconcile and sizes_pin pinning numbers, the architecture rules reading .ck source. full gate, warm ............................. 5m20s -> 34.8 s Unchanged: the eight red capability cells, identical to pristine HEAD.