概述
通知音效的 AudioContext 只创建、从不释放。一旦用户开启音效并在窗口里点过一下,扬声器输出流就被占住直到 codeg 退出:coreaudiod 常驻 6% 左右 CPU,并且全程持有 PreventUserIdleSystemSleep —— 系统无法空闲睡眠、音频硬件也无法进入低功耗。对笔记本续航是持续性的损耗(不播放任何声音的时候也在损耗)。
环境
- codeg 0.32.2(最新 release)
- macOS,MacBook Air M4(Mac16,13),16GB
- 电池 Cycle Count 72 / 最大容量 100% / Condition Normal —— 已排除电池老化,纯粹是软件负荷
现象与实测
- codeg 启动后,
coreaudiod 稳定在 6.1% ~ 6.7% CPU,与当前负载无关(连续 20 秒采样,数值几乎不动):
573 6.7 /usr/sbin/coreaudiod
573 6.6 /usr/sbin/coreaudiod
573 6.3 /usr/sbin/coreaudiod
573 6.1 /usr/sbin/coreaudiod
pmset -g assertions 里有一条由 coreaudiod 持有的扬声器断言,Created for PID 指向 codeg 的 WebKit GPU 进程,持续时间正好等于 codeg 本次启动至今(实测 6 分 25 秒,与进程 etime 一致):
pid 573(coreaudiod): [...] PreventUserIdleSystemSleep named: "com.apple.audio.BuiltInSpeakerDevice.context.preventuseridlesleep"
Created for PID: 8254.
Resources: audio-out BuiltInSpeakerDevice
8254 是 /System/Library/.../com.apple.WebKit.GPU.xpc,与 codeg 主进程同时启动(属于 codeg 的 webview)。
-
采样该 WebContent 进程,线程列表里存在 RemoteAudioDestinationProxy render thread —— webview 侧确实有一条活跃的音频输出流在渲染。
-
关闭 codeg 后,上述断言立刻消失、coreaudiod 回落到 1% 以下。开启音效 + 点一下窗口 → 重启 codeg → 立刻复现。
复现步骤
- Settings → General 打开通知音效(
settings:notification-sound:v1 中 enabled: true)
- 在 codeg 窗口里做任意一次点击/按键(触发手势解锁)
- 之后一声提示音都不播,
coreaudiod 也会一直占着扬声器,并持有上述 anti-sleep 断言
(默认值 enabled: false 是安全的,所以只有主动开启音效的用户会踩到。)
根因
src/lib/notification-sound.ts:
- 模块级单例:
let context: AudioContext | null = null
getAudioContext() 创建后只挂 watchOutputState(context)(statechange 监听)
primeNotificationSoundOutput() → armUnlockOnGesture() 会在用户第一次 pointerdown / keydown / touchstart 时创建 context 并 resume() 到 running
整个文件没有任何 ctx.suspend() 或 ctx.close(),因此 context 一旦进入 running 就保持到窗口销毁。
需要说明:文件里关于"playEventSound 不能等待 resume()"的那段设计(suspended 时 currentTime 冻结,排队的声音会在用户下次点击时迟到触发)是正确的,这里要修的不是那部分,而只是缺少释放路径。
建议修复(已实现并通过测试,按需可提 PR)
在最后一个音符播完后宽限一段时间再 suspend(),把设备还回去:
retireNote() 中当 scheduledNotes 清空时,arm 一个 IDLE_SUSPEND_MS 定时器
scheduleTone() 进入时清掉它(有新的提示音要播,就别释放)
- 定时器到点,且
ctx.state === "running" 且没有仍在播的音符 → ctx.suspend()
- 不需要额外写重新解锁:现有的
watchOutputState() 在 statechange 时会调用 armUnlockOnGesture(),所以下次点击就会重新打开输出
测试:新增 5 个用例,全套 24/24 通过;并做了变异验证(撤掉源码改动后,其中 2 个关键用例立刻失败),确认测试确实覆盖这个 bug。eslint 与 tsc --noEmit 均干净。
取舍(需要你决定):宽限期内到达的提示音会取消释放;若超出宽限期且期间没有手势重新解锁,那一声会被丢掉。丢提示音本身是现有代码已定义并接受的路径(playEventSound 返回 false 且不消耗冷却,所以下一声照响)。我先取了 5 分钟 —— 用电脑时点击/打字会立刻重新打开输出,只有"纯盯着看超过 5 分钟"才会漏。
另一个可选方向是:下一次事件到达时先尝试一次无手势的 resume(),成功就直接补播,失败再回落到手势解锁。这可能让"走开等长任务完成"的核心场景不再漏声,但它和测试中"没播出去的提示音不消耗冷却"这一不变量会冲突,需要一起重新设计,所以我没有把它放进上面的最小改动里。
备注
另外观察到一个独立问题(还没定位完,先不混在这个 issue 里,定位到后另开):在「回合进行中但 agent 没有输出文字」的状态下,WebContent 主线程持续执行 rAF 回调并触发 MutationObserver,受控实测 30 秒消耗 5.39s CPU(约 18% 一个核心)。
概述
通知音效的
AudioContext只创建、从不释放。一旦用户开启音效并在窗口里点过一下,扬声器输出流就被占住直到 codeg 退出:coreaudiod常驻 6% 左右 CPU,并且全程持有PreventUserIdleSystemSleep—— 系统无法空闲睡眠、音频硬件也无法进入低功耗。对笔记本续航是持续性的损耗(不播放任何声音的时候也在损耗)。环境
现象与实测
coreaudiod稳定在 6.1% ~ 6.7% CPU,与当前负载无关(连续 20 秒采样,数值几乎不动):pmset -g assertions里有一条由coreaudiod持有的扬声器断言,Created for PID指向 codeg 的 WebKit GPU 进程,持续时间正好等于 codeg 本次启动至今(实测 6 分 25 秒,与进程etime一致):8254是/System/Library/.../com.apple.WebKit.GPU.xpc,与 codeg 主进程同时启动(属于 codeg 的 webview)。采样该 WebContent 进程,线程列表里存在
RemoteAudioDestinationProxy render thread—— webview 侧确实有一条活跃的音频输出流在渲染。关闭 codeg 后,上述断言立刻消失、
coreaudiod回落到 1% 以下。开启音效 + 点一下窗口 → 重启 codeg → 立刻复现。复现步骤
settings:notification-sound:v1中enabled: true)coreaudiod也会一直占着扬声器,并持有上述 anti-sleep 断言(默认值
enabled: false是安全的,所以只有主动开启音效的用户会踩到。)根因
src/lib/notification-sound.ts:let context: AudioContext | null = nullgetAudioContext()创建后只挂watchOutputState(context)(statechange监听)primeNotificationSoundOutput()→armUnlockOnGesture()会在用户第一次pointerdown/keydown/touchstart时创建 context 并resume()到running整个文件没有任何
ctx.suspend()或ctx.close(),因此 context 一旦进入running就保持到窗口销毁。需要说明:文件里关于"
playEventSound不能等待resume()"的那段设计(suspended 时currentTime冻结,排队的声音会在用户下次点击时迟到触发)是正确的,这里要修的不是那部分,而只是缺少释放路径。建议修复(已实现并通过测试,按需可提 PR)
在最后一个音符播完后宽限一段时间再
suspend(),把设备还回去:retireNote()中当scheduledNotes清空时,arm 一个IDLE_SUSPEND_MS定时器scheduleTone()进入时清掉它(有新的提示音要播,就别释放)ctx.state === "running"且没有仍在播的音符 →ctx.suspend()watchOutputState()在statechange时会调用armUnlockOnGesture(),所以下次点击就会重新打开输出测试:新增 5 个用例,全套 24/24 通过;并做了变异验证(撤掉源码改动后,其中 2 个关键用例立刻失败),确认测试确实覆盖这个 bug。
eslint与tsc --noEmit均干净。取舍(需要你决定):宽限期内到达的提示音会取消释放;若超出宽限期且期间没有手势重新解锁,那一声会被丢掉。丢提示音本身是现有代码已定义并接受的路径(
playEventSound返回false且不消耗冷却,所以下一声照响)。我先取了 5 分钟 —— 用电脑时点击/打字会立刻重新打开输出,只有"纯盯着看超过 5 分钟"才会漏。另一个可选方向是:下一次事件到达时先尝试一次无手势的
resume(),成功就直接补播,失败再回落到手势解锁。这可能让"走开等长任务完成"的核心场景不再漏声,但它和测试中"没播出去的提示音不消耗冷却"这一不变量会冲突,需要一起重新设计,所以我没有把它放进上面的最小改动里。备注
另外观察到一个独立问题(还没定位完,先不混在这个 issue 里,定位到后另开):在「回合进行中但 agent 没有输出文字」的状态下,WebContent 主线程持续执行 rAF 回调并触发
MutationObserver,受控实测 30 秒消耗 5.39s CPU(约 18% 一个核心)。