opencode 启动耗时验证:注入 [TRACE] 日志实测完整链路
七月 22, 2026 [debug, source-analysis] #opencode #npm #plugin #effect #arborist #docker #logging #performance背景
前两篇文章分别从排查和源码分析两个角度,定位了 opencode 设置 OPENCODE_CONFIG_DIR 后启动变慢的根因。
- 排查篇:通过
strace//proc/fd发现 plugin 系统在后台下载@opencode-ai/plugin的完整依赖树(63MB/27 个包) - 分析篇:理清了
ConfigPaths.directories()→npmSvc.install()→waitForDependencies()→Fiber.join的调用链
但这些分析是推理出来的——代码里每一步怎么写我们知道,但实际跑起来每一步要花多少时间?本文在源码中注入调试日志,编译正式版二进制,docker 模拟容器冷启动,把推理变成实测数据。
日志注入
在三个关键文件中,用统一的 [TRACE] 前缀添加 Effect.logDebug() 调用。只加日志,不改逻辑。
packages/core/src/npm.ts — Npm.install 三步逻辑:
① node_modules 不存在 → reify() 〔直接下载〕
② 存在 → 读 package.json + package-lock.json
③ 脏检:声明的包 vs 锁定的包 → 缺了就 reify,都满足就跳过
const install = Effect.fn("Npm.install")(function* (dir, input) {
yield* Effect.logDebug(`[TRACE] Npm.install dir=${dir} add=[${addPkgs}]`)
// ...
yield* Effect.logDebug(`[TRACE] Npm.install reify dir=${dir} reason=no-node_modules`)
// ...
yield* Effect.logDebug(`[TRACE] Npm.install dirty-check dir=${dir} declared=[...] locked=[...]`)
yield* Effect.logDebug(`[TRACE] Npm.install done dir=${dir} reason=deps-satisfied`)
})
const reify = (input) => {
let tReify = 0 // ← 必须在 generator 外部声明
return Effect.gen(function* () {
tReify = Date.now()
yield* Effect.logDebug(`[TRACE] reify start dir=${input.dir} add=[...]`)
// ... arborist.reify() ...
}).pipe(
Effect.tap(() => Effect.logDebug(`[TRACE] reify done dir=${input.dir} time=${Date.now() - tReify}ms`))
)
}
let tReify必须在Effect.gen闭包外面声明,否则.pipe(Effect.tap(...))中的箭头函数无法通过闭包访问到它——第一次测试吃了这个亏,ReferenceError: tReify is not defined。
packages/opencode/src/config/config.ts — 目录遍历和 ConfigPlugin.load:
yield* Effect.logDebug(`[TRACE] directories count=${directories.length} items=[...]`)
yield* Effect.logDebug(`[TRACE] npmSvc.install start dir=${dir}`)
yield* Effect.logDebug(`[TRACE] ConfigPlugin.load dir=${dir} found=${list?.length ?? 0}`)
yield* Effect.logDebug(`[TRACE] dir done dir=${dir} time=${Date.now() - tDir}ms`)
packages/opencode/src/plugin/index.ts — 阻塞点:
if (plugins.length) {
yield* Effect.logDebug(`[TRACE] waitForDeps start plugins=${plugins.length}`)
yield* config.waitForDependencies()
yield* Effect.logDebug(`[TRACE] waitForDeps done time=${Date.now() - tWait}ms`)
}
测试环境
# 编译正式版(npm registry 有对应的 @opencode-ai/plugin@1.17.12)
OPENCODE_VERSION=1.17.12 OPENCODE_CHANNEL=latest \
bun run --cwd packages/opencode build --single
# docker 模拟容器冷启动,每次 /root 都是空的
docker run --rm \
-v /tmp/agent-config/:/agent-config/ \
-v /tmp/agent-log/:/root/.local/share/opencode/log/ \
debug-image \
env OPENCODE_CONFIG_DIR="/agent-config" "/agent-config/opencode" "--log-level=DEBUG"
容器内的情况:
/root/.config/opencode/——空目录,无package.json,无node_modules/agent-config/——有package.json+package-lock.json+node_modules+plugins/wfuzz-agent.ts
通过 grep '\[TRACE\]' /root/.local/share/opencode/log/opencode.log 过滤。
dev 版本实测(0.0.0-dev-*)
npm registry 上不存在 @opencode-ai/plugin@0.0.0-dev-*,下载必然失败。
02:23:18.532 [TRACE] directories count=2 items=[/root/.config/opencode | /agent-config]
02:23:18.533 [TRACE] npmSvc.install start dir=/root/.config/opencode
02:23:18.535 [TRACE] Npm.install dir=/root/.config/opencode add=[@opencode-ai/plugin@0.0.0-dev-xxx]
02:23:18.537 [TRACE] Npm.install reify reason=no-node_modules ← ① node_modules 不存在
02:23:18.559 [TRACE] dir done dir=/root/.config/opencode time=105ms ← 本地操作返回
02:23:18.576 [TRACE] npmSvc.install start dir=/agent-config
02:23:18.578 [TRACE] Npm.install dirty-check ... matched → done ← ②③ 脏检通过
02:23:18.583 [TRACE] ConfigPlugin.load found=1 ← 发现 wfuzz-agent.ts
02:23:18.586 [TRACE] waitForDeps start plugins=1 ← 开始等!
02:23:18.653 [TRACE] reify start dir=/root/.config/opencode add=[...] ← 后台 fiber 开始跑
── 3.2 秒(npm 上找不到版本,超时退出)──
02:23:21.874 [TRACE] waitForDeps done time=3217ms ← Fiber.join 返回
Fiber.join 等的是 npm 超时,不是下载。但阻塞已经发生——3.2 秒的空白窗口,TUI 不渲染。
正式版实测(1.17.12)
这个版本 npm registry 上有,会真正走完下载。
02:58:28.454 [TRACE] directories count=2 items=[/root/.config/opencode | /agent-config]
02:58:28.454 [TRACE] npmSvc.install start dir=/root/.config/opencode
02:58:28.457 [TRACE] Npm.install dir=/root/.config/opencode add=[@opencode-ai/plugin@1.17.12]
02:58:28.459 [TRACE] Npm.install reify reason=no-node_modules ← ① 触发完整下载
02:58:28.559 [TRACE] dir done dir=/root/.config/opencode time=105ms ← 本地操作返回(lforkDetach)
02:58:28.576 [TRACE] npmSvc.install start dir=/agent-config
02:58:28.578 [TRACE] Npm.install dirty-check declared=[@opencode-ai/plugin, @types/node, typescript]
02:58:28.578 [TRACE] locked=[@opencode-ai/plugin, @types/node, typescript]
02:58:28.578 [TRACE] Npm.install done dir=/agent-config reason=deps-satisfied
02:58:28.580 [TRACE] reify start dir=/root/.config/opencode add=[@opencode-ai/plugin@1.17.12]
02:58:28.583 [TRACE] ConfigPlugin.load found=1
02:58:28.586 [TRACE] waitForDeps start plugins=1 ← 开始等!
── 18.5 秒(arborist.reify 下载 27 个包)──
02:58:47.142 [TRACE] reify done time=18562ms ← 下载完成
02:58:47.150 [TRACE] waitForDeps done time=18564ms ← 解除阻塞
完整下载 18.6 秒。从 waitForDeps start 到 done,TUI 窗口一片空白。
完整链路
ConfigPaths.directories()
│
├─ /root/.config/opencode/ ───── 空目录,无 node_modules
│ npmSvc.install() → Npm.install: ① node_modules 不存在 → reify()
│ ConfigPlugin.load() → found=0
│
├─ /agent-config/ ────────────── 有完整 node_modules + lock
│ npmSvc.install() → Npm.install: ② 存在 → ③ 脏检 matched → 跳过
│ ConfigPlugin.load() → found=1
│
▼
plugin_origins.length = 1
│
▼
waitForDependencies()
→ Effect.forEach(s.deps, Fiber.join) ← 等所有 fiber
→ /root/.config/opencode 的 reify fiber 还在跑...
→ arborist.reify(): 下载 @opencode-ai/plugin@1.17.12 + 26 个依赖
→ 18.6 秒后完成
→ Fiber.join 全部返回
每一步对应源码位置和耗时:
| 步骤 | 文件:行 | 耗时 |
|---|---|---|
| 目录发现 | paths.ts:23 | <1ms |
| Npm.install 可写检查 | npm.ts:141-144 | <1ms |
| node_modules 存在检查 | npm.ts:149-150 | 1ms |
| reify(下载依赖) | npm.ts:80-113 | 18,562ms |
| 脏检(比对 lock) | npm.ts:161-187 | 2ms |
| ConfigPlugin.load | plugin.ts:18-30 | 1ms |
| waitForDependencies | config.ts:642-646 | 18,564ms |
对比:两种优化策略的效果
我们把之前的实验数据也放进来对比:
| 场景 | 对 /root/.config/opencode | waitForDeps | TUI 出现时间 |
|---|---|---|---|
| 原始(无优化) | reify 下载 18.6s | 阻塞 18.6s | ~19s |
| guard 跳过 | 不调用 npmSvc.install | 0ms | ~1s |
| 预置 node_modules | 脏检 matched → skip | 0ms | ~1s |
预置 node_modules 是工程侧最优解——不碰源码,只需在容器构建时将 package.json + package-lock.json + node_modules 复制到 directories() 返回的每个目录。
题外:插件目录 vs 文件
在验证过程中还有一个发现:open code 的插件自动发现只支持单文件,不支持目录。
ConfigPlugin.load() 的 glob 是 {plugin,plugins}/*.{ts,js},不匹配子目录。但加载层 resolvePathPluginTarget() 已经支持目录(找 index.ts 或 package.json)。如果有目录式插件的需求,需要在 opencode.jsonc 中显式配置 "plugin": ["./plugins/my-plugin"]。
结论
实测数据完整验证了源码分析的每一个推断:
directories()返回的每个目录都无条件调用npmSvc.install()- 空目录无
node_modules→Npm.install第 ① 步直接 reify(后续的脏检根本没机会跑) - 只要有插件被
ConfigPlugin.load发现 →waitForDependencies阻塞 - 容器冷启动每次都要付这笔 18 秒的下载开销
grep '\[TRACE\]' ~/.local/share/opencode/log/opencode.log 一行命令就能看到完整链路和每步耗时。