| 1 | # 构建与测试性能 |
| 2 | |
| 3 | > 英文原文:[BUILD_PERFORMANCE.md](../BUILD_PERFORMANCE.md)。 |
| 4 | > 最后与英文同步日期(last synced with English revision):2026-09-29。 |
| 5 | |
| 6 | 这篇文档记录实测数据:Codewhale 的构建和测试各要多久,为了让贡献者的开发循环 |
| 7 | 更快做了哪些改动,哪些又推迟了。数字来自一台机器(Apple Silicon,14 核,rustc 1.97.0, |
| 8 | Xcode 26.2 `ld-1230`),采集时还跑着另外四个 cargo 任务(1 分钟负载均值 10–27, |
| 9 | 每个数字旁边都记了当时的负载),所以把这些数字当作前后对比的相对证据,不要当基准测试结果。 |
| 10 | |
| 11 | > 拆分计划:[TUI_DECONSTRUCTION.md](../design/TUI_DECONSTRUCTION.md) 记录了 9 月 9 日的 |
| 12 | > 源码审计和当前建议的抽取顺序。下面的测量都是历史数据;B3 和“推迟”两处的候选清单 |
| 13 | > 不是执行队列。 |
| 14 | |
| 15 | ## 时间花在哪里(基线,commit 533c530b) |
| 16 | |
| 17 | | 步骤 | 墙钟 | 说明 | |
| 18 | | --- | --- | --- | |
| 19 | | 冷启动 `cargo build -p codewhale-tui`(空 target) | 94 s(user 270 s) | 543 个单元;单是 `codewhale-tui` 就要 70 s,它是关键路径;紧随其后的单元是 `codewhale-config` 7.5 s、`jsonschema` 6.3 s、`codewhale-workflow` 5.6 s、`tokio` 5.4 s。负载 13。 | |
| 20 | | 冷启动 `cargo test -p codewhale-tui --lib --no-run`(空 target) | 148 s(user 347 s) | 装着 1.05 万个测试的单元测试二进制有 357 MB,会触发 macOS 链接器的 `__eh_frame > 16MB` compact-unwind 警告(无害)。负载 20。 | |
| 21 | | 改一行代码后的增量 `cargo build -p codewhale-tui` | 12.5 s | 负载 11。 | |
| 22 | | 改一行代码后的增量 `cargo test -p codewhale-tui --lib --no-run` | 19 s | 负载 11。 | |
| 23 | | 依赖已预热时 `cargo test --workspace --all-features --locked --no-run` | 155 s(user 366 s) | 61 个测试二进制。负载 6→11。 | |
| 24 | | 近乎冷启动的 `cargo check --workspace --all-targets --locked`(依赖已构建) | 82 s | 负载 19。 | |
| 25 | | 用 libtest 跑 tui 单元测试套件(`cargo test -p codewhale-tui --lib`) | 268 s | 取自发布门禁日志;10,531 个测试。负载约 10。 | |
| 26 | | 用 `cargo nextest run -p codewhale-tui --lib` 跑同一套件 | 96–108 s | 同样的测试,每个测试一个进程,所有核心跑满。负载 12–20。 | |
| 27 | | 用 `cargo nextest run --workspace --all-features` 跑整个工作区 | 353 s | 12,744 个测试,PTY 套件按 nextest 配置串行执行。负载 17。 | |
| 28 | |
| 29 | 这些数字背后的结构性事实: |
| 30 | |
| 31 | - `crates/tui` 约有 74.6 万行 Rust(其中 60.9 万行非测试代码,13.7 万行内联测试 |
| 32 | 分布在 488 个 `#[cfg(test)]` 模块里,另有 1.06 万个 `#[test]`/`#[tokio::test]` |
| 33 | 函数)。它按一个 crate 编译,所以这个 crate 的前端就是每次构建的关键路径, |
| 34 | 而每跑一次单元测试,都要带上 `cfg(test)` 把它重新编译一遍。 |
| 35 | - 依赖已经精简过了(`reqwest` 用 rustls-no-provider,`image` 只要 png, |
| 36 | `syntect` 用 default-fancy,`rmcp` 不带默认特性,`mimalloc` 不带默认特性)。 |
| 37 | `cargo tree -d` 只报常规重复(`toml` 0.8/1.1、`thiserror` 1/2、`strum` 0.27/0.28、 |
| 38 | `syn` 2/3、`sha2` 0.10/0.11),而且都来自第三方 crate,不是工作区的选择造成的。 |
| 39 | 全局分配器默认是 mimalloc;`codewhale-tui`/`codewhale-cli` 上默认关闭的 |
| 40 | `rusty-alloc` cargo 特性会把它换成纯 Rust 重写的 `rusty_alloc`(#5872)。用 |
| 41 | `cargo build -p codewhale-cli --no-default-features --features rusty-alloc` |
| 42 | (或 `-p codewhale-tui`)可以排除 mimalloc 及其 C 构建依赖。Cargo 特性是叠加的: |
| 43 | 只写 `--features rusty-alloc` 时,即使分配工作已经交给 Rust 分配器,默认的 |
| 44 | mimalloc 依赖依然在。这样做能去掉分配器的 C 构建路径;其他原生依赖可能仍然 |
| 45 | 需要 C 工具链。两个分配器特性都不开时,用标准库的系统分配器。 |
| 46 | - `[profile.dev] debug = "line-tables-only"` 已经设好了(#5246),macOS 上 Cargo |
| 47 | 也已经使用 `split-debuginfo = unpacked`。 |
| 48 | - `target/debug` 会超过 50 GB,但那是不同特性集和不同 worktree 长期累积的结果; |
| 49 | 一次全新的测试构建约 7 GB。 |
| 50 | |
| 51 | ## A0 回执(commit 533c530b + 封闭性修复;空 target 目录) |
| 52 | |
| 53 | `CARGO_TARGET_DIR=/Volumes/VIXinSSD/CW/.tmp/compile-speed-baseline`,HTML 计时报告 |
| 54 | 存档在 `backups/compile-speed-evidence-20260815/`(a0-cold-lib-test-timing.html、 |
| 55 | a0-incremental-lib-test-timing.html、a0-llvm-lines-top40.txt)。 |
| 56 | |
| 57 | | 回执 | 墙钟 | 负载(1 分钟) | |
| 58 | | --- | --- | --- | |
| 59 | | 冷启动 `cargo test -p codewhale-tui --lib --locked --no-run --timings` | 127 s(user 329 s) | 8.9 | |
| 60 | | `touch crates/runtime/src/elapsed.rs` + 同样的命令 | 21 s | 12.3 | |
| 61 | | `touch` + `cargo test -p codewhale-tui --lib --locked elapsed::`(日常循环) | 20 s(跑了 4 个测试) | 11.2 | |
| 62 | | 库测试二进制大小 | 357 MB(`codewhale_tui-<hash>`);链接时报 `__eh_frame section too large (max 16MB)` compact-unwind 警告 | — | |
| 63 | | `touch` 之后增量执行 `cargo check -p codewhale-tui --lib --tests`(仅前端) | 14 s | 6.2 | |
| 64 | | `touch` 之后增量做完整库测试构建,条件相同 | 28 s | 6.2 | |
| 65 | |
| 66 | 冷启动计时报告里耗时最长的单元(共 605 个单元):`codewhale-tui` 库测试 |
| 67 | **106.0 s**,`codewhale-config` 7.9 s,`jsonschema` 5.8 s,`moxcms` 4.7 s, |
| 68 | `codewhale-protocol` 4.2 s,`tokio` 4.0 s,`rustls` 3.8 s,`schemaui` 3.4 s, |
| 69 | `rmcp` 3.4 s,`h2` 3.3 s,`jsonschema`(第二份副本)3.2 s,`codewhale-workflow` |
| 70 | 3.2 s,`syn` 3.1 s,`rio-vt` 3.1 s,`regex-automata` 3.0 s。增量报告里只有一个 |
| 71 | 非零单元:`codewhale-tui` 库测试 20.8 s。所以日常要交的“税”就是 tui crate 自己, |
| 72 | 大致一半花在前端(check --tests 14 s),一半花在代码生成加链接(合计 28 s); |
| 73 | 耗时不在依赖和链接器上。 |
| 74 | |
| 75 | `cargo llvm-lines -p codewhale-tui --lib`:**8,138,810 行、223,052 份副本**。 |
| 76 | 最大的单个函数是 `rust_i18n` 后端闭包(`_RUST_I18N_BACKEND::{closure#0}`, |
| 77 | 311,782 行,单它一个就占了这个 crate 的 3.8 %——`i18n!` 宏把这 15 个语言包都编译进 |
| 78 | 一个 match),接着是 `run_event_loop` 2.7 万行、`Engine::run_turn` 2.6 万行、 |
| 79 | `RuntimeThreadManager::monitor_turn` 1.6 万行,然后是 `Config`/`ProvidersConfig`/ |
| 80 | `Settings` 的 serde `Deserialize` 展开(各 5–6 千行,每个 toml 反序列化器都有 |
| 81 | 好几份副本)。 |
| 82 | |
| 83 | ### A0.1 依赖棘轮 |
| 84 | |
| 85 | `cargo metadata --locked` 数出 **690** 个包,`cargo deny check bans` 对重复的 |
| 86 | `fancy-regex`、`jsonschema`、`jsonschema-regex`、`referencing` 报警,另外还报了 |
| 87 | 过期的 `jni`/`jni-sys`/`redox_syscall` 跳过项。原因:工作区把 `jsonschema` 的 pin |
| 88 | 提到了 0.49,而 `schemaui` 0.12(含最新的 0.12.4)仍然要求 `^0.46`。把工作区 |
| 89 | pin 改回 0.46 系列,第二套 jsonschema 依赖栈就消失了(**685** 个包;deny bans |
| 90 | 和 advisories 都干净;`--locked` 能解析;codewhale-workflow-js 的 61 个测试和 |
| 91 | tui 的 schema 测试都通过)。冷启动省下的就是那两个重复单元(单元时间约 9 s, |
| 92 | 墙钟约 3 s)。 |
| 93 | |
| 94 | ### A1 缓存拓扑(本机使用,未提交) |
| 95 | |
| 96 | 新建 worktree 的冷启动 `cargo test -p codewhale-tui --lib --locked --no-run`, |
| 97 | 同一台机器,连续执行: |
| 98 | |
| 99 | | 拓扑 | 墙钟 | CPU(user) | 说明 | |
| 100 | | --- | --- | --- | --- | |
| 101 | | 每个 worktree 各自全新 target(对照) | 127 s | 329 s | A0 | |
| 102 | | 一个共享的 `CARGO_TARGET_DIR`(已被另一个 worktree 预热) | 121 s | 188 s | 依赖复用;工作区里每个 crate 都要重编(按路径做键);没观察到等锁;target 14 GB | |
| 103 | | 每个工作区各自的 `build.build-dir = ".../{workspace-path-hash}"` 加共享的预热 `sccache`(`CARGO_INCREMENTAL=0`) | 107 s | 161 s | sccache 命中率 73.6 %(337 个 Rust 依赖单元全命中;125 次未命中都是工作区 crate);每个工作区 2.7 GB build 目录加 483 MB 缓存;*填充*缓存的那条命令本身耗时 108 s / 157 s CPU | |
| 104 | |
| 105 | 每种拓扑里,墙钟时间都由 tui crate 决定;这些拓扑换来的是 CPU 的节省(约 50 %), |
| 106 | 而在多份 checkout 同时构建时,省 CPU 才是关键。推荐的用户级 |
| 107 | `~/.cargo/config.toml`(两个根路径按需调整): |
| 108 | |
| 109 | ```toml |
| 110 | [build] |
| 111 | # One build root for every checkout; each workspace gets its own subdir, |
| 112 | # so worktrees never wait on each other's target lock. |
| 113 | build-dir = "/path/to/cache/codewhale/build/{workspace-path-hash}" |
| 114 | # Optional: reuse dependency compilation across checkouts. |
| 115 | # rustc-wrapper = "sccache" |
| 116 | ``` |
| 117 | |
| 118 | 这次测量用的机器上,`sccache` 是用 `brew install sccache` 装的。 |
| 119 | |
| 120 | ### 公共辅助脚本(A1/A5)——`dev-test.sh` 现在到底做什么 |
| 121 | |
| 122 | `scripts/dev-test.sh` 以前只把某个区域映射到 `cargo test -p`。它**没有**启用实测的 |
| 123 | build-dir + sccache 拓扑,所以新 worktree 仍然要往 `./target` 里做一次冷编译。 |
| 124 | |
| 125 | `scripts/dev-cache.sh` 是可移植的按需启用辅助脚本。`scripts/dev-cargo.sh` 和 |
| 126 | `scripts/dev-test.sh` 都 source 它。 |
| 127 | |
| 128 | | 类别 | 改了什么 | 它不是什么 | |
| 129 | | --- | --- | --- | |
| 130 | | **编译期** | `scripts/dev-test.sh` / `scripts/dev-cargo.sh` 会设置 `CARGO_BUILD_BUILD_DIR=$CODEWHALE_CACHE_ROOT/build/{workspace-path-hash}`,这样并发的 worktree 就不会抢同一个 Cargo 锁。残留的 `./target`(拆分 build-dir 后 Cargo 仍会往那里写 `CACHEDIR.TAG`)**不会**让隔离失效;如果你确实想留着 `./target`,用 `CODEWHALE_DEV_CACHE=local`。Cargo 低于 1.91 时,会退回按工作区设置 `CARGO_TARGET_DIR`。 | 不能缩小 rustc 编译单元。工作区 crate 照样重编。 | |
| 131 | | **编译期(sccache)** | 只有在增量已经关闭(`CARGO_INCREMENTAL=0` 或 `CODEWHALE_SCCACHE=1`)**且** `sccache` 在 `PATH` 上时,才设置 `RUSTC_WRAPPER=sccache` 和 `SCCACHE_DIR=$CODEWHALE_CACHE_ROOT/sccache/<rustc-commit>`。 | 日常增量循环里不启用。sccache 缓存不了增量单元;包装这类构建只会增加开销,命中率 0%。sccache 缺失时只打印一句回退说明,不算错误。 | |
| 132 | | **测试运行时** | 装了 `cargo-nextest` 时,`scripts/dev-test.sh` 用 `cargo nextest run`(`CODEWHALE_DEV_NEXTEST=0` 强制走 libtest)。二进制不变;每个测试一个进程。重试次数保持 0。`RUST_MIN_STACK=16MiB` 未设置时会导出。 | 不是编译优化。nextest 不跑 doctest;`cargo test --doc` 仍是单独的门禁。 | |
| 133 | | **易用性** | `--list` 和路径映射覆盖每个工作区 crate(`app-server`、`workflow-js` 等)。`scripts/dev-cache.sh --status` / `--self-check` 会打印当前拓扑。 | 不改变产品行为。 | |
| 134 | |
| 135 | 默认值里不含任何机器专属的绝对路径: |
| 136 | |
| 137 | ```sh |
| 138 | # Portable default: |
| 139 | # ${XDG_CACHE_HOME:-$HOME/.cache}/codewhale |
| 140 | # Desk override, if you want the cache on a particular volume: |
| 141 | export CODEWHALE_CACHE_ROOT=/path/to/cache/codewhale |
| 142 | |
| 143 | scripts/dev-cache.sh --self-check |
| 144 | scripts/dev-test.sh crates/runtime/src/elapsed.rs |
| 145 | CARGO_INCREMENTAL=0 scripts/dev-cargo.sh test -p codewhale-config --lib --locked --no-run |
| 146 | ``` |
| 147 | |
| 148 | 封闭性脚本测试(不编译 rustc):`sh scripts/dev-cache.test.sh`。 |
| 149 | `scripts/dev-test.sh --self-check` 会报告辅助脚本解析出的缓存拓扑;它自己的脚本 |
| 150 | 测试 `scripts/dev-test.test.sh` 已在 `d64b9429b7` 中移除。 |
| 151 | |
| 152 | ### 辅助脚本验证(2026-08-15,本 worktree) |
| 153 | |
| 154 | 记录于其他支线让出机器之后(负载 3.2–5.6)。rustc 1.97.0,cargo 1.97.0, |
| 155 | sccache 0.17.0。这次运行把 `CODEWHALE_CACHE_ROOT` 设成卷内的一个覆盖路径; |
| 156 | 没有删任何缓存或 target。 |
| 157 | |
| 158 | 对这个 worktree,Cargo 把 `{workspace-path-hash}` 展开成 `build/d4/96565f96fb3682`。 |
| 159 | 第一次隔离执行 `codewhale-config` 的 `--no-run` 时创建了一个占位的 `./target` |
| 160 | (`CACHEDIR.TAG`);如果把它当成预热过的传统 target,下一条命令就会重新编译进 |
| 161 | `./target`(8.65 s)。现在除非设置 `CODEWHALE_DEV_CACHE=local` 或 `0`,辅助脚本 |
| 162 | 都会保持隔离。 |
| 163 | |
| 164 | **编译期**(`scripts/dev-cargo.sh test … --locked --offline --no-run`): |
| 165 | |
| 166 | | 步骤 | 墙钟 | 说明 | |
| 167 | | --- | ---: | --- | |
| 168 | | 第一次隔离执行 `codewhale-config --lib --no-run` | 9.14 s(user 23.8 s) | 90 个单元写进带哈希的 build-dir | |
| 169 | | 预热后隔离执行同样的命令(修掉占位 target 问题之后) | 0.13 s | `Finished` 用 0.07 s | |
| 170 | | `touch crates/config/src/lib.rs` + 隔离 `--no-run` | 0.93 s | 只重编了 `codewhale-config` | |
| 171 | | 第一次隔离执行 `codewhale-tui --lib --no-run` | **121.5 s**(user 305 s) | 600 个单元;二进制 340 MB;A0 空 target 是 127 s / 329 s | |
| 172 | | `touch crates/runtime/src/elapsed.rs` + 隔离 `--no-run` | **18.15 s** | 日常编译循环;A0 是 21 s / 19 s | |
| 173 | | 在已经预热的树上执行 `CODEWHALE_SCCACHE=1` config `--no-run` | 5.21 s,随后 0.14 s | 设置了 wrapper 和 `SCCACHE_DIR=…/sccache/<rustc-commit>`;sccache 命中 0 次,因为只有工作区 crate 重编,而且 build-dir 没被清空 | |
| 174 | |
| 175 | **测试运行时**: |
| 176 | |
| 177 | | 步骤 | 墙钟 | 说明 | |
| 178 | | --- | ---: | --- | |
| 179 | | `scripts/dev-test.sh config`(nextest,557 个测试) | run 0.479 s / real 2.40 s | 含一次 0.85 s 的 profile 切换编译 | |
| 180 | | `CODEWHALE_DEV_NEXTEST=0 scripts/dev-test.sh config`(libtest) | body 0.11 s / real 0.27 s | 557 个小测试:这里每个测试一个进程反而更慢 | |
| 181 | | `scripts/dev-test.sh crates/runtime/src/elapsed.rs` | run 0.023 s / real 2.81 s | 4 个通过,10,516 个跳过;nextest 过滤器有效 | |
| 182 | |
| 183 | 268 s → 约 100 s 的 nextest 收益仍是更早那条 tui 单元测试套件回执。config 太小, |
| 184 | 吃不到这个收益;对不加过滤的 crate/工作区运行来说,nextest 仍然是合适的默认选择。 |
| 185 | |
| 186 | **易用性:** 当时 `sh` 和 `dash` 都能通过 `dev-cache.test.sh`(22)和 |
| 187 | `dev-test.test.sh`(27);`dev-test.test.sh` 后来被移除(`d64b9429b7`)。 |
| 188 | sccache 缺失会走回退。`--list` 覆盖每个工作区 crate。 |
| 189 | |
| 190 | ### A2 CI 中的 nextest |
| 191 | |
| 192 | `cargo test --workspace --all-features --locked --doc` 清点出 **21 个 crate 中 |
| 193 | 3 个通过 / 8 个忽略的 doctest**;CI 把它们保留为独立一步,与 |
| 194 | `cargo nextest run --workspace --all-features --locked --profile ci` 并列。 |
| 195 | |
| 196 | ### A3/A4(已测量,未采用) |
| 197 | |
| 198 | 前端和代码生成在 tui 单元里大致各占一半(增量 14 s / 14 s);链接器只占其中 |
| 199 | 一小部分,而依赖在首次构建后就已经预热。所以 `[profile.dev.package."*"] opt-level = 1` |
| 200 | (配对结果见上)、`-Wl,-dead_strip` 和其他 `RUSTFLAGS` 都不进仓库(它们会作用到 |
| 201 | 发布的 profile 上);macOS 上 `split-debuginfo` 已经是 `unpacked`。 |
| 202 | |
| 203 | ## 内存峰值(为什么 OHOS/Windows 构建会出现两个约 4 GB 的 rustc 进程) |
| 204 | |
| 205 | 对本支线 target 目录下的每个 rustc 进程每秒采样一次 `ps -o rss` |
| 206 | (`backups/compile-speed-evidence-20260815/rss-sample.sh`、`mem-incremental.log`、 |
| 207 | `mem-cold-cgu.log`);每行一个 rustc。 |
| 208 | |
| 209 | | 单元 | 模式 | RSS 峰值 | 墙钟 | 负载 | |
| 210 | | --- | --- | --- | --- | --- | |
| 211 | | `codewhale-tui` lib(dev) | 增量,cgu 256 | 3.3 GB | 12–14 s | 6.3 | |
| 212 | | `codewhale-tui` lib test | 增量,cgu 256 | 6.0 GB | 21–28 s | 6.3 | |
| 213 | | `codewhale-tui` lib(dev) | 非增量(`CARGO_INCREMENTAL=0`),cgu 16 | **6.0 GB** | 78 s | 5.5 | |
| 214 | | `codewhale-tui` lib test | 非增量,cgu 16 | **8.0 GB** | 105 s | 5.5 | |
| 215 | | `codewhale-tui` lib test | 非增量,`codegen-units = 4` | 6.1 GB(−24 %) | 145 s(+38 %) | 5.5 | |
| 216 | | `codewhale-tui` lib test | 非增量,`codegen-units = 1` | 7.8 GB(−3 %) | 161 s(+53 %) | 5.5 | |
| 217 | | 次大的几个单元(codewhale-config、rmcp、tokio、schemaui、codewhale-workflow) | 任一模式 | 0.4–0.7 GB | — | — | |
| 218 | |
| 219 | 所以单跑一个 `cargo build -p codewhale-tui`,一个 rustc 就要约 6 GB;单元测试 |
| 220 | 构建要约 8 GB;而 `cargo test --workspace`(或 `--all-targets`)会把 tui crate 的 |
| 221 | lib 和 lib test 两个单元与 CLI 一起排进并发队列,这正是社区成员在 Windows 上为 |
| 222 | OHOS 交叉编译时报告的“两个各占约 4 GB 的 rustc 进程”(不同系统对 RSS 的统计 |
| 223 | 方式不同,形状是一样的)。内联测试模块给这个 crate 的峰值再加约 2 GB(+33 %); |
| 224 | 峰值和时间两头都被泛型膨胀推高(810 万行 LLVM、`rust_i18n` 闭包 31.2 万行、 |
| 225 | config 结构体的 serde `Deserialize` 展开)。减少代码生成单元,峰值降得不多, |
| 226 | 墙钟时间却涨得很多,默认不采用。 |
| 227 | |
| 228 | ### 低内存构建配方(内存小于 16 GB 的机器、交叉构建) |
| 229 | |
| 230 | ```bash |
| 231 | # One rustc at a time: the tui lib and its unit-test build never overlap. |
| 232 | export CARGO_BUILD_JOBS=1 # or: cargo build -j1 ... |
| 233 | # Only the crate you are working on, only its library: |
| 234 | cargo build -p codewhale-tui |
| 235 | cargo test -p codewhale-tui --lib -- <filter> |
| 236 | # Do NOT use --workspace/--all-targets on a small machine; run crates one |
| 237 | # at a time (scripts/dev-test.sh <area> picks the narrowest command). |
| 238 | # Optional, if 8 GB for the unit-test build is still too much (slower): |
| 239 | export CARGO_PROFILE_DEV_CODEGEN_UNITS=4 # ~6 GB peak, ~+40 % wall |
| 240 | # Cross-builds (e.g. OHOS) inherit the same numbers: add -j1 to the |
| 241 | # cargo/ohrs invocation and build the release profile, which peaks lower |
| 242 | # than the unit-test build because it carries no test modules. |
| 243 | ``` |
| 244 | |
| 245 | ### B1(剥离巨型测试)——已审计,但在约束下无法落地 |
| 246 | |
| 247 | 最大的六个内联测试文件(tui/ui/tests.rs 2.21 万行 / 643 个测试, |
| 248 | tools/subagent/tests.rs 1.87 万 / 448,core/engine/tests.rs 1.78 万 / 358, |
| 249 | config/tests.rs 1.27 万 / 393,runtime_threads/tests.rs 9.3 千 / 141, |
| 250 | runtime_api/tests.rs 9.1 千 / 151)分别引用 crate 内部项 826 / 226 / 627 / |
| 251 | 85 / 152 / 217 次(`crate::llm_client::mock`、 |
| 252 | `crate::test_support::{EnvVarGuard, lock_test_env}`、 |
| 253 | `core::engine::mock_engine_handle`、`crate::tui::app::App` 等),而 codewhale-tui |
| 254 | 库总共只对外暴露四个 `pub` 项。它们全是白盒测试;除非把模块树公开,否则一个都 |
| 255 | 搬不到 `crates/tui/tests/` 去——而本支线接到的要求就是不要这么做。这个方案本可以 |
| 256 | 换来的收益是测试模块给 lib-test 单元加上的约 2 GB / 约 35 s;但要拿到它,先得做一个决定: |
| 257 | 要么提供 `#[doc(hidden)] pub mod test_api`(为黑盒测试子集用到的那约 30 个符号 |
| 258 | 提供一个有意公开、不稳定的接口),要么接受单元测试套件留在 crate 内部。 |
| 259 | 这里只做记录,没有动手。 |
| 260 | |
| 261 | ### B2 已落地(把叶子类型移出 codewhale-tui) |
| 262 | |
| 263 | | 迁移 | 移出 tui 的行数 | 受影响的调用方 | |
| 264 | | --- | --- | --- | |
| 265 | | `core/tool_parser.rs` → `codewhale_core::tool_parser` | 662 | 0(re-export;集成 harness 改成 import 而不是 `#[path]`) | |
| 266 | | `tls.rs` → `codewhale_release::tls` | 21 | 0(crate 根处写 `use codewhale_release::tls;`) | |
| 267 | | `AppMode`(含纯实现)→ `codewhale_config::AppMode`;本地化的选择器字符串仍留在 `AppModeUi` | 约 150 | 3 个文件 import 这个 trait | |
| 268 | | `ApprovalMode`(含纯实现)→ `codewhale_execpolicy::ApprovalMode` | 约 60 | 0(re-export) | |
| 269 | |
| 270 | 合起来只占这个 crate 74.6 万行里的约 900 行:依赖方向理顺了,但 tui 单元的时间 |
| 271 | 和内存暂时看不出可测量的变化(上面那个 8.0 GB 的 lib-test 峰值是在这些迁移之后 |
| 272 | 采样的)。没有迁移的项以及原因:`ReasoningEffort`——它的实现要用到 TUI 自己 |
| 273 | 定义的 `ApiProvider`(`crates/tui/src/config.rs`),还要调用 |
| 274 | `crate::config::is_exact_*_k3_route` / `crate::provider_lake`,所以必须先迁 |
| 275 | `ApiProvider`(见下面 B3);`approval/policy.rs`(风险分类)依赖 `command_safety` |
| 276 | 和 `auto_review`;`hashing.rs` 是 15 行 sha2 包装,53 处调用点,单独搬出去对 |
| 277 | 编译时间没有价值;那些没参与编译的 |
| 278 | `core/runtime_contract/{budget,context,ledger,manifest,profile,progress,retry,terminal,work}.rs` |
| 279 | 没有任何调用方,也没有构建成本(`core/mod.rs` 把它们记为分阶段搭的脚手架, |
| 280 | TUI-DOG-017)——保持原样。 |
| 281 | |
| 282 | ### B3 顺序 |
| 283 | |
| 284 | 1. 把 `ApiProvider` 和精确路由辅助函数(`is_exact_*_route`)从 |
| 285 | `crates/tui/src/config.rs` 移进 codewhale-config,从而解开 `ReasoningEffort` |
| 286 | 的阻塞。**尚未开始,现在是关键路径**——见第 4 条。 |
| 287 | 2. **已落地。** `localization` + `locales/*.json` → `codewhale-localization` |
| 288 | (31.2 万行的 `rust_i18n` 闭包离开了 tui 单元;只改语言包不再重编 TUI)。 |
| 289 | 3. **已落地。** `palette` → `codewhale-palette`;`command_safety` → |
| 290 | `codewhale-execpolicy`(它本来就拥有 `ApprovalMode`,所以这次迁移是去掉一条 |
| 291 | 依赖边,而不是新增)。 |
| 292 | 4. `client/`(各提供商(provider)的传输协议适配器)→ `codewhale-client`:**被第 1 条卡住, |
| 293 | 不只是排在它后面而已。** 排除文档注释和 `#[cfg(test)]` 块之后,`client` 仍有 |
| 294 | 20 条生产代码里的 `crate::` 依赖边。其中三条很难处理: |
| 295 | - `crate::config`——`Config`、`ProvidersConfig`、`ProviderConfig`、`TuiConfig`、 |
| 296 | `ApiProvider`、`RetryPolicy`、`validate_route`、`wire_model_for_provider_route`, |
| 297 | 还有约 130 个提供商 base-URL / model-id 常量。`crates/tui/src/config` |
| 298 | 本身有 2.97 万行,在生产代码里还依赖 `config_persistence`、`oauth`、 |
| 299 | `credentials`、`tui`、`fleet`、`goal_loop`、`sandbox`、`lsp` 等,所以它没法 |
| 300 | 跟着 `client` 一起搬出去。 |
| 301 | - `crate::tools` ⇄ `client` 是真正的循环依赖:`client` 用 |
| 302 | `tools::schema_sanitize`、`tools::large_output_router` 和 `tools::truncate`, |
| 303 | 而 `tools/{spec,review,registry,rlm,verify,speech,fim,web_search,web/backend,subagent/advisor}.rs` |
| 304 | 用 `client::{CodewhaleClient, ProviderNativeSearchClient, |
| 305 | ProviderNativeSearchRequest, SpeechSynthesisRequest, |
| 306 | RemoteControlInferencePermit}`。 |
| 307 | - `crate::core` ⇄ `client` 形状相同:`client` 用 |
| 308 | `core::events::bounded_tool_projection_warning_names`,而 |
| 309 | `core/{engine,engine/preview,engine/dispatch,engine/turn_loop,engine/reviewer,protocol_parity}.rs` |
| 310 | 用 `client::{CodewhaleClient, PreparedOutboundRequest, canonical_json, |
| 311 | parse_usage, is_reasoning_replay_placeholder, redact_url_for_display}`。 |
| 312 | 所以第 1 条是全部前提:先把 `ApiProvider`、精确路由辅助函数和提供商常量 |
| 313 | 移进 codewhale-config,然后再重新测量 `tools` 和 `core` 这两处的循环依赖。 |
| 314 | 5. **已落地,属于第 4 条中可做的那部分。** `models` + `model_catalog` → |
| 315 | `codewhale-models`(1,835 行,140 个调用方文件)。它们在依赖主干上正好位于 |
| 316 | `client` 下面,彼此之间只有一条生产依赖边,对 TUI 其余部分则没有依赖边; |
| 317 | 而 `models` 本来就已经有一半是在给 `codewhale_core::{request, role}` 做 re-export 门面。 |
| 318 | 6. 然后是 `fleet/`、`tools/`、`core/engine`——切开的位置就是它们的测试本来就遵守的 |
| 319 | crate 边界,测量用 A0 表格。 |
| 320 | |
| 321 | ## 每台机器只跑一个构建 |
| 322 | |
| 323 | `scripts/dev-cargo.sh` 和 `scripts/dev-test.sh` 在整个 Cargo 调用期间持有机器级 |
| 324 | 独占构建锁(`<cache root>/build.lock`,由 `scripts/build-lock.py` 实现)。Cargo |
| 325 | 自带的锁是按 target 目录分的,所以两个智能体(agent)往不同的 target 目录构建时依然会并发 |
| 326 | 跑起来,把内存吃光。第二个构建会等待,并打印出锁在谁手里。设置 |
| 327 | `CODEWHALE_BUILD_LOCK=0` 可以跳过锁,设置 `CODEWHALE_BUILD_LOCK_FILE` 可以指定 |
| 328 | 锁文件。同一台机器上如果跑着自托管 CI runner,而它的 `.env` 把 |
| 329 | `CODEWHALE_BUILD_LOCK_FILE` 指向同一个路径,runner 也会参与这把锁:于是 macOS Test |
| 330 | 任务从第一次测试构建一直持锁到任务结束。这把锁是建议性的:绕过这些脚本直接 |
| 331 | 启动 Cargo 不会取锁;在没有 `fcntl` 的平台上(Windows),会先打印一条警告,然后不加锁构建。 |
| 332 | |
| 333 | ## 本支线改了什么 |
| 334 | |
| 335 | 1. **`scripts/dev-cache.sh` / `scripts/dev-cargo.sh` 启用了从 |
| 336 | `scripts/dev-test.sh` 实测出来的隔离 build-dir 拓扑。** 除非停用辅助脚本,新 |
| 337 | worktree 不再往私有的冷 `./target` 里编译。sccache 按需启用,且受增量 |
| 338 | 开关限制。脚本自检位于 `scripts/dev-cache.test.sh` 和 |
| 339 | `scripts/dev-test.sh --self-check`(`scripts/dev-test.test.sh` 已在 |
| 340 | `d64b9429b7` 中移除)。 |
| 341 | 2. **`cargo nextest` 已支持并有文档**(`.config/nextest.toml`)。测试二进制不变, |
| 342 | 每个测试一个进程,所以 tui 单元测试套件在这台机器上约 100 s 跑完,而不是 |
| 343 | 约 270 s;慢测试或卡住的测试会直接报出名字,而不是把整个二进制拖住。PTY 二进制 |
| 344 | 固定为一次只跑一个测试(它要操作伪终端和共享的 mock 服务器;现在它靠 |
| 345 | 进程内互斥锁串行,而 nextest 的“每测试一进程”模型本来会绕过这把锁); |
| 346 | 而会启动真实 `codewhale` 可执行文件的集成二进制,则限制最多四个测试并发, |
| 347 | 这样它的 30 s 启动预算在满负载机器上也能撑住。 |
| 348 | `cargo test --workspace --all-features --locked` 仍是权威门禁;nextest 是 |
| 349 | 本地循环。 |
| 350 | 3. **有三个测试依赖执行顺序**——它们能通过,只是因为同一进程里另一个测试先 |
| 351 | 安装了 rustls 加密提供商(crypto provider): |
| 352 | `codewhale-tui mcp::sse::endpoint_tests::message_before_endpoint_is_rejected_instead_of_buffered`、 |
| 353 | `codewhale-app-server tests::failed_config_set_keeps_the_stdio_bridge`,以及 |
| 354 | `tests::successful_config_set_still_invalidates_the_stdio_bridge`。现在每个 |
| 355 | 测试自己安装提供商,跟生产代码启动时的做法完全一致。运行时代码没有任何改动。 |
| 356 | 4. **CONTRIBUTING.md 增加了一节 “Fast local loop”**:先讲 |
| 357 | `scripts/dev-cargo.sh` / `scripts/dev-test.sh`,然后是定向的 `-p` 过滤、 |
| 358 | nextest、每个 worktree 各自的隔离 build 目录,以及下面那些可选的加速项。 |
| 359 | 共享 `CARGO_TARGET_DIR` 只在串行做主干工作时才推荐。 |
| 360 | |
| 361 | ## 已测量但刻意未采用 |
| 362 | |
| 363 | - `[profile.dev.package."*"] opt-level = 1`(依赖只优化一次,工作区 crate 不动)。 |
| 364 | 配对测量,连续执行,target 布局相同:从空 target 冷启动 |
| 365 | `cargo test -p codewhale-tui --lib --no-run` 从 148 s 变成 193 s |
| 366 | (user 347 s → 778 s);nextest 下的 tui 单元测试套件从 96 s 变成 81 s; |
| 367 | 增量重建没变化。测试快约 15 %,代价却是冷构建贵 2.2 倍,对第一次想构建 Codewhale |
| 368 | 的人来说不划算。如果贡献者平时主要是反复跑测试,可以自行在本地启用:把这段 |
| 369 | 配置加到用户级 `~/.cargo/config.toml` 的 `[profile.dev.package."*"]` 段里。 |
| 370 | - 在仓库的 `.cargo/config.toml` 里加额外的 `RUSTFLAGS`/链接器参数 |
| 371 | (`-no_deduplicate`、替代链接器)。Rustflags 会作用到每个 profile,可能改变 |
| 372 | 发布的二进制;macOS 的系统链接器已经是 `ld-prime`,而实测的增量链接开销就在 |
| 373 | 上面 12–19 s 的增量数字里。改为作为可选的本地加速项记录在文档里。 |
| 374 | |
| 375 | ## 推迟:拆分 `codewhale-tui` |
| 376 | |
| 377 | 唯一还能改变这些数字形态的杠杆,是把 crate 拆开,这样改动一个叶子模块时, |
| 378 | 不必重新类型检查 60 万行代码,也不必重新链接 357 MB 的测试二进制。按依赖顺序排列的 |
| 379 | 机械式候选(每一个目前都只依赖 `codewhale-config`/`codewhale-paths` 加上第三方 |
| 380 | crate,而且调用方今天也只通过单一模块路径使用它们): |
| 381 | |
| 382 | | 候选 crate | 来自 | 为什么可以干净地切出去 | 需要 re-export 的调用方 | |
| 383 | | --- | --- | --- | --- | |
| 384 | | `codewhale-glyphs` | `crates/tui/src/tui/glyphs.rs` | 常量表加纯函数;不依赖 crate 内部任何东西。 | `crate::tui::glyphs` | |
| 385 | | `codewhale-i18n` | `crates/localization/src/lib.rs` + `crates/localization/locales/*.json` | `rust_i18n::i18n!` 宏会把全部 15 个语言包编译进承载它的那个 crate;搬出去之后,只改语言包不再重编 TUI。`MessageId` 是普通枚举。 | `crate::localization` | |
| 386 | | `codewhale-mcp-transport` | `crates/tui/src/mcp/{sse,stdio,external_import}.rs` | 已经在和 `codewhale-mcp` 通信;与 tui 的唯一耦合点是 reviewed-launch 绑定。 | `crate::mcp` | |
| 387 | |
| 388 | 拆分规则:只做纯搬迁,在原路径上加 `pub use` re-export,不改行为,一个 PR |
| 389 | 一个 crate,每个 PR 都用上面的表来测量(冷构建、增量构建、增量测试构建、 |
| 390 | `cargo test -p codewhale-tui --lib --no-run`)。预期收益:tui 前端耗时会随搬走的 |
| 391 | 行数一起下降;在这些模块的测试跟着搬走之前,测试二进制的链接时间不变。 |
| 392 | |
| 393 | ## 可选加速项(非必需) |
| 394 | |
| 395 | - `cargo install cargo-nextest`——见上文。 |
| 396 | - `scripts/dev-cargo.sh` / `scripts/dev-test.sh`——每个 worktree 隔离的 |
| 397 | `build-dir`,外加可选的 sccache。用 `CODEWHALE_CACHE_ROOT` 覆盖根路径; |
| 398 | 不要把机器专属路径提交进仓库。 |
| 399 | - 共享一个 `CARGO_TARGET_DIR` 只在串行做主干工作时才用(两个 cargo 落到同一个 |
| 400 | target 会 flock)。优先用上面的辅助脚本。 |
| 401 | - `sccache` 作为 `RUSTC_WRAPPER` 时,在 `CARGO_INCREMENTAL=0` 下可以跨干净的 |
| 402 | checkout 缓存依赖编译,而且和 CI 的做法一致(`.github/workflows/ci.yml` 用 |
| 403 | `mozilla-actions/sccache-action` 加 `Swatinem/rust-cache`)。 |
| 404 |