Skip to content

热键 bridge 串行阻塞:一次慢 IO 就能让热键彻底失灵(已为此挖了三条旁路) #872

Description

@bigsongeth

现象

热键的按下 / 松开事件走一条串行 bridge 线程(hotkey_bridge_loopasync_runtime::block_on 逐个处理)。而这条线程上跑的是开麦克风、连 ASR、转写、LLM 润色这些慢活。

结果是:这条线程上任何一次慢 IO,都会让热键在此期间完全失去响应 —— 按下开不了录音,松开也停不了,且没有任何 UI 提示,用户只会认为「热键坏了」。

实测(macOS,StepFun 实时 ASR):连续开会话时一次 WebSocket 建连没有返回,之后热键完全无响应,只能退出 app 重开。日志里 tap 线程仍在正常记录按键,只是没人处理:

07:20:14.201  [hotkey] 触发键与其他键组合按下 —— 撤销本次触发     ← tap 活着
07:20:15.985  [hotkey] 触发键与其他键组合按下 —— 撤销本次触发     ← tap 活着
                                                    ↑ 但没有任何 [coord] hotkey pressed

sample 确认 openless-hotkey-bridge 停在 __psynch_cvwaitblock_on 等 future),主线程与 tap 线程都正常。

#871 给三个供应商的建连补了 5s 超时,把「永远死」降级成「死 5 秒」。但那是止血,不是修复。

这个结构已经反复付出代价

它不是一个假设中的隐患 —— 目前代码里至少有三处逻辑,存在的唯一理由就是绕开这条线程会堵:

  1. Esc 取消专用通道fix(hotkey): Esc 取消改走独立通道——修复转写/润色期间按 Esc 停不下来 #853)。转写 + 润色在 bridge 线程上同步跑,Esc 若与 Pressed/Released 同队列就只能排在后面,等流程结束才被取出,此时 phase 已回 Idle、cancel 变 no-op —— 用户按 Esc 毫无反应。修法是给 Esc 单开一条 Sender<()> + 专用消费线程。

  2. 组合键撤销专用通道fix(hotkey): 修饰键热键被当组合键用时不再误唤起听写(150ms 仲裁 + 即时撤销,胶囊不再赖着) #858)。同一个病:撤销事件排在这次按下自己的 begin_session 后面,胶囊要等开麦跑完才消失(实测差 0.8~1s)。又单开一条通道。

  3. 「排队接力」判定HOTKEY_QUEUE_GRACE_MS = 120is_queued_chain_press)。转写期间的按下被缓在 channel 里,会话收尾后才取出;需要额外逻辑区分「刚才排队的那次按下」和「用户新按的」,以免与 [ui] 听写快速连按应在上一轮完全结束前忽略激活(动画已缓解,逻辑未改) #545 的冷却保护打架。

每加一条旁路,就多一组「两条通道之间谁先谁后」的竞态要考虑。#858 里已经为此加了回归测试(Released 抢先跑完时撤销仍须生效)。这是在给同一个病贴第 N 张膏药。

为什么不能简单地把串行撤掉

串行本身是 #468/#475 的解药:Pressed/Released 若并行执行,hotkey_trigger_held latch 的翻转顺序会乱(Windows 的 WH_KEYBOARD_LL 边沿间隔可到微秒级),表现为「录音停不下来」或「下次按键被静默吞掉」。代码注释里标为 P0。

所以这不是「删掉 block_on 就好」,而是要换一种方式同时满足:

  • 按键边沿的处理顺序必须严格跟随物理顺序;
  • 慢 IO(开麦 / 建连 / 转写 / 润色)不得占住边沿处理。

候选方向(列出来,不替维护者选)

  • A. 边沿处理与会话工作分离:bridge 只做纯同步的状态机迁移(latch、phase 判定),把 begin_session / end_session 派发到运行时,用 session id + 代次号丢弃过期结果。顺序由 bridge 保证,慢活不再占线程。
  • B. 保留串行,但给所有落在 bridge 上的 await 强制上限:本质是 fix(asr): WebSocket 建连补超时——握手挂死不再让热键彻底失灵(只能重启 app) #871 的推广,简单但治标;只要有人漏加一个超时就会复发。
  • C. 单一事件队列 + 优先级:把 Esc / 组合键撤销那些旁路收回同一条队列,用优先级而非独立通道,减少竞态面。不解决「慢活占线程」,但能止住旁路增殖。

各有代价,且都动到当初为修 #468/#475 而引入的部分,风险不低。

我的打算

暂不动手,等维护者定方向。这块是全仓库最容易出竞态的地方,我不想在方向未定时先改,那只会制造一处最难合并的分叉。

如果维护者认可某条路子,我可以按那个方向做,并先把按键顺序那部分单独抽出来补测试,确认改完顺序仍然正确再动线程。

相关:#853#858#871(均为绕开或止血),#468 / #475(串行的由来),#545(冷却保护,与排队接力互相牵扯)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions