Skip to content

fix(runtime): 回收分离子进程,并按在线状态/退避收敛常驻轮询 - #801

Merged
coder-hhx merged 1 commit into
mainfrom
fix/perf-backend-resources
Sep 11, 2026
Merged

fix(runtime): 回收分离子进程,并按在线状态/退避收敛常驻轮询#801
coder-hhx merged 1 commit into
mainfrom
fix/perf-backend-resources

Conversation

@coder-hhx

Copy link
Copy Markdown
Collaborator

Linked issue

Closes #798

Summary

现场(v1.3.3,无任何交互):主进程 ps 衰减均值 65.6%top 稳态 13.3~41%
43 个线程;并且每个工具会话都在父进程下留一个 <defunct>(其中一个明确是 Browser 工具
拉起的 Chrome 实例)。两件事其实是一个主题:子进程与常驻轮询都没有收敛

僵尸

std::process::Child 不实现 drop-reap:spawn() 之后把句柄丢掉,子进程退出时没人 wait()
就会一直挂在父进程下。本轮找到 4 处这种"启动器"写法:

  • commands/workspace/fs.rs:macOS open / Windows explorer.exe / Linux xdg-open 三个分支。
  • commands/workspace/git.rsspawn_system_file_manager(git 面板"在文件管理器中显示")。

浏览器路径机制不同:services/browser/launcher.rs 持有 Child 以便 kill,但子进程存活期间
(整个会话)没有任何 tick 会 try_wait 它——用户手关窗口或它崩掉后,defunct 一直挂到会话结束。

做法:新增 runtime::process::spawn_and_reap / spawn_child_reaper("不等它、但要收尸",
分离线程里 wait()),替换上面 4 处;浏览器改为把 Child 交给收尸线程,结构体只保留
pid + 启动时间,Drop 走新增的 terminate_process_tree_by_pid_if_same——先比对
ps -o etime 反推的启动时间(±2s 容差)再发信号。收尸之后 pid 可能被内核复用,直接按 pid
KILL 会误伤无辜进程;这个守卫也顺带修掉了旧 terminate_process_tree_by_pid 的同一隐患
(探测失败 Unknown 时仍沿用"保守发信号"的旧语义,不改变既有取舍)。

已经正确、没有改动的地方(避免重复"修"):shell_runnerwait_timeout 收尸)、
managed_process 的 live 条目(try_wait)、MCP stdio、cua_driverpower_activity
caffeinatestop()kill + wait)、git clone/subagent_worktreewait_with_output)。

空闲 CPU

按贡献排序核对后,改了其中 4 项(都是"无条件常驻"的确定性浪费):

位置 原状 现状
runtime/managed_process.rs restored 条目每 2s fork/exec 一次 ps -p <pid> -o etime= 15s
services/gateway/controller.rs 租约清扫 5s 循环,未配置网关也照跑 status().online 时工作
services/gateway/controller.rs status 重发布 5s 循环,离线时每次仍重建 + protobuf 编码整份信封 同上
commands/integration/mcp.rs SSE 重连 固定 1s 重连(DNS+TCP+TLS 握手) 1s→30s 指数退避,连上即复位

未覆盖、需要单独处理的最大单项:每个 workdir 一个递归 FSEvents 监听 + 聚合线程
(外加 git 元数据目录的额外递归监听),不随窗口可见性收敛
services/workspace_watch/watcher.rs)。构建产物/编辑器自动保存这类高频写入会持续产生
事件。之所以没在本 PR 里做:需要把可见性信号接到 WorkspaceWatchService::set_desired 且不能
影响 gateway 来源的监听集合,属于必须在真机上验证行为的功能改动,收益大但风险不匹配,
已在 #798 里单列。前端网关模式的 1s idle poll 与网关重连退避上限(5s→30~60s)同属行为权衡,
也没动。

Change scope

  • Modules: agent-gui / src-tauri(runtime、commands/workspace、commands/integration、services/browser、services/gateway)
  • Key paths:
    • crates/agent-gui/src-tauri/src/runtime/process.rs(新增 reaper 与 pid 守卫,+1 回归测试)
    • crates/agent-gui/src-tauri/src/runtime/managed_process.rs
    • crates/agent-gui/src-tauri/src/services/browser/launcher.rs
    • crates/agent-gui/src-tauri/src/services/gateway/controller.rs
    • crates/agent-gui/src-tauri/src/commands/workspace/{fs.rs,git.rs}
    • crates/agent-gui/src-tauri/src/commands/integration/mcp.rs

Screenshots / preview

非 UI 改动,下面是行为/进程层面的前后对照。

僵尸(新增回归测试的观察方式:ps -p <pid> -o stat=

修复前(spawn() 后丢弃 Child):子进程退出后 stat 一直以 Z 开头,直到父进程退出
修复后:                       stat 很快变为不存在(已回收)→ 测试通过
                              旧实现下该循环会一直读到 "Z" 并 panic

空闲轮询

修复前:restored 托管进程存在时,每 2s 起一个短命 `ps` 子进程(24h 约 43k 次 fork/exec)
修复后:15s 一次(约 5.7k 次)

修复前:未配置/未连上网关时,两条 5s 循环仍在锁 inbox、扫账本、编码并"发送"整份 runtime-status
修复后:离线直接跳过该 tick(发送本来就是 no-op)

Verification

  • cargo check --manifest-path crates/agent-gui/src-tauri/Cargo.toml --lib → 通过,无 warning。
  • cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml --lib runtime::process
    3/3 通过,含新增 detached_child_is_reaped_instead_of_left_defunct
  • cargo test --manifest-path crates/agent-gui/src-tauri/Cargo.toml --lib -- chat_history ssh_local_forward shell_runner integration_commands::mcp(CI 的同一条过滤器)→ 126/126 通过
  • 新增测试的观察口径是 ps -o stat=:进程消失或非 Z 即通过;旧实现(丢弃 Child)下
    会一直读到 Z,所以这条测试在修复前必然失败。
  • 注意:CI 的 Rust 步骤只跑上面那组过滤器(chat_history / ssh_local_forward /
    shell_runner / integration_commands::mcp),不覆盖 runtime::process 的新用例
    它需要 cargo test --lib runtime::process(已在本地跑通)。CI 的过滤器在 workflow 文件里,
    而本机凭据缺 workflow scope 无法推送 workflow 改动,故未顺手扩过滤器。

Pre-submit checklist

现场:主进程空闲时 `ps` 衰减均值 65.6%、`top` 稳态 13.3~41%,且每个工具会话在
父进程下留一个 <defunct>(其中一个明确是 Browser 工具拉起的 Chrome)。
`std::process::Child` 不实现 drop-reap——spawn 后丢弃句柄就没人 wait()。

- 新增 `runtime::process::spawn_and_reap` / `spawn_child_reaper`:启动器"不等它、
  但要收尸",在分离线程里 wait() 掉子进程。替换 4 处会漏僵尸的写法:
  `commands/workspace/fs.rs` 的 open / explorer.exe / xdg-open 三个分支,以及
  `commands/workspace/git.rs` 的 `spawn_system_file_manager`(文件管理器中显示)。
- 浏览器子进程(`services/browser/launcher.rs`):Child 句柄交给收尸线程,结构体只
  保留 pid + 启动时间;Drop 改走 `terminate_process_tree_by_pid_if_same`——先比对
  `ps -o etime` 反推的启动时间(±2s 容差)再发信号,避免收尸后 pid 被复用误杀。
- `runtime/managed_process.rs`:restored 条目的存活探测从每 2s 一次 fork/exec `ps`
  放宽到 15s(遗留进程的存活粒度不需要秒级)。
- `services/gateway/controller.rs`:租约清扫与 runtime-status 重发布两条 5s 循环改为
  仅在 `status().online` 时工作(此前未配置/未连上网关也照跑,后者每次都要重建并
  protobuf 编码整份信封)。
- `commands/integration/mcp.rs`:SSE 重连从固定 1s 改为 1s→30s 指数退避,连上即复位。
- 新增回归测试:分离子进程不得停留在 defunct(`ps -o stat=` 观察,旧写法必失败)。
@coder-hhx
coder-hhx merged commit 7dc360b into main Sep 11, 2026
9 checks passed
@coder-hhx
coder-hhx deleted the fix/perf-backend-resources branch September 11, 2026 17:43
coder-hhx added a commit that referenced this pull request Sep 11, 2026
v1.3.5 发布时 Windows x64 job 在 `Build Windows bundles` 失败:

    error[E0425]: cannot find value `PID_START_TIME_TOLERANCE_MS` in this scope
       --> crates\agent-gui\src-tauri\src\runtime\process.rs:246:56

#801 把这个常量定义为 `#[cfg(unix)]`(它的注释只讲了 `ps -o etime=` 的秒级精度),
但使用它的 `terminate_process_tree_by_pid_if_same` 是跨平台的:macOS / Linux 编译
都看不到问题,只有 Windows 目标才落到这条分支。

修复:常量改为所有平台可见,并补齐注释说明为何不能在 unix 下单独定义;windows 侧
`GetProcessTimes` 给的是精确创建时间,同一份 ±2s 宽容度只是稍宽,不会漏判 pid 复用。

验证:
- 把 `process.rs` 单独按 `--target x86_64-pc-windows-msvc` 做类型检查:修复前精确
  复现同一处 E0425,修复后干净通过(本机缺 Windows SDK,整 crate 检查会死在
  aws-lc-sys 的 C 构建)。
- `cargo check --lib`(host)无 warning;`cargo test --lib runtime::process` 3/3。
- 对 #801 全部改动做了 cfg 门控审计:新增引用到的 `spawn_and_reap` /
  `spawn_child_reaper` / `terminate_process_tree_by_pid_if_same` /
  `process_start_time_ms` 均无门控,此处是唯一的跨平台遗漏点。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Performance] 主进程空闲持续占用 13~66% CPU,且每个工具会话泄漏一个僵尸子进程

1 participant