跑多 agent 的时候遇到一个很烦的问题:Mac 烫到 70℃,风扇全速,但打开活动监视器根本看不出是谁干的。

十几行一模一样的 node,CPU 百分比还在跳,一行行对下去二十分钟没了。而且很多时候元凶是某个跑了两小时的 agent ,你也不想直接 kill ——中断了上下文就丢。
所以写了个菜单栏 App ,叫 Manatee,就做两件事:找出谁在烫,把它挪到能效核。
怎么找出「谁」
难点是同一个 agent 会 fork 出一堆同名进程,按可执行名分组没有意义。所以按 rootPid 聚合成会话,一个会话一行,CPU 百分比是整棵进程树求和。
另一个坑是判定。编译的时候瞬时 800% 是正常的,不能算异常。所以记录每个会话最近 6 分钟的采样,连续 5 分钟超过 200%(两个核)才标记出来。实测这个窗口能把编译抖动和「真的卡住了」区分开。
相关代码在 SessionHistory.swift:
/// 记录每个会话最近 6 分钟的 CPU 采样,用来判断「连续满载」。
/// 只看瞬时值会把编译时的短暂 800% 当成异常,所以必须带时间窗。
private let keep: TimeInterval = 360 // 保留窗口
let sustain: TimeInterval // 连续多久算异常,默认 300s
let threshold: Double // CPU% 阈值,默认 200 (= 两个核)
怎么降温
就这两行:
taskpolicy -b -p <pid> # 降到 Background QoS → 系统调度到能效核
taskpolicy -B -p <pid> # 恢复
降到 background 之后进程还在跑,只是被调度到能效核,速度慢下来、发热下来,但进程不冻结、不中断、上下文不丢。
实测同一台机器,70.6℃ → 44.9℃,降 25.7℃。元凶是连着跑了 12 分钟的 Claude Code ,340%。
几个实现取舍
- 进程表用 libproc 直扫(
proc_listallpids+proc_pidinfo),不 forkps。fork 一次几十毫秒,扫全表太慢。 - 温度走
IOHIDEventSystemClient私有符号,免 sudo 免 entitlement 。代价是不能上 Mac App Store,而且 macOS 大版本升级可能改传感器命名,需要容错。 - 只处理自己的进程。别人的进程没权限降 QoS ,界面上会标出来但不动。
- 不杀进程是刻意设计。AI 干活时被 kill ,前面跑的全白费,降速继续跑通常更划算。
目前的状态
macOS 14+,Apple Silicon ,菜单栏常驻。源代码公开:https://github.com/luogao/manatee
想听听反馈,特别是:多 agent 并发的场景下,「连续 5 分钟 200%」这个判定阈值合不合适?我担心对短任务太钝,但又不想把编译误判进去。