1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
四个优化叠加达成:
1. 2.3.4.
e2e gate(`.dev/scripts/e2e-gate.sh`)验证 agent 的端到端行为:启动一个 recursive
二进制 + mock LLM,跑两个 smoke 场景(write_file / read_file),断言工具调用和
session 记录正确。**真正的测试只占 ~15 秒。**
```
e2e-gate.sh 全流程:
[1] host 侧 cargo build -q ~0.3s(增量,几乎 no-op)
[2] argusai docker build ~12-25min ← 瓶颈:容器内全量 release 编译
[3] argusai image-mock (aimock) 启动 ~5s
[4] argus-setup(启动 recursive-e2e 容器) ~30-60s
[5] argus-run --filter smoke ~10-30s ← 真正的测试
[6] argus-clean + docker rm ~5s
```
**95%+ 的时间花在 [2] docker build**——在 colima 虚拟机里从零编译整个项目。
原 `e2e/Dockerfile`:
```dockerfile
FROM rust:1.88-slim AS builder
WORKDIR /build
COPY Cargo.toml Cargo.lock providers.toml ./
COPY src/ src/
COPY crates/ crates/
RUN cargo build --release -p recursive-cli
```
问题在于 `COPY src/` 和 `RUN cargo build` 在**同一层失效边界**上:src/ 改一行 →
`COPY src/` 层指纹变了 → 后续 `RUN cargo build` 层失效 → cargo **重新编译全部 588
个依赖 crate**(即使依赖的源码根本没变)。
依赖(AWS SDK、tokio、reqwest、serde...)占编译量的 **99.5%**;本项目只有 3 个
workspace crate。每次为 0.5% 的改动重编 99.5% 的依赖——这是浪费的根源。
tui-mutants / agent-mutants / cli-mutants 这些 gate 都有"未动 X 即跳过"的变更检测
(`git diff --name-only` + 路径过滤)。唯独 e2e gate 没有——每次都跑全套 docker
build。但如果改动只触及 `tests/`、`README.md`、`Cargo.toml` 版本号这类路径,根本
不可能影响 agent 的端到端行为,e2e 必然通过。
在 `e2e-gate.sh` 的 `--check-prereqs` 退出之后、`docker build` 之前,加 diff-scope
检测:
```bash
if [[ "${RECURSIVE_E2E_GATE_FORCE:-0}" != "1" ]]; then
fi
fi
```
白名单只含 e2e 真正关心的路径:`src/`(lib 源码)、`crates/*/src/`(各 crate 源码)、
`e2e/`(e2e 套件自身)。其余(docs/tests/.dev 配置)直接绿灯。
`RECURSIVE_E2E_GATE_FORCE=1` 可强制跑(release 前全量验证用)。
纯测试 goal 的 e2e gate:**435 毫秒**(exit 0,无 docker build)。
colima(macOS 上的 Linux VM,跑 Docker)默认 4 CPU / 6GB。Rust release 编译是 CPU
密集型,加 CPU 直接提速。
```bash
colima stop && colima start --cpu 6 --memory 10
```
全量编译从 ~25min 降到 **~9.5min**(提速 ~60%)。零风险、立即生效、对每个 e2e gate
都有帮助。这是纯环境配置,不进仓库。
colima 是轻量级 Docker runtime(替代 Docker Desktop),默认资源配置保守以保证在
普通笔记本上不抢占主机资源。开发密集型编译时,手动加 CPU 是值得的——colima 的 CPU
是软限制(hypervisor 层面),不占满时主机仍可用。
首先尝试了 BuildKit 的 `--mount=type=cache`:
```dockerfile
RUN --mount=type=cache,target=/build/target,sharing=locked \
```
理论上 cache mount 持久化 `target/`,src/ 改动后 cargo 走增量编译。**但实测失败**:
改 src/ 后 build 仍要 **23.8 分钟**(和全量一样)。
**失败原因**:cache mount 确实持久化了 `target/`(`docker buildx du` 显示 550MB
数据),但 cargo 在 src/ 改动后仍全量重编。两个可能原因:
1. 2.
**教训**:cache mount 是 BuildKit 的特性,但它的可靠性依赖 driver 实现。
**Docker 原生 layer cache 更可靠**——这是 cargo-chef 成功的基础。
[cargo-chef](https://github.com/LukeMathWalker/cargo-chef) 是专门解决 Rust Docker
依赖缓存的工具,分三个 stage:
```
planner → cooker → builder
```
**Stage 1: planner**——生成 recipe.json(依赖图描述,不含源码):
```dockerfile
FROM rust:1.88-slim AS planner
RUN cargo install cargo-chef --locked
COPY Cargo.toml Cargo.lock ./
COPY .cargo .cargo
COPY crates crates
RUN cargo chef prepare --recipe-path recipe.json
```
`recipe.json` 包含全部 workspace 成员的 manifests + Cargo.lock + .cargo 配置,但
**不含任何 .rs 源码**。它是依赖图的"配方"。
**Stage 2: cooker**——根据 recipe.json 编译所有依赖:
```dockerfile
FROM rust:1.88-slim AS cooker
RUN cargo install cargo-chef --locked
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
```
`cargo chef cook` 用 recipe.json 构造占位 src(让 cargo 能解析依赖图),编译全部
588 个依赖 crate。这层的输入**只有 recipe.json**——src/ 改动不影响它。
**Stage 3: builder**——COPY 真实 src + cooker 的 target/,编译本项目:
```dockerfile
FROM rust:1.88-slim AS builder
COPY Cargo.toml Cargo.lock providers.toml ./
COPY .cargo .cargo
COPY crates crates
COPY src src
COPY --from=cooker /app/target target # 依赖已编译好
RUN cargo build --release -p recursive-cli # 只编 3 个本项目 crate
```
cargo 看到 target/ 里依赖的 `.rlib` 已存在,只编译本项目 3 个 crate + 链接。
**核心区别**:cache mount 持久化的是 cargo 的**构建产物**(target/),cargo 要自己
判断能不能复用(fingerprint 机制,在 colima 下不可靠)。cargo-chef 持久化的是一整个
**Docker layer**(cooker 的产物),靠的是 Docker 最成熟的 layer cache——只要输入
(recipe.json)不变,这层就 CACHED,不依赖 cargo 的判断。
`cargo chef prepare` 读取的是 `Cargo.toml`(manifest)和 `Cargo.lock`,它们描述了
依赖图的结构(哪些 crate、什么版本、什么 feature)。recipe.json 就是这个依赖图描述
的序列化。**源码内容不在依赖图描述里**——`cargo chef cook` 用占位 `fn main(){}`
生成 dummy crate 来触发依赖编译,不需要真实源码。
这意味着 planner 的输出(recipe.json)只依赖 manifests,**跨 worktree 完全相同**
(不同 worktree 的 Cargo.toml/Cargo.lock 一致,只有 src/ 不同)。
这是最关键的一步——从"main checkout 快、worktree 慢"变成"全都快"。
优化 3 后,main checkout 的 src/ 改动 build 只要 ~1 分钟。但 **self-improve flow
的每个 goal 在独立 worktree 里跑**(`.worktrees/selfimprove-<id>/`),worktree 的
docker build 仍要 **~12 分钟**。
最初的 planner stage:
```dockerfile
COPY Cargo.toml Cargo.lock ./
COPY .cargo .cargo
COPY crates crates
COPY src src # ← 这行是问题
RUN cargo chef prepare --recipe-path recipe.json
```
虽然 `cargo chef prepare` 不需要 src(前面验证过),但 planner **COPY 了 src/**。
src/ 改动让 `COPY src` 层失效 → planner 的 `RUN cargo chef prepare` 层也失效 →
recipe.json 虽然内容不变,但它所在的 layer 重建了 → cooker 的 `COPY --from=planner
recipe.json` 看到一个"新"layer 输入 → cooker 层也失效 → 全量重编依赖。
**删掉 planner 的 `COPY src`**。验证过 `cargo chef prepare` 在无 src/ 时正常生成
recipe.json(175K,和有 src/ 时完全相同)。
这是整个优化最精妙的技术点——**BuildKit 的 layer cache 按文件内容指纹做 key,
不按 build context 路径做 key**。
```
Worktree A 的 build context: /repo/.worktrees/selfimprove-AAA/
Worktree B 的 build context: /repo/.worktrees/selfimprove-BBB/
```
两个 context 路径不同,但内容中:
- -
删掉 planner 的 COPY src 后,planner 的输入只有 manifests(跨 worktree 相同)→
planner layer 指纹相同 → cooker 的 recipe.json 输入相同 → cooker layer 指纹相同 →
**cooker 缓存跨 worktree 自动共享**。
只有 builder stage(COPY src + cargo build)每个 worktree 不同——但它只编 3 个本项目
crate,~1 分钟。
用三个对照实验定论:
实验 C 是决定性的——完全不同的 build context 路径,cooker 层**跨 context 命中**。
BuildKit 的 layer cache 按 **"指令 + 输入文件内容指纹"** 做 key:
- --
这就是为什么 worktree 缓存共享可行:不同 worktree 的 manifests 内容相同 → 相同的
layer key → 命中同一缓存。
一个 Rust workspace 的编译产物分两部分:
- - +
cargo-chef 的本质就是把这两部分**分离到不同的 Docker layer**:cooker 编依赖(稳定),
builder 编本项目(变动)。这样项目代码的变动只触发 builder 层重建。
**结论**:对于 Rust 编译缓存,layer cache(cargo-chef)比 cache mount 更可靠。
`cargo chef cook` 怎么在不看真实源码的情况下编译依赖?它为每个 workspace 成员创建
占位 crate:
- -
占位 crate 提供了最小化的编译目标,让 cargo 能解析依赖图并编译所有外部依赖。
真实源码在 builder stage 才 COPY 进来,此时依赖已编译好,cargo 只编本项目。
colima 默认启用 BuildKit(docker daemon `Server Version: 27.x` 自带)。但要使用
`--mount=type=cache` 等 BuildKit 特性,需要:
- -
声明 `# syntax` 是更可靠的方式——它让 Dockerfile 自带 BuildKit 要求,不依赖外部
环境变量。
**不推荐**。e2e 的瓶颈是单次 docker build 的 CPU 编译。并发跑两个 build = 两个
docker build 争 colima 的 CPU 和 BuildKit 锁,每个都变慢,总时间基本不降,还可能
死锁。
并发只在多个 goal 的 smoke 测试阶段(~15s 那段)才有意义,但它本就不是瓶颈。
host(macOS)编译的二进制是 Mach-O,容器是 Linux——不能直接 COPY。交叉编译
(`--target aarch64-unknown-linux-gnu`)Rust + AWS SDK + openssl 配置复杂、风险高。
不用 cargo-chef,手写 Dockerfile 的"先编依赖再编本项目"分层。可行但繁琐——workspace
有 3 个 target + 跨包 path 依赖,占位 src 要精确伪造整个依赖图。cargo-chef 自动处理
这些,更稳。
```dockerfile
FROM rust:1.88-slim AS planner
WORKDIR /app
RUN cargo install cargo-chef --locked
COPY Cargo.toml Cargo.lock ./
COPY .cargo .cargo
COPY crates crates
RUN cargo chef prepare --recipe-path recipe.json
FROM rust:1.88-slim AS cooker
WORKDIR /app
RUN cargo install cargo-chef --locked
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
FROM rust:1.88-slim AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock providers.toml ./
COPY .cargo .cargo
COPY crates crates
COPY src src
COPY --from=cooker /app/target target
RUN cargo build --release -p recursive-cli
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
COPY --from=builder /app/target/release/recursive /usr/local/bin/recursive
```
1. 2.3.4.5.6.