# nigig-traffic performance runbook (plan Phase 6) Rule zero (HPC ch.1 "When to Optimize", ch.5): no optimization without a profile line pointing at it, and no number counts until it is in `PERF_BASELINE.md`. If it isn't in the baseline file, it didn't happen. ## Live frame stats The view collects tick + draw microseconds over a 300-sample window and reports p50/p95/p99 to the log (`src/ui/game_view.rs`: `FrameStats`). Collection is a few integer stores per frame; reporting only fires with: ```sh NIGIG_TRAFFIC_STATS=1 cargo run -p nigig-traffic # look for: traffic stats: tick us p50=.. p95=.. p99=.. (n=300) ``` ## Repeatable harness ```sh cargo test -p nigig-traffic --test perf -- --test-threads=1 --nocapture ``` `tests/perf.rs` prints median build µs per category and per-tick µs over 600 ticks × every scenario. Timings are deliberately NOT asserted — wall-clock asserts are flaky by nature (HPC ch.5.6). The sanity asserts (tick count, Intro→Driving transition, black-boxed score) exist only so the optimizer cannot hollow the loop. ## Methodology (HPC ch.5.6, condensed) - `--test-threads=1`, warmed runs, one machine, nothing else building. (Background `cargo-makepad android` jobs invalidated early readings here — check `ps aux | grep rustc` before trusting a number.) - Report medians, never means; re-run to check outlier stability. Outliers that move between runs are noise, not findings. - Debug profile numbers are for shape (which function, which scenario class), never for absolute budgets. Ship budgets come from release runs on the target device. ## Statistical profiling - macOS: Instruments → Time Profiler, attach to `target/debug/nigig-traffic` while driving; look for `step_world`, `draw_scene`, `evaluate`, text shaping (`shaper.rs`), not the frame loop itself. - Linux: `perf record -g` + `perf report`. Top-down first, bottom-up to confirm callers. - Sampling finds the hot 3%; instrumentation lies about the rest. The `FrameStats` ring is the instrumentation — treat it as a tripwire, and reach for the sampler before changing code. ## What the numbers said so far See `PERF_BASELINE.md`. Headline: the tick is memory-latency-bound on tiny data (median ~15–25µs debug, 47 scenarios), builds are single-digit µs. There is no compute hot spot to SIMD away — Phases 7–8 accordingly target lookups, allocations, and branches, not intrinsics. ## Build environment notes (hard-won, do not re-learn) - `RUSTC_WRAPPER=kache` is exported on dev machines (`~/.profile`) but the `kache` binary may not exist — cargo then fails with `could not execute process kache ... os error 2`, surfacing in makepad-test runs as `BuildStopped 101`. Fix: `ln -s sccache ~/.cargo/bin/kache`, or standardize on `sccache` + `SCCACHE_DIR` in one place. - One target dir: the workspace root `target/`. Per-crate `target/` dirs (a stale 2.2G one lived under `nigig-traffic/` until Sep 2026) waste disk and split test artifacts across two trees. The makepad-test harness historically pointed `CARGO_TARGET_DIR` at the per-crate dir; keep it on the workspace root. - Release-vs-debug profile decision (`opt-level 3` vs `z` for the game binary) is BLOCKED on a green workspace build — the fork rev bump broke compilation repo-wide (see Phase 9 notes in PERF_BASELINE.md). Measure both on the Phase-6 bench before choosing; do not pick on aesthetics.