| 1 | # Build and test performance |
| 2 | |
| 3 | Measured facts about how long Codewhale takes to build and test, what was |
| 4 | changed to make the contributor loop faster, and what is deferred. Numbers |
| 5 | are from one machine (Apple Silicon, 14 cores, rustc 1.97.0, Xcode 26.2 |
| 6 | `ld-1230`) taken while four other cargo jobs were running (1-minute load |
| 7 | average 10–27, recorded next to each number), so treat them as relative |
| 8 | before/after evidence, not benchmarks. |
| 9 | |
| 10 | > Split plan: [TUI deconstruction](design/TUI_DECONSTRUCTION.md) contains the |
| 11 | > September 9 source audit and current proposed extraction order. Measurements |
| 12 | > below are historical; the B3/deferred candidate lists are not an execution queue. |
| 13 | |
| 14 | ## Where the time goes (baseline, commit 533c530b) |
| 15 | |
| 16 | | Step | Wall | Notes | |
| 17 | | --- | --- | --- | |
| 18 | | Cold `cargo build -p codewhale-tui` (empty target) | 94 s (user 270 s) | 543 units; `codewhale-tui` alone is 70 s and is the critical path; next longest units are `codewhale-config` 7.5 s, `jsonschema` 6.3 s, `codewhale-workflow` 5.6 s, `tokio` 5.4 s. Load 13. | |
| 19 | | Cold `cargo test -p codewhale-tui --lib --no-run` (empty target) | 148 s (user 347 s) | The 10.5k-test unit binary is 357 MB and trips the macOS linker's `__eh_frame > 16MB` compact-unwind warning (harmless). Load 20. | |
| 20 | | Incremental `cargo build -p codewhale-tui` after a one-line edit | 12.5 s | Load 11. | |
| 21 | | Incremental `cargo test -p codewhale-tui --lib --no-run` after a one-line edit | 19 s | Load 11. | |
| 22 | | `cargo test --workspace --all-features --locked --no-run` with deps warm | 155 s (user 366 s) | 61 test binaries. Load 6→11. | |
| 23 | | Cold-ish `cargo check --workspace --all-targets --locked` (deps built) | 82 s | Load 19. | |
| 24 | | Running the tui unit suite with libtest (`cargo test -p codewhale-tui --lib`) | 268 s | From the release gate log; 10,531 tests. Load ~10. | |
| 25 | | Running the same suite with `cargo nextest run -p codewhale-tui --lib` | 96–108 s | Same tests, one process per test, all cores busy. Load 12–20. | |
| 26 | | Running the whole workspace under `cargo nextest run --workspace --all-features` | 353 s | 12,744 tests, PTY suite serialized by the nextest config. Load 17. | |
| 27 | |
| 28 | Structural facts behind those numbers: |
| 29 | |
| 30 | - `crates/tui` is ~746k lines of Rust (609k non-test, 137k inline tests in |
| 31 | 488 `#[cfg(test)]` modules, 10.6k `#[test]`/`#[tokio::test]` functions). |
| 32 | It compiles as one crate, so the frontend of that crate is the critical |
| 33 | path of every build and every unit-test run recompiles it with |
| 34 | `cfg(test)`. |
| 35 | - Dependencies are already trimmed (`reqwest` rustls-no-provider, `image` |
| 36 | png only, `syntect` default-fancy, `rmcp` no default features, `mimalloc` |
| 37 | no default features). `cargo tree -d` shows only routine duplicates |
| 38 | (`toml` 0.8/1.1, `thiserror` 1/2, `strum` 0.27/0.28, `syn` 2/3, |
| 39 | `sha2` 0.10/0.11) that come from third-party crates, not from workspace |
| 40 | choices. The global allocator is mimalloc by default; the off-by-default |
| 41 | `rusty-alloc` cargo feature on `codewhale-tui`/`codewhale-cli` swaps it for |
| 42 | the pure-Rust `rusty_alloc` remake (#5872). Use |
| 43 | `cargo build -p codewhale-cli --no-default-features --features rusty-alloc` |
| 44 | (or `-p codewhale-tui`) to exclude mimalloc and its C build dependency. |
| 45 | Cargo features are additive: `--features rusty-alloc` alone retains the |
| 46 | default mimalloc dependency even though the Rust allocator handles allocations. |
| 47 | This removes the allocator's C build path; other native dependencies may still |
| 48 | require a C toolchain. With neither allocator feature, the standard library |
| 49 | system allocator is used. |
| 50 | - `[profile.dev] debug = "line-tables-only"` is already set (#5246) and |
| 51 | Cargo already uses `split-debuginfo = unpacked` on macOS. |
| 52 | - `target/debug` grows past 50 GB only through accumulation across |
| 53 | feature sets and worktrees; a fresh test build is ~7 GB. |
| 54 | |
| 55 | ## A0 receipts (commit 533c530b + hermeticity fixes; empty target dir) |
| 56 | |
| 57 | `CARGO_TARGET_DIR=/Volumes/VIXinSSD/CW/.tmp/compile-speed-baseline`, HTML |
| 58 | timing reports archived under |
| 59 | `backups/compile-speed-evidence-20260815/` (a0-cold-lib-test-timing.html, |
| 60 | a0-incremental-lib-test-timing.html, a0-llvm-lines-top40.txt). |
| 61 | |
| 62 | | Receipt | Wall | Load (1 min) | |
| 63 | | --- | --- | --- | |
| 64 | | Cold `cargo test -p codewhale-tui --lib --locked --no-run --timings` | 127 s (user 329 s) | 8.9 | |
| 65 | | `touch crates/tui/src/elapsed.rs` + same command | 21 s | 12.3 | |
| 66 | | `touch` + `cargo test -p codewhale-tui --lib --locked elapsed::` (the everyday loop) | 20 s (4 tests run) | 11.2 | |
| 67 | | Lib-test binary size | 357 MB (`codewhale_tui-<hash>`); links with the `__eh_frame section too large (max 16MB)` compact-unwind warning | — | |
| 68 | | `cargo check -p codewhale-tui --lib --tests` incremental after `touch` (frontend only) | 14 s | 6.2 | |
| 69 | | Incremental full lib-test build after `touch`, same conditions | 28 s | 6.2 | |
| 70 | |
| 71 | Cold timing report, top units (605 units): `codewhale-tui` lib test |
| 72 | **106.0 s**, `codewhale-config` 7.9 s, `jsonschema` 5.8 s, `moxcms` 4.7 s, |
| 73 | `codewhale-protocol` 4.2 s, `tokio` 4.0 s, `rustls` 3.8 s, `schemaui` |
| 74 | 3.4 s, `rmcp` 3.4 s, `h2` 3.3 s, `jsonschema` (second copy) 3.2 s, |
| 75 | `codewhale-workflow` 3.2 s, `syn` 3.1 s, `rio-vt` 3.1 s, `regex-automata` |
| 76 | 3.0 s. The incremental report has exactly one non-zero unit: `codewhale-tui` |
| 77 | lib test 20.8 s. So the everyday tax is the tui crate itself, split |
| 78 | roughly half frontend (check --tests 14 s) and half codegen + link (28 s |
| 79 | total); dependencies and the linker are not where the time is. |
| 80 | |
| 81 | `cargo llvm-lines -p codewhale-tui --lib`: **8,138,810 lines in 223,052 |
| 82 | copies**. Largest single function is the `rust_i18n` backend closure |
| 83 | (`_RUST_I18N_BACKEND::{closure#0}`, 311,782 lines, 3.8 % of the crate on |
| 84 | its own — the 15 locale packs are compiled into a match by the `i18n!` |
| 85 | macro), then `run_event_loop` 27 k, `Engine::run_turn` 26 k, |
| 86 | `RuntimeThreadManager::monitor_turn` 16 k, then serde `Deserialize` |
| 87 | expansions for `Config`/`ProvidersConfig`/`Settings` (5–6 k each, several |
| 88 | copies per toml deserializer). |
| 89 | |
| 90 | ### A0.1 dependency ratchet |
| 91 | |
| 92 | `cargo metadata --locked` counted **690** packages and `cargo deny check |
| 93 | bans` warned on duplicate `fancy-regex`, `jsonschema`, `jsonschema-regex`, |
| 94 | `referencing` plus stale `jni`/`jni-sys`/`redox_syscall` skips. Cause: the |
| 95 | workspace `jsonschema` pin had been bumped to 0.49 while `schemaui` 0.12 |
| 96 | (latest 0.12.4 included) still requires `^0.46`. Pinning the workspace |
| 97 | back to the 0.46 line removes the second jsonschema stack (**685** |
| 98 | packages; deny bans and advisories clean; `--locked` resolves; |
| 99 | codewhale-workflow-js 61 tests and the tui schema tests pass). Cold saving |
| 100 | is the two duplicated units (~9 s of unit time, ~3 s of wall). |
| 101 | |
| 102 | ### A1 cache topology (desk-local, not committed) |
| 103 | |
| 104 | New-worktree cold `cargo test -p codewhale-tui --lib --locked --no-run`, |
| 105 | same machine, back to back: |
| 106 | |
| 107 | | Topology | Wall | CPU (user) | Notes | |
| 108 | | --- | --- | --- | --- | |
| 109 | | Per-worktree fresh target (control) | 127 s | 329 s | A0 | |
| 110 | | One shared `CARGO_TARGET_DIR` (warm from another worktree) | 121 s | 188 s | deps reused; every workspace crate recompiles (path-keyed); no lock waits observed; target 14 GB | |
| 111 | | `build.build-dir = ".../{workspace-path-hash}"` per workspace + warm shared `sccache` (`CARGO_INCREMENTAL=0`) | 107 s | 161 s | 73.6 % sccache hit rate (all 337 Rust dep units hit; the 125 misses are workspace crates); 2.7 GB build dir per workspace + 483 MB cache; the same command that *populated* the cache took 108 s / 157 s CPU | |
| 112 | |
| 113 | Wall time is the tui crate in every topology; the topologies buy CPU |
| 114 | (~50 %), which is what matters when several checkouts build at once. |
| 115 | Recommended user-level `~/.cargo/config.toml` (adjust the two roots): |
| 116 | |
| 117 | ```toml |
| 118 | [build] |
| 119 | # One build root for every checkout; each workspace gets its own subdir, |
| 120 | # so worktrees never wait on each other's target lock. |
| 121 | build-dir = "/path/to/cache/codewhale/build/{workspace-path-hash}" |
| 122 | # Optional: reuse dependency compilation across checkouts. |
| 123 | # rustc-wrapper = "sccache" |
| 124 | ``` |
| 125 | |
| 126 | `sccache` was installed with `brew install sccache` on this machine for the |
| 127 | measurement. |
| 128 | |
| 129 | ### Public helper (A1/A5) — what `dev-test.sh` actually does now |
| 130 | |
| 131 | `scripts/dev-test.sh` previously only mapped an area to `cargo test -p`. |
| 132 | It did **not** activate the measured build-dir + sccache topology, so a |
| 133 | new worktree still paid a cold compile into `./target`. |
| 134 | |
| 135 | `scripts/dev-cache.sh` is the portable opt-in helper. |
| 136 | `scripts/dev-cargo.sh` and `scripts/dev-test.sh` source it. |
| 137 | |
| 138 | | Class | What changed | What it is not | |
| 139 | | --- | --- | --- | |
| 140 | | **Compile-time** | `scripts/dev-test.sh` / `scripts/dev-cargo.sh` set `CARGO_BUILD_BUILD_DIR=$CODEWHALE_CACHE_ROOT/build/{workspace-path-hash}`, so concurrent worktrees do not share a Cargo lock. A leftover `./target` (Cargo still writes `CACHEDIR.TAG` there when build-dir is split) does **not** turn isolation off; `CODEWHALE_DEV_CACHE=local` keeps `./target` if you want that. Cargo older than 1.91 falls back to a per-workspace `CARGO_TARGET_DIR`. | Not a smaller rustc unit. Workspace crates still rebuild. | |
| 141 | | **Compile-time (sccache)** | `RUSTC_WRAPPER=sccache` and `SCCACHE_DIR=$CODEWHALE_CACHE_ROOT/sccache/<rustc-commit>` only when incremental is already off (`CARGO_INCREMENTAL=0` or `CODEWHALE_SCCACHE=1`) **and** `sccache` is on `PATH`. | Not enabled on the everyday incremental loop. sccache cannot cache incremental units; wrapping those builds adds overhead and 0% hits. Missing sccache is a printed fallback, not an error. | |
| 142 | | **Test-runtime** | `scripts/dev-test.sh` uses `cargo nextest run` when `cargo-nextest` is installed (`CODEWHALE_DEV_NEXTEST=0` forces libtest). Same binaries; process per test. Retries stay 0. `RUST_MIN_STACK=16MiB` is exported when unset. | Not a compile win. nextest does not run doctests; `cargo test --doc` remains a separate gate. | |
| 143 | | **Ergonomics** | `--list` and path mapping cover every workspace crate (`app-server`, `workflow-js`, …). `scripts/dev-cache.sh --status` / `--self-check` print the topology. | No product behavior change. | |
| 144 | |
| 145 | Defaults never contain a machine-specific absolute path: |
| 146 | |
| 147 | ```sh |
| 148 | # Portable default: |
| 149 | # ${XDG_CACHE_HOME:-$HOME/.cache}/codewhale |
| 150 | # Desk override, if you want the cache on a particular volume: |
| 151 | export CODEWHALE_CACHE_ROOT=/path/to/cache/codewhale |
| 152 | |
| 153 | scripts/dev-cache.sh --self-check |
| 154 | scripts/dev-test.sh crates/tui/src/elapsed.rs |
| 155 | CARGO_INCREMENTAL=0 scripts/dev-cargo.sh test -p codewhale-config --lib --locked --no-run |
| 156 | ``` |
| 157 | |
| 158 | Hermetic script tests (no rustc compile): `sh scripts/dev-cache.test.sh` and |
| 159 | `sh scripts/dev-test.test.sh`. |
| 160 | |
| 161 | ### Helper verification (2026-08-15, this worktree) |
| 162 | |
| 163 | Recorded after other lanes released the machine (load 3.2–5.6). rustc |
| 164 | 1.97.0, cargo 1.97.0, sccache 0.17.0. `CODEWHALE_CACHE_ROOT` set to a |
| 165 | volume-local override for the run; no caches or targets were deleted. |
| 166 | |
| 167 | Cargo expands `{workspace-path-hash}` to `build/d4/96565f96fb3682` for |
| 168 | this worktree. The first isolated `codewhale-config` `--no-run` created a |
| 169 | stub `./target` (`CACHEDIR.TAG`); treating that as a warm traditional |
| 170 | target made the next command recompile into `./target` (8.65 s). The |
| 171 | helper now stays isolated unless `CODEWHALE_DEV_CACHE=local` or `0`. |
| 172 | |
| 173 | **Compile-time** (`scripts/dev-cargo.sh test … --locked --offline --no-run`): |
| 174 | |
| 175 | | Step | Wall | Notes | |
| 176 | | --- | ---: | --- | |
| 177 | | First isolated `codewhale-config --lib --no-run` | 9.14 s (user 23.8 s) | 90 units into the hashed build-dir | |
| 178 | | Warm isolated same command (after the stub-target fix) | 0.13 s | `Finished` in 0.07 s | |
| 179 | | `touch crates/config/src/lib.rs` + isolated `--no-run` | 0.93 s | only `codewhale-config` rebuilt | |
| 180 | | First isolated `codewhale-tui --lib --no-run` | **121.5 s** (user 305 s) | 600 units; 340 MB binary; A0 empty-target was 127 s / 329 s | |
| 181 | | `touch crates/tui/src/elapsed.rs` + isolated `--no-run` | **18.15 s** | everyday compile loop; A0 was 21 s / 19 s | |
| 182 | | `CODEWHALE_SCCACHE=1` config `--no-run` on the already-warm tree | 5.21 s then 0.14 s | wrapper and `SCCACHE_DIR=…/sccache/<rustc-commit>` set; 0 sccache hits because only workspace crates recompiled and the build-dir was not emptied | |
| 183 | |
| 184 | **Test-runtime**: |
| 185 | |
| 186 | | Step | Wall | Notes | |
| 187 | | --- | ---: | --- | |
| 188 | | `scripts/dev-test.sh config` (nextest, 557 tests) | run 0.479 s / real 2.40 s | includes a 0.85 s profile flip compile | |
| 189 | | `CODEWHALE_DEV_NEXTEST=0 scripts/dev-test.sh config` (libtest) | body 0.11 s / real 0.27 s | 557 tiny tests: process-per-test is slower here | |
| 190 | | `scripts/dev-test.sh crates/tui/src/elapsed.rs` | run 0.023 s / real 2.81 s | 4 passed, 10,516 skipped; nextest filter works | |
| 191 | |
| 192 | The 268 s → ~100 s nextest win remains the earlier tui-unit-suite receipt. |
| 193 | Config is too small for that win; nextest is still the right default for |
| 194 | unfiltered crate/workspace runs. |
| 195 | |
| 196 | **Ergonomics:** `sh` and `dash` both pass `dev-cache.test.sh` (22) and |
| 197 | `dev-test.test.sh` (27). Missing sccache is a fallback. `--list` covers |
| 198 | every workspace crate. |
| 199 | |
| 200 | ### A2 nextest in CI |
| 201 | |
| 202 | `cargo test --workspace --all-features --locked --doc` inventories |
| 203 | **3 passing / 8 ignored doctests across 21 crates**; CI keeps them as a |
| 204 | separate step next to `cargo nextest run --workspace --all-features |
| 205 | --locked --profile ci`. |
| 206 | |
| 207 | ### A3/A4 (not adopted, measured) |
| 208 | |
| 209 | Frontend and codegen split the tui unit roughly evenly (14 s / 14 s |
| 210 | incremental); the linker is a small part of that and dependencies are |
| 211 | already warm after the first build, so `[profile.dev.package."*"] |
| 212 | opt-level = 1` (paired result above), `-Wl,-dead_strip`, and other |
| 213 | `RUSTFLAGS` stay out of the repo (they would apply to shipped profiles); |
| 214 | `split-debuginfo` is already `unpacked` on macOS. |
| 215 | |
| 216 | ## Peak memory (why OHOS/Windows builds see two ~4 GB rustc processes) |
| 217 | |
| 218 | Sampled `ps -o rss` once a second for every rustc under this lane's target |
| 219 | dir (`backups/compile-speed-evidence-20260815/rss-sample.sh`, |
| 220 | `mem-incremental.log`, `mem-cold-cgu.log`); one rustc per row. |
| 221 | |
| 222 | | Unit | Mode | Peak RSS | Wall | Load | |
| 223 | | --- | --- | --- | --- | --- | |
| 224 | | `codewhale-tui` lib (dev) | incremental, cgu 256 | 3.3 GB | 12–14 s | 6.3 | |
| 225 | | `codewhale-tui` lib test | incremental, cgu 256 | 6.0 GB | 21–28 s | 6.3 | |
| 226 | | `codewhale-tui` lib (dev) | non-incremental (`CARGO_INCREMENTAL=0`), cgu 16 | **6.0 GB** | 78 s | 5.5 | |
| 227 | | `codewhale-tui` lib test | non-incremental, cgu 16 | **8.0 GB** | 105 s | 5.5 | |
| 228 | | `codewhale-tui` lib test | non-incremental, `codegen-units = 4` | 6.1 GB (−24 %) | 145 s (+38 %) | 5.5 | |
| 229 | | `codewhale-tui` lib test | non-incremental, `codegen-units = 1` | 7.8 GB (−3 %) | 161 s (+53 %) | 5.5 | |
| 230 | | next-largest units (codewhale-config, rmcp, tokio, schemaui, codewhale-workflow) | either | 0.4–0.7 GB | — | — | |
| 231 | |
| 232 | So a plain `cargo build -p codewhale-tui` needs ~6 GB for one rustc, the |
| 233 | unit-test build ~8 GB, and `cargo test --workspace` (or `--all-targets`) |
| 234 | schedules the lib and lib-test units of the tui crate concurrently with |
| 235 | the CLI, which is exactly the "two rustc processes at ~4 GB each" a |
| 236 | community member reported while cross-compiling for OHOS on Windows (RSS |
| 237 | accounting differs by OS; the shape is the same). The inline test modules |
| 238 | add ~2 GB (+33 %) to the crate's peak; generic bloat is the driver on both |
| 239 | axes (8.1 M LLVM lines, `rust_i18n` closure 312 k, serde `Deserialize` |
| 240 | expansions for the config structs). Fewer codegen units trade a little |
| 241 | peak for a lot of wall time and are not adopted by default. |
| 242 | |
| 243 | ### Low-memory build recipe (machines with < 16 GB, cross-builds) |
| 244 | |
| 245 | ```bash |
| 246 | # One rustc at a time: the tui lib and its unit-test build never overlap. |
| 247 | export CARGO_BUILD_JOBS=1 # or: cargo build -j1 ... |
| 248 | # Only the crate you are working on, only its library: |
| 249 | cargo build -p codewhale-tui |
| 250 | cargo test -p codewhale-tui --lib -- <filter> |
| 251 | # Do NOT use --workspace/--all-targets on a small machine; run crates one |
| 252 | # at a time (scripts/dev-test.sh <area> picks the narrowest command). |
| 253 | # Optional, if 8 GB for the unit-test build is still too much (slower): |
| 254 | export CARGO_PROFILE_DEV_CODEGEN_UNITS=4 # ~6 GB peak, ~+40 % wall |
| 255 | # Cross-builds (e.g. OHOS) inherit the same numbers: add -j1 to the |
| 256 | # cargo/ohrs invocation and build the release profile, which peaks lower |
| 257 | # than the unit-test build because it carries no test modules. |
| 258 | ``` |
| 259 | |
| 260 | ### B1 (megatest peel) — audited, not landable under the constraints |
| 261 | |
| 262 | The six largest inline test files (tui/ui/tests.rs 22.1 k lines / 643 |
| 263 | tests, tools/subagent/tests.rs 18.7 k / 448, core/engine/tests.rs 17.8 k / |
| 264 | 358, config/tests.rs 12.7 k / 393, runtime_threads/tests.rs 9.3 k / 141, |
| 265 | runtime_api/tests.rs 9.1 k / 151) reference crate internals 826 / 226 / |
| 266 | 627 / 85 / 152 / 217 times respectively (`crate::llm_client::mock`, |
| 267 | `crate::test_support::{EnvVarGuard, lock_test_env}`, |
| 268 | `core::engine::mock_engine_handle`, `crate::tui::app::App`, …), and the |
| 269 | codewhale-tui library exposes four `pub` items in total. Every one of them |
| 270 | is white-box; none can move to `crates/tui/tests/` without making the |
| 271 | module tree public, which this lane was told not to do. The lever this |
| 272 | would have bought — the ~2 GB / ~35 s that the test modules add to the |
| 273 | lib-test unit — needs a decision first: either a `#[doc(hidden)] pub mod |
| 274 | test_api` (a deliberately public, unstable surface for the ~30 symbols the |
| 275 | black-box subsets use) or accepting that the unit suite stays inside the |
| 276 | crate. Recorded here rather than done. |
| 277 | |
| 278 | ### B2 landed (leaf types out of codewhale-tui) |
| 279 | |
| 280 | | Move | Lines out of tui | Consumers changed | |
| 281 | | --- | --- | --- | |
| 282 | | `core/tool_parser.rs` → `codewhale_core::tool_parser` | 662 | 0 (re-export; integration harness imports instead of `#[path]`) | |
| 283 | | `tls.rs` → `codewhale_release::tls` | 21 | 0 (`use codewhale_release::tls;` at the crate root) | |
| 284 | | `AppMode` (+ pure impl) → `codewhale_config::AppMode`; localized picker strings stay as `AppModeUi` | ~150 | 3 files import the trait | |
| 285 | | `ApprovalMode` (+ pure impl) → `codewhale_execpolicy::ApprovalMode` | ~60 | 0 (re-export) | |
| 286 | |
| 287 | Together ~0.9 k of the crate's 746 k lines: correct dependency direction |
| 288 | established, no measurable change to the tui unit's time or RAM yet (the |
| 289 | lib-test peak above, 8.0 GB, was sampled after these moves). Not moved, |
| 290 | with the reason: `ReasoningEffort` — its impl takes the TUI-defined |
| 291 | `ApiProvider` (`crates/tui/src/config.rs`) and calls |
| 292 | `crate::config::is_exact_*_k3_route` / `crate::provider_lake`, so |
| 293 | `ApiProvider` has to move first (B3, below); `approval/policy.rs` (risk |
| 294 | classify) depends on `command_safety` and `auto_review`; `hashing.rs` is |
| 295 | 15 lines of sha2 wrappers with 53 call sites and no compile-time value on |
| 296 | its own; the uncompiled `core/runtime_contract/{budget,context,ledger, |
| 297 | manifest,profile,progress,retry,terminal,work}.rs` have zero consumers and |
| 298 | zero build cost (`core/mod.rs` documents them as staged scaffolding, |
| 299 | TUI-DOG-017) — left as they are. |
| 300 | |
| 301 | ### B3 order |
| 302 | |
| 303 | 1. `ApiProvider` + the exact-route helpers (`is_exact_*_route`) out of |
| 304 | `crates/tui/src/config.rs` into codewhale-config, unblocking |
| 305 | `ReasoningEffort`. **Not started, and now the critical path** — see |
| 306 | item 4. |
| 307 | 2. **Landed.** `localization` + `locales/*.json` → |
| 308 | `codewhale-localization` (the 312 k-line `rust_i18n` closure left the |
| 309 | tui unit; locale-only edits no longer rebuild the TUI). |
| 310 | 3. **Landed.** `palette` → `codewhale-palette`; `command_safety` → |
| 311 | `codewhale-execpolicy` (it already owned `ApprovalMode`, so that move |
| 312 | removes a dependency edge rather than adding one). |
| 313 | 4. `client/` (provider wire adapters) → `codewhale-client`: **blocked on |
| 314 | item 1, not merely ordered after it.** With doc comments and |
| 315 | `#[cfg(test)]` blocks excluded, `client` still has 20 production |
| 316 | `crate::` edges. Three of them are hard: |
| 317 | - `crate::config` — `Config`, `ProvidersConfig`, `ProviderConfig`, |
| 318 | `TuiConfig`, `ApiProvider`, `RetryPolicy`, `validate_route`, |
| 319 | `wire_model_for_provider_route` and ~130 provider base-URL / model-id |
| 320 | constants. `crates/tui/src/config` is 29.7 k lines and itself reaches |
| 321 | `config_persistence`, `oauth`, `credentials`, `tui`, `fleet`, |
| 322 | `goal_loop`, `sandbox`, `lsp` … in production, so it cannot follow |
| 323 | `client` out. |
| 324 | - `crate::tools` ⇄ `client` is a genuine cycle: `client` uses |
| 325 | `tools::schema_sanitize`, `tools::large_output_router` and |
| 326 | `tools::truncate`, while `tools/{spec,review,registry,rlm,verify, |
| 327 | speech,fim,web_search,web/backend,subagent/advisor}.rs` use |
| 328 | `client::{CodewhaleClient, ProviderNativeSearchClient, |
| 329 | ProviderNativeSearchRequest, SpeechSynthesisRequest, |
| 330 | RemoteControlInferencePermit}`. |
| 331 | - `crate::core` ⇄ `client` is the same shape: `client` uses |
| 332 | `core::events::bounded_tool_projection_warning_names`, while |
| 333 | `core/{engine,engine/preview,engine/dispatch,engine/turn_loop, |
| 334 | engine/reviewer,protocol_parity}.rs` use `client::{CodewhaleClient, |
| 335 | PreparedOutboundRequest, canonical_json, parse_usage, |
| 336 | is_reasoning_replay_placeholder, redact_url_for_display}`. |
| 337 | Item 1 is therefore the whole precondition: move `ApiProvider`, the |
| 338 | exact-route helpers and the provider constants into codewhale-config |
| 339 | first, then re-measure the `tools` and `core` cycles. |
| 340 | 5. **Landed as the tractable part of item 4.** `models` + |
| 341 | `model_catalog` → `codewhale-models` (1,835 lines, 140 consumer files). |
| 342 | These sit directly under `client` on its dependency spine, had exactly |
| 343 | one production edge between them and none to the rest of the TUI, and |
| 344 | `models` was already half a re-export facade over |
| 345 | `codewhale_core::{request, role}`. |
| 346 | 6. Then `fleet/`, `tools/`, `core/engine` — each behind the crate boundary |
| 347 | its tests already respect, measured with the A0 table. |
| 348 | |
| 349 | ## What changed (this lane) |
| 350 | |
| 351 | 1. **`scripts/dev-cache.sh` / `scripts/dev-cargo.sh` activate the measured |
| 352 | isolated build-dir topology** from `scripts/dev-test.sh`. New worktrees |
| 353 | no longer compile into a private cold `./target` unless the helper is |
| 354 | disabled. sccache is opt-in and incremental-gated. Script self-checks |
| 355 | live in `scripts/dev-cache.test.sh` and `scripts/dev-test.test.sh`. |
| 356 | 2. **`cargo nextest` is supported and documented** (`.config/nextest.toml`). |
| 357 | Same test binaries, one process per test, so the tui unit suite runs in |
| 358 | ~100 s instead of ~270 s here and slow or hanging tests are named instead |
| 359 | of stalling the binary. The PTY binary is pinned to one test at a time |
| 360 | (it drives pseudo-terminals and shared mock servers; today it serializes |
| 361 | on an in-process mutex, which nextest's process-per-test model would |
| 362 | otherwise bypass), and the integration binary that spawns the real |
| 363 | `codewhale` executable is capped at four concurrent tests so its 30 s |
| 364 | start-up budgets survive a fully loaded machine. |
| 365 | `cargo test --workspace --all-features --locked` remains the |
| 366 | authoritative gate; nextest is the local loop. |
| 367 | 3. **Three tests depended on test order** and only passed because another |
| 368 | test in the same process had installed the rustls crypto provider first: |
| 369 | `codewhale-tui mcp::sse::endpoint_tests::message_before_endpoint_is_rejected_instead_of_buffered`, |
| 370 | `codewhale-app-server tests::failed_config_set_keeps_the_stdio_bridge`, |
| 371 | and `tests::successful_config_set_still_invalidates_the_stdio_bridge`. |
| 372 | Each now installs the provider itself, exactly as production does at |
| 373 | startup. No runtime code changed. |
| 374 | 4. **CONTRIBUTING.md has a "Fast local loop" section**: `scripts/dev-cargo.sh` |
| 375 | / `scripts/dev-test.sh` first, targeted `-p` filters, nextest, isolated |
| 376 | per-worktree build dirs, and the optional accelerators below. A shared |
| 377 | `CARGO_TARGET_DIR` is documented only for serialized trunk work. |
| 378 | |
| 379 | ## Measured and deliberately not adopted |
| 380 | |
| 381 | - `[profile.dev.package."*"] opt-level = 1` (dependencies optimized once, |
| 382 | workspace crates untouched). Paired measurement, back to back, same |
| 383 | target layout: cold `cargo test -p codewhale-tui --lib --no-run` from an |
| 384 | empty target went 148 s → 193 s (user 347 s → 778 s); the tui unit suite |
| 385 | under nextest went 96 s → 81 s; incremental rebuilds are unchanged. A |
| 386 | ~15 % faster test run is not worth a 2.2× more expensive cold build for |
| 387 | people trying to build Codewhale for the first time. Contributors who |
| 388 | mostly re-run tests can opt in locally by adding that table to a |
| 389 | user-level `~/.cargo/config.toml` `[profile.dev.package."*"]` section. |
| 390 | - Extra `RUSTFLAGS`/linker flags in a repo `.cargo/config.toml` |
| 391 | (`-no_deduplicate`, alternative linkers). Rustflags apply to every |
| 392 | profile and would change shipped binaries; the macOS system linker is |
| 393 | already `ld-prime`, and the measured incremental link cost is inside the |
| 394 | 12–19 s incremental numbers above. Documented as optional local |
| 395 | accelerators instead. |
| 396 | |
| 397 | ## Deferred: split `codewhale-tui` |
| 398 | |
| 399 | The single lever left that changes the shape of the numbers is splitting |
| 400 | the crate so a change to a leaf module does not re-typecheck 600k lines |
| 401 | and re-link a 357 MB test binary. Mechanical candidates, in dependency |
| 402 | order (each already only depends on `codewhale-config`/`codewhale-paths` |
| 403 | plus third-party crates, and each is consumed through a single module |
| 404 | path today): |
| 405 | |
| 406 | | Candidate crate | From | Why it is a clean cut | Consumers to re-export from | |
| 407 | | --- | --- | --- | --- | |
| 408 | | `codewhale-glyphs` | `crates/tui/src/tui/glyphs.rs` | Constant tables + pure fns; no crate-internal deps. | `crate::tui::glyphs` | |
| 409 | | `codewhale-i18n` | `crates/localization/src/lib.rs` + `crates/localization/locales/*.json` | The `rust_i18n::i18n!` macro compiles all 15 packs into whichever crate hosts it; moving it out means locale-only edits no longer rebuild the TUI. `MessageId` is a plain enum. | `crate::localization` | |
| 410 | | `codewhale-mcp-transport` | `crates/tui/src/mcp/{sse,stdio,external_import}.rs` | Already talks to `codewhale-mcp`; the reviewed-launch binding is the only tui coupling. | `crate::mcp` | |
| 411 | |
| 412 | Rules for the split: pure moves plus `pub use` re-exports at the old |
| 413 | paths, no behavior change, one crate per PR, each PR measured with the |
| 414 | table above (cold build, incremental build, incremental test build, |
| 415 | `cargo test -p codewhale-tui --lib --no-run`). Expected win: the tui |
| 416 | frontend time drops with lines removed; the test-binary link is unchanged |
| 417 | until the tests that live with those modules move with them. |
| 418 | |
| 419 | ## Optional accelerators (not required) |
| 420 | |
| 421 | - `cargo install cargo-nextest` — see above. |
| 422 | - `scripts/dev-cargo.sh` / `scripts/dev-test.sh` — isolated `build-dir` |
| 423 | per worktree plus optional sccache. Override the root with |
| 424 | `CODEWHALE_CACHE_ROOT`; do not commit a machine path. |
| 425 | - One shared `CARGO_TARGET_DIR` only for serialized trunk work (two |
| 426 | cargos on the same target flock). Prefer the helper above. |
| 427 | - `sccache` as `RUSTC_WRAPPER` caches dependency compilation across clean |
| 428 | checkouts when `CARGO_INCREMENTAL=0`, and matches what CI does |
| 429 | (`.github/workflows/ci.yml` uses `mozilla-actions/sccache-action` plus |
| 430 | `Swatinem/rust-cache`). |
| 431 |