返回 CodeWhale
BUILD_PERFORMANCE.md
根目录 / docs / BUILD_PERFORMANCE.md
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
431 lines MARKDOWN