Skip to content

perf(runtime): sandbox pump loop idle-spins ~200k setImmediate wake-ups/sec while awaiting a host call #3233

Description

@os-zhuang

背景

在排查 #1867 时顺带发现的一处 CPU 效率问题(非 bug,与 #1867 无关,按 AGENTS.md Prime Directive #10 单独立项)。

QuickJSScriptRunner.execute(packages/runtime/src/sandbox/quickjs-runner.ts)驱动脚本异步续体的 pump 循环大致是:

for (;;) {
  await new Promise((resolve) => setImmediate(resolve)); // 让出事件循环
  const pending = runtime.executePendingJobs();          // 排空 VM 任务
  // 读取 __error / __result,检查 deadline
}

当脚本在 await 一个尚未 settle 的宿主调用(例如一次较慢的 DB 查询、或一次嵌套写入链)时,这一轮 executePendingJobs() 无事可做、__result 仍为 undefined,循环立即进入下一轮 —— 于是在整段等待期间以 setImmediate 的节奏空转。

实测

用一个「永不 settle」的宿主调用触发 250ms 超时,错误信息里带出了迭代计数:

hook 'slow' exceeded timeout of 250ms (after 50864 pump iterations)

即 250ms 内约 5 万次空转(≈ 20 万次/秒)。一个 30s 预算的 action 会累计约 600 万次唤醒。setImmediate 每轮都会让出事件循环,所以不是硬 busy-spin、不会阻塞其它任务,但这些唤醒纯属浪费(每次 setImmediate 调度 + executePendingJobs 的固定开销)。

建议

让宿主侧在某个 deferred 真正 settle 时通知 pump 循环(共享一个「有新结果」的信号,例如一个每次宿主 leaf settle 时 resolve 的 Promise),循环只在确有工作时才 executePendingJobs 并检查结果;deadline 用一个独立的定时器兜底,而不是靠每轮轮询。退一步的最小改动:对连续空轮引入极小的自适应退避。

需要保留的现有语义:

影响

@objectstack/runtime(hook/action 沙箱执行)。纯 CPU 效率,优先级低(P3)。

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions