首页
/ opencode 测试套件提速实践:从全量基准到按文件剖析的假设驱动优化方法

opencode 测试套件提速实践:从全量基准到按文件剖析的假设驱动优化方法

2026-09-06 14:15:35作者:滑思眉Philip

perf/test-suite.md 记录了 opencode 仓库针对 packages/opencode 测试套件的一次完整提速研究:如何在不降低覆盖率、不掩盖失败的前提下缩短全量测试时间。本文基于该文档并结合仓库源码,完整还原其基准命令、指标体系、34 项已保留的优化决策与 5 个被放弃的尝试,并深入剖析配套的基准脚本(bench-test-suite.tsprofile-test-files.ts)和测试夹具基础设施(test/lib/effect.ts),读完你可以掌握一套“单跑中位数基准 + 按文件剖析 + 假设循环验证”的测试提速方法论,并理解其中每项优化背后的源码依据。

一、优化目标与边界

文档开宗明义地给出了唯一目标与硬约束:

Speed up the packages/opencode test suite without reducing coverage or hiding failures.

即:只优化耗时,不允许通过删测试、放宽断言或降低隔离强度来“赢得”时间。围绕这一约束,文档划定了文件范围(Files In Scope)

  • packages/opencode/test/** 下的测试文件;
  • 测试 fixture(夹具);
  • package 中的测试脚本(packages/opencode/package.jsonscripts);
  • 仅当基准剖析明确指向实现层时,才允许触碰实现代码的 setup 路径。

以及五类需要重点观察的信号(Signals To Watch)

  1. 重复执行的 setup 工作;
  2. 长 sleep / 长超时等待;
  3. 串行运行的集成测试;
  4. 文件系统/数据库 fixture 的构建成本;
  5. 宽泛的 test glob 把无关工作拉进当前运行。

后续全部 34 项保留优化与 5 个 Dead End,本质上都是对这五类信号的逐个击破。

二、基准工具链:两个脚本、六个环境变量

提速研究的工具链由 packages/opencode/package.json 中注册的两个 npm script 组成:

脚本 作用
bun run bench:test 全量套件计时,输出主/次指标
bun run profile:test 逐文件剖析,定位慢文件

2.1 全量基准:bun run bench:test

packages/opencode 目录下执行:

bun run bench:test

全量基准默认只测量一次。文档特别强调“重复运行只用在拿到定向收益之后”:

BENCH_WARMUPS=1 BENCH_RUNS=3 bun run bench:test

其实现 bench-test-suite.ts 只有 52 行,逻辑与文档完全对应:

  • BENCH_WARMUPS(默认 0,必须是非负整数)、BENCH_RUNS(默认 1,必须是正整数),非法值直接退出(L4-L15);
  • 每次运行通过 Bun.spawn 拉起 bun test --timeout 30000(即与 test 脚本相同的超时参数),逐次打印 bench:test run i/N x.xxxsL17-L39);
  • 结束前计算并打印中位数、均值、最好、最坏四档数据,随后输出机器可解析的 METRIC 行(L41-L52)。

2.2 按文件剖析:bun run profile:test

定位慢文件:

bun run profile:test

探索期通过环境变量缩小范围:

TEST_PROFILE_GLOB='test/server/**/*.test.ts' bun run profile:test
TEST_PROFILE_LIMIT=20 bun run profile:test

实现 profile-test-files.ts 支持四个环境变量(L4-L9):

环境变量 默认值 说明
TEST_PROFILE_GLOB test/**/*.test.{ts,tsx} 扫描 glob,相对 packages/opencode 解析
TEST_PROFILE_LIMIT 0(不限) 截断到前 N 个文件
TEST_PROFILE_TIMEOUT 30000 传给每个 bun test--timeout
TEST_PROFILE_TOP 20 最终“最慢文件榜”显示条数

它的工作方式是串行逐个文件 spawn bun test --timeout <timeout> <file>,逐行打印 PASS/FAIL 耗时 文件,任何文件失败都会让进程以退出码 1 结束(L12-L42)。文档中说明这样做的原因:该版本的 Bun 没有 slowest-test reporter,只能直接按文件剖析(见假设循环第 1 行的 Notes)。

三、指标体系:为什么主指标是中位数

指标 含义 来源
test_suite_seconds主指标 全量套件运行的中位数 wall-clock 秒数 脚本 METRIC test_suite_seconds=
test_suite_best_seconds / test_suite_worst_seconds 多次运行中的最好/最坏 同上
failures / noisy spread 失败数与噪声波动幅度 人工判读
slowest_test_file_seconds 剖析模式下最慢单文件耗时 profile 脚本 METRIC
最慢文件列表 Top N 慢文件及其 PASS/FAIL 状态 profile 脚本打印

中位数而非均值/单次值,是因为测试耗时受冷缓存、CI 负载、机器状态影响大——文档中多次出现“median from 3/5 targeted runs because ... noisy”的表述,且“Profiling Results”一节明确记录了同一文件初始 23s 与定向 3 跑 10s 的巨大差异,证明单次全量跑的数据不可直接用于归因。

四、假设循环(Hypothesis Loop):34 项已保留的优化全记录

文档的核心资产是一张假设循环表:每条记录包含假设、改动、前后耗时、决策(keep/discard)与备注。以下按主题完整重组全部 34 项 keep 决策(数据与备注逐条对应原文,未做删减)。

4.1 方法与基线

# 假设 改动 备注
1 重复全量跑对“发现期”太昂贵 全量基准改为单跑,并新增按文件剖析器 ~250s/run pending keep:该版本 Bun 无 slowest-test reporter,改为直接剖析文件

4.2 移除不必要的 git fixture(7 项)

多个测试只断言 shell/config/session 行为,却为每个临时目录初始化 git 仓库。统一做法是把 git: true 从临时目录 fixture 中去掉,同时保留 config setup 等真实依赖:

# 假设 改动 备注
2 httpapi-listen PTY 路由测试为它并不断言的 git 仓库付费 临时目录去掉 git: true,保留 config setup 10.554s 7.818s 3 次定向跑中位数;HTTP 路由、tickets、websocket 升级、重启、无鉴权路径仍通过
3 SDK parity 助手为“只需要文件/config/session 状态”的测试创建 git 仓库 withProject 默认改为无 git;显式的 git init 测试显式选择 no-git fixture 8.011s 5.180s 5 次中位数(首跑冷/有噪声)
4 Skill 工具测试只读本地 skill 文件却初始化了 git 临时目录 fixture 去掉 git: true 2.320s 1.425s 单次定向复跑;仍覆盖 skill 发现、权限请求、bundle 文件输出
5 Prompt shell 语义测试只断言 shell/session 行为却初始化 git shell 类 prompt fixture 去掉 git: true,保留 config setup 26.930s 23.400s 改动后 3 次复跑通过:23.80s / 23.55s / 23.40s
6 其余 prompt 行为测试基本不需要仓库状态 safe loop / reference / error fixture 去掉 git;恢复 shell queue/cancel 用例 23.400s 19.610s 安全评审发现 shell runner 就绪状态在若干测试中依赖 git-backed setup;当前单次复跑通过
7 Session processor effect 测试不需要仓库状态 所有 processor-effect 临时 server fixture 去掉 git 12.500s 9.230s 2 次复跑通过:9.61s / 9.23s
8 Processor AI SDK tool-call 用例不断言 git 行为 非原生 tool-call processor 测试去掉 git: true 10.22s 9.48s 单基线;全文件 3 次中位数 9.48 / 9.60 / 9.36;聚焦用例 1.39s

4.3 缩短等待与超时(7 项)

针对“长 sleep / 生产超时被测试全额等待”这一信号,原则是:生产默认值保持不变,测试注入短超时或用既有 fixture 消除空等

# 假设 改动 备注
9 插件安装并发测试启动了比“验证锁竞争”所需更多的 worker worker 数从 12/10/8 降到 6/6/5;保留 holdMs: 30 7.800s 6.204s 3 次中位数;仍覆盖对 server、server+tui、既有 json config 的跨进程并发写
10 workspace.waitForSync 超时测试等满了生产超时 新增可选 timeout 参数,默认值仍为生产超时;超时测试用 25ms 12.949s 8.305s 3 次中位数;生产调用方保持 5000ms 默认
11 .gitignore 是同步写入的,config.test 在依赖就绪后仍在空等 去掉可写 OPENCODE_CONFIG_DIR 测试中废弃的 1000ms sleep 10.270s 9.433s 5 次中位数(一次有噪声);更简单的测试、无固定 sleep
12 Provider 插件过滤测试在等“插件依赖就绪”的 setup 用既有 fixture 助手把本地插件依赖标记为 ready 7.543s 6.366s 3 次中位数;与相邻 plugin provider 测试的 setup 方式对齐
13 HTTP provider 测试生成本地插件但没有 dependency-ready 的 fixture 状态 把生成的 .opencode 插件 fixture 标记为 dependency-ready 7.905s 2.980s 3 次中位数;避免路由测试里做无关的插件依赖 setup
14 TUI 插件生命周期超时覆盖等满了生产清理超时 新增可选 runtime dispose 超时覆写,超时测试用 25ms 7.330s 1.507s 3 次中位数;生产默认仍为 5000ms
15 文件 watcher 就绪判定可能在异步原生订阅激活前就写入 对短的就绪写入做重试,并接受 symlink-realpath 的 HEAD 事件 failed 4.62s 3 次顺序聚焦跑通过:4.62 / 4.57 / 4.64s;全量套件不再在 watcher.test.ts 失败

其中两条超时改动有明确的源码依据:

4.4 用例归并与并发化(2 项)

# 假设 改动 备注
16 HTTP listen PTY ticket 测试把同一监听拓扑重启了两遍 将目录级 ticket 回归折叠进更广的 unsafe-ticket 测试 7.051s 6.170s 2 次复跑通过:6.76s / 6.17s;仍覆盖 mint 失败与同目录升级成功
17 CLI run 子进程用例彼此独立,可并发 run-process.test.ts 子进程用例标记为 concurrent 11.87s 4.13s 最新 dev 单基线;3 次中位数 4.13 / 4.17 / 4.11s;每个用例有独立临时 home 与 LLM 端口

4.5 迁移到 it.instance:Effect 感知实例夹具(14 项)

这一组是最大的主题:把手工 tmpdir + withTestInstance 的样板代码逐步迁移到测试框架自带的 it.instance。从 test/lib/effect.ts 看,it.instance 会把测试体包进 withTmpdirInstance(options) 再挂到 live layer 上运行,options 支持 git / config / init 三个字段(L13-L23)——即临时目录创建、git 开关、配置文件注入全部由夹具统一承担,测试代码里不再手写文件写入与实例生命周期管理。文档把这个迁移定为后续 provider 切片的标准模式(第 20 项 Notes:“use as the pattern for later provider slices”)。

# 假设 改动 备注
18 首个 provider config/env/filtering 块可用 Effect 感知实例夹具 6 个 tmpdir + withTestInstance 用例迁到 it.instance 6.06s 6.07s 计时中性,但消除了手工配置文件写入与实例管道;作为后续 provider 切片的模式
19 自定义 provider/model 配置用例同样适用 再迁移 3 个配置密集 provider 用例 6.07s 6.12s 噪声内中性;在首个 provider fixture 之上继续消除手工配置写入
20 provider env 优先级与 model 查找用例同样适用 再迁移 4 个 provider 查找/默认模型用例 6.12s 6.36s 5 跑中位数有噪声;作为小切片保留,不宣称提速
21 简单 config 加载用例同样适用 JSON、shell、formatter、lsp 配置加载用例迁到 it.instance 14.18s 3.93s 前后均为 3 跑中位数;消除首个简单 config 块的手工 tmpdir + withTestInstance
22 config 模板、file include 与简单 agent 用例同样适用 JSONC、env/file 替换、非法 config、agent config 用例迁移 1.87s 1.90s 栈式叠加在首个 config 切片上;中性计时,继续消除手工管道
23 agent 选项、command 与 legacy 迁移 config 用例同样适用 agent 变体、command、autoshare、mode 迁移用例迁移 1.90s 1.83s 小幅中性偏正;手工 setup 更少
24 本地 config update 与 directories 用例同样适用 本地 updatedirectories 用例迁移 1.77s 1.71s 3 跑中位数;顺带消除了一处既有的不安全类型断言(unsafe cast)
25 .opencode agent/command 文件加载用例同样适用 单/复数 agent 与 command 的 markdown fixture 用例迁移 7.21s 1.87s 父基线有噪声(7.42 / 7.21 / 2.83);改动后稳定在 1.87 / 1.98 / 1.83;作为清理保留,不做宽泛声明
26 legacy tools 与 permission-order config 用例同样适用 legacy tools 迁移与 permission order 用例迁移 1.87s 1.87s 中性计时;继续减少 legacy config 迁移覆盖的手工临时实例管道
27 剩余简单 config 加载用例同样适用 默认 config 加载与 legacy TUI-key 用例迁移 7.78s 6.39s 改动前单基线;改动后 3 次顺序中位数 5.76 / 6.39 / 6.53;谨慎保留计时结论
28 托管设置(managed settings)config 用例同样适用 managed 覆写与 managed 文件缺失用例迁移 2.40s 1.76s 改动前单基线;改动后 1.75 / 1.76 / 1.80
29 本地插件与 subagent config fixture 同样适用 scoped npm 插件与自定义 subagent markdown 用例迁移 2.37s 1.67s 改动前单基线;改动后 1.66 / 1.67 / 1.67
30 MCP 合并 config 用例同样适用 3 个 MCP merge/override 用例迁移 1.98s 1.95s 噪声内中性;消除隔离文件系统用例的手工 tmpdir + withTestInstance
31 剩余 legacy tools config 用例同样适用 allow/deny 的 legacy tools 权限用例迁移 2.65s 1.90s 改动前单基线;改动后 2.58 / 1.90 / 1.90

4.6 夹具瘦身与 Runner 调整(3 项)

# 假设 改动 备注
32 超大规模 snapshot 批处理测试只需跨过 100 文件边界 缩小大 diff/revert fixture 体积,但保证每个用例仍越过 batch 边界 4.32s 3.66s 涉及 3 个 snapshot 测试;3 次中位数 4.32 / 3.66 / 3.66
33 不需要调用 LLM 的 prompt 测试不必启动测试 LLM server 新增 no-server runner,把明显的非 LLM prompt/shell 用例迁过去 25.41s 21.03s 整文件 3 次中位数 20.66 / 21.03 / 21.64;LLM 类测试留在原 runner
34 snapshot 初始化不需要在源仓库里提交种子文件 从 snapshot 测试 initialize() 助手里去掉多余的 git add/commit 22.22s 20.23s 最新 dev 单基线;3 次中位数 20.23 / 22.59 / 20.11;fixture 仍会创建 git 仓库根提交

五、剖析结果:慢文件榜单与全量 sanity check

5.1 发现期的初始慢文件

剖析命令形态:

TEST_PROFILE_GLOB='test/<area>/**/*.test.ts' TEST_PROFILE_TOP=15 bun run profile:test

发现期观测到的最慢文件(文档明确注明:这是历史剖析输入,不是保留改动后的当前排名):

文件 秒数 范围
test/config/config.test.ts 23.546 config
test/provider/provider.test.ts 18.747 provider
test/control-plane/workspace.test.ts 16.447 control-plane
test/plugin/install-concurrency.test.ts 14.804 plugin
test/server/httpapi-cors.test.ts 14.620 server
test/server/httpapi-listen.test.ts 10.073 server
test/server/httpapi-sdk.test.ts 8.661 server
test/server/httpapi-provider.test.ts 7.905 server
test/cli/tui/plugin-lifecycle.test.ts 7.330 cli/tui
test/file/index.test.ts 7.214 file

(上表路径均相对 packages/opencode/。)

5.2 定向 3 跑基线

对候选目标做 3 次定向基准,区分“真慢”与“混合范围噪声/顺序效应”:

文件 三次运行 中位数 备注
test/control-plane/workspace.test.ts 12.949, 12.949, 12.773 12.949 稳定的慢目标
test/server/httpapi-listen.test.ts 10.554, 10.631, 10.479 10.554 稳定慢目标;WebSocket/listener 生命周期
test/config/config.test.ts 10.270, 9.042, 10.737 10.270 大串行文件;初始 23s 是混合范围竞争/噪声
test/server/httpapi-sdk.test.ts 7.600, 8.011, 8.035 8.011 稳定慢目标
test/plugin/install-concurrency.test.ts 7.949, 7.800, 7.712 7.800 稳定慢目标;大量子进程
test/provider/provider.test.ts 8.323, 7.543, 7.474 7.543 大串行文件
test/server/httpapi-cors.test.ts 2.621, 1.682, 1.518 1.682 并非独立头部目标;初始 14s 是混合范围噪声/顺序效应

这个基线本身就是一个方法论样本:httpapi-cors.test.ts 在混合范围下高达 14.6s,定向 3 跑后中位数只有 1.68s,因此没有成为优化对象——避免了针对噪声的无效改动。

5.3 全量套件 sanity check

每积累一批定向收益后,用 bun run bench:test 做全量回归:

命令 结果 备注
bun run bench:test 225.069s 继续 prompt/session 工作前
bun run bench:test 186.729s prompt、processor、PTY 收益之后,安全评审恢复之前
bun run bench:test 202.317s 恢复 prompt shell 覆盖与 SDK VCS parity 覆盖之后
bun run bench:test failed watcher 阻塞点已清除;该次运行后续在单跑通过的 tool/skill.test.ts 与 prompt shell 超时用例上失败(全量负载下)

值得注意:第 2 → 3 行耗时从 186.7s 回升到 202.3s,是因为把安全评审判定为“依赖 git-backed setup”的用例恢复了覆盖——用耗时换回覆盖率,恰好兑现了开头“不降低覆盖”的承诺。最后一行则说明全量负载下会出现单跑不暴露的不稳定,这也是文档保留该记录的价值之一。

六、Dead Ends:5 个被放弃的尝试

与保留决策同等重要的是失败记录。原文完整保留的 Dead Ends 如下(decision 均为 discard):

假设 尝试的改动 备注
file/index.test.ts 存在不必要的逐测试全局实例清理 移除 afterEach(disposeAllInstances),保留显式 disposal 测试导入 5.262s 5.089s 改善在噪声范围内,且该清理是很多实例状态测试的安全护栏
Socket reset 重试测试可以缩短其空闲超时路径 调小 Bun server 空闲超时并尝试强制关闭 server 16.46s failed 更短的空闲超时改变了错误形态;强制关闭直接挂起。保留真实的 socket reset
tool/webfetch 可以避免逐测试实例 setup 本地 HTTP 测试从 it.instance 切到 it.live 1.219s failed 工具执行读取实例本地的 agent 状态,临时实例是必需的
LSP client 互操作测试可以缩短粗粒度的请求处理 sleep 固定通知后等待从 100ms 降到 10ms 4.270s 4.740s 首跑曾降到 3.870s,但验证轮比基线更慢;不构成明确收益
Config 内容 env 用例可用 Effect 感知实例夹具 两个 OPENCODE_CONFIG_CONTENT token 替换用例迁到 it.instance 1.95s 2.06s 通过但定向复跑不优于基线;保留既有显式 env 清理

disposeAllInstances 对应 test/fixture/fixture.ts 中暴露的 disposeAllInstances / disposeAllInstancesEffect。)

七、方法论提炼

把整篇研究文档抽象出来,这套提速方法有五个可复用的要点:

  1. 两级工具,分工明确:全量基准(中位数,单次或多次测量)只用于阶段性 sanity check;发现期用按文件剖析器逐文件计时。脚本层面二者刻意解耦,见 bench-test-suite.tsprofile-test-files.ts 文件头注释。
  2. 中位数 + 多次定向跑对抗噪声:每条 keep 决策都注明基线是“单次”还是“3/5 次中位数”,噪声大的显式声明“不宣称提速”(如 it.instance 迁移的多个中性切片)。
  3. 先归因再动手httpapi-cors 的案例说明,混合范围下的高耗时可能只是顺序效应,定向 3 跑基线是进入假设循环前的必要闸门。
  4. 优化只削“多余成本”,不削覆盖:移除未断言的 git 初始化、缩短测试专属超时(生产默认 5000ms 不动,见 workspace.ts)、把独立用例并发化(每个用例独立临时 home 与端口)。
  5. 失败也入库:Dead Ends 表与“恢复覆盖后全量耗时回升”“全量负载下偶发失败”的记录,保证了后续研究者不会重复踩坑,也让耗时曲线的每一次回升都有解释。

如果你要在自己的仓库复用这套方法,最小可行配置就是:一个“单跑 + METRIC 行”的全量计时脚本、一个“glob 扫描 + 逐文件 spawn + Top N 榜单”的剖析脚本,以及一张强制填写 before/after/decision/notes 四列的假设循环表——opencode 的这份 perf/test-suite.md 就是这三者协作产出的完整范本。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390