opencode 启动耗时验证:注入 [TRACE] 日志实测完整链路

七月 22, 2026 [debug, source-analysis] #opencode #npm #plugin #effect #arborist #docker #logging #performance

背景

前两篇文章分别从排查和源码分析两个角度,定位了 opencode 设置 OPENCODE_CONFIG_DIR 后启动变慢的根因。

但这些分析是推理出来的——代码里每一步怎么写我们知道,但实际跑起来每一步要花多少时间?本文在源码中注入调试日志,编译正式版二进制,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"

容器内的情况:

通过 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 startdone,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-1501ms
reify(下载依赖)npm.ts:80-11318,562ms
脏检(比对 lock)npm.ts:161-1872ms
ConfigPlugin.loadplugin.ts:18-301ms
waitForDependenciesconfig.ts:642-64618,564ms

对比:两种优化策略的效果

我们把之前的实验数据也放进来对比:

场景对 /root/.config/opencodewaitForDepsTUI 出现时间
原始(无优化)reify 下载 18.6s阻塞 18.6s~19s
guard 跳过不调用 npmSvc.install0ms~1s
预置 node_modules脏检 matched → skip0ms~1s

预置 node_modules 是工程侧最优解——不碰源码,只需在容器构建时将 package.json + package-lock.json + node_modules 复制到 directories() 返回的每个目录。

题外:插件目录 vs 文件

在验证过程中还有一个发现:open code 的插件自动发现只支持单文件,不支持目录。

ConfigPlugin.load() 的 glob 是 {plugin,plugins}/*.{ts,js},不匹配子目录。但加载层 resolvePathPluginTarget() 已经支持目录(找 index.tspackage.json)。如果有目录式插件的需求,需要在 opencode.jsonc 中显式配置 "plugin": ["./plugins/my-plugin"]

结论

实测数据完整验证了源码分析的每一个推断:

  1. directories() 返回的每个目录都无条件调用 npmSvc.install()
  2. 空目录无 node_modulesNpm.install 第 ① 步直接 reify(后续的脏检根本没机会跑)
  3. 只要有插件被 ConfigPlugin.load 发现 → waitForDependencies 阻塞
  4. 容器冷启动每次都要付这笔 18 秒的下载开销

grep '\[TRACE\]' ~/.local/share/opencode/log/opencode.log 一行命令就能看到完整链路和每步耗时。