jyk/voice-ime · v0.4.3 增量质检报告
- 对比区间:base
3bc1698c0ae11f0101c330400365d7b3626927a3 → head bfff0e5958a03c621a22b7f3dd5aff53238440b0
- 分支:v0.4.3
- 变更文件(4 个):
docs/17-开发经验库.md、docs/19-识别链路与后处理设计.md、internal/session/session.go、internal/session/final_off_test.go
- 工具差异结论:修复 6 条 / 新增 7 条(均为静态规则命中,没有功能性 bug 被修复,详见第三节甄别)
- 人工复核额外发现:1 处新增逻辑缺陷(度量被污染,Low),1 处新增并发面(可接受但需知悉)
一、本次改了什么(一句话)
把「预览流收尾 + final/预览编辑距离度量」从"松手→上屏"的关键路径,搬到识别结束后的后台协程。
动因(作者已写进 docs/17、docs/19):预览流要补 500 ms 静音收尾才能拿到与 final 可比的文本,而这一步实测要 167 ms(即使帧已全部解完);第一版把它压在松手路径上,pack_ms 从 0~1 ms 涨到 53~122 ms,而 docs/03 §7 的预算只有 20 ms。
逐点对照
| 位置 | 变更 | 说明 |
|---|
session.go:2596 collectAudio() | 不再收尾/关闭预览流,改挂到 s.pendingPreview | 返回值从 ([]int16, string) 收窄为 []int16 |
session.go:1684 finishAndInject() | 新增 defer s.settlePendingPreview() | 在识别结束后、函数返回时处理 |
session.go:1691-1697 | asr.final.enabled=false 时同步收尾并摘掉 pendingPreview | 那种模式下预览文本就是要上屏的文本,167 ms 是识别本身成本 |
session.go:2649-2680 settlePendingPreview()(新) | 后台 goroutine:收尾 + 与 final 比对 + 无条件 close() | 先在本协程取快照再起协程,避免后台读可变字段(数据竞争纪律) |
finishPreview → (:2613)/ (:2629) |
方向评价:这是一次正确的性能修复——把纯度量/埋点移出用户可感知延迟,符合项目自己总结的准则("加埋点后先量一次它自己花多少时间")。改动是纯内部重构 + 一条日志口径变化,没有对外接口变化,没有新增文件。
二、修复的问题(工具 issues_diff:6 条)
| # | 严重度/类型 | 位置 | 建议 | 我的甄别 |
|---|
| 1 | Medium/Complexity 函数过长 | session.go:1749 | 拆分函数 | ⚠️ 位移重复:同名项在 head @1772(deliver,115 行)仍被报出。函数体没变小,只是行号挪了 |
| 2 | Low/Maintainability 行过长 | session.go:1749 | 换行 | ⚠️ 位移重复:head @1772 仍报 |
| 3 | Low/Maintainability 过度日志 | session.go:1749 | 降噪 | ⚠️ 位移重复:head @1772 仍报(4 处 log) |
| 4 | Low/Maintainability 过度日志 | session.go:1870 | 降噪 | ⚠️ 位移重复:head @1897 仍报 |
| 5 |
结论:6 条"修复"里,5 条是同一批函数因行号位移被重新识别(函数本身没改小),只有第 5 条(高耦合)像是本次去掉参数带来的真实下降。本次没有修复任何功能性缺陷——它修的是一处 2026-10-08 当天引入又当天发现的性能回归(pack_ms 超标),那是 base 里相对"接线前"的回归,静态规则表里并不体现。
三、新增问题(工具 issues_diff:7 条)
| # | 严重度/类型 | 位置 | 建议 | 我的甄别 |
|---|
| 1 | Medium/Complexity 圈复杂度过高 | session.go:1662(finishAndInject,count=12) | 拆分 | ✅ 真实新增:新增了 asr.final.enabled=false 的同步收尾分支,复杂度上升 |
| 2 | Medium/Complexity 函数过长 | session.go:1772(deliver,115 行) | 拆分 | ⚠️ 与"修复#1/#2/#3"同源(位移) |
| 3 | Low/Maintainability 行过长 | session.go:1772 | 换行 | ⚠️ 同上 |
| 4 | Low/Maintainability 过度日志 | session.go:1772(4 处 log) | 降噪 | ⚠️ 同上 |
| 5 | Low/Maintainability 过度日志 | session.go:1897(3 处 log) | 降噪 | ⚠️ 与"修复#4"同源(位移) |
|
关于新增 #1(真实)
finishAndInject 从纯粹的"收音频→deliver"多了两段:defer s.settlePendingPreview() 与 final-disabled 的同步收尾分支,圈复杂度到 12。属于可接受的小幅上升,但该函数确实在向"什么都干"演进(它现在还管设备释放、声纹过滤、无语音判定、度量收尾)。
关于新增 #6(goroutine 泄漏风险)——需辨析,非直接定论
- 按 head 源码行号落位,
session.go:2744 指向 hidePanelAfter 的 go func()(既有代码,不在本次 diff 内);而本次 diff 新增的分离 goroutine 是 settlePendingPreview 里的那个(约 :2663)。分析器在本文件只报出一条 goroutine 项,因此这条很可能是既有模式的位移/去重结果,或是对新增 goroutine 的指认——两者都值得评估,但不能简单记成"本次引入了一个泄漏"。
- 实测评估(两个 goroutine 都"无调用方 context 取消"):
settlePendingPreview 的 goroutine:内部 finalizePreview 用 context.WithTimeout(Background, 5s),且 defer ps.close() 无条件释放;读的是启动前的局部快照,不触碰可变字段。生命周期有界(≤ ~5 s,除非 cgo 解码不可中断),不会泄漏 C 资源。属可接受,只是它脱离了 Session.Close() 的生命周期——进程退出时可能仍在跑。
hidePanelAfter 的 goroutine:time.Sleep(d) 后按 panelGen 判断再隐藏,同样有界,属既有设计。
- 建议:把这类"分离 goroutine"的生命周期边界写进注释(本 diff 已对
settlePendingPreview 做了说明),并在 Close() 里考虑等待/取消,避免工具每轮都报。
四、人工复核发现(工具未报,但值得记录)
4.1 【新引入·Low】识别对照 度量可能被上一轮的残留值污染
事实链(均已核对行号):
pendingFinalText / pendingHasFinal 只在 deliver 成功时写入(session.go:1795,if err == nil)。
settlePendingPreview 只在 ps != nil 时清空这两个字段(session.go:2657-2661);ps == nil 提前 return(:2653)不清空。
- 重试路径
handleRetry(session.go:2287)也调用 deliver,previewText="",但重试路径从不调用 settlePendingPreview → 若重试识别成功,pendingFinalText="重试文本"、pendingHasFinal=true 会残留到下一轮会话。
- 每轮开始时只有
collectAudio 写 pendingPreview,不重置 pendingFinalText/pendingHasFinal。
触发条件(窄但真实):某次"重试识别"成功 → 之后某次普通会话里 recognize 失败(err != nil,于是 :1795 不写入)但存在预览流 → settlePendingPreview 用上一轮重试的 final 文本去和本轮预览做编辑距离 → 打出错误的 final_runes / diff_runes。
影响:只污染诊断日志里的数字(不含文本内容,红线未破,无用户可见后果)。但这条日志被 docs/19 明确称为"asr.final.enabled 该不该关的唯一证据来源",一旦被污染会把那个决策带偏。
建议:在每轮会话起点(如 finishAndInject 开头或 collectAudio 里)重置这两个字段;或让 settlePendingPreview 在 ps == nil 时也清空;或干脆把二者并入 pendingPreview 同一个结构、只随预览流生命周期存在。
4.2 【口径变更·运维】日志 grep 命令与脚本需同步
- 旧口径
grep '识别完成' … preview_runes 已失效,新口径为 grep '识别对照' daemon.log | grep -o 'diff_runes=[0-9]*' …(docs/19 §2.2 已更新)。
- 已核查:
scripts/soak/report.py 只读 sample.sh 写的 TSV(sessions/injects 计数),不解析守护进程日志的这两个字段,不受影响;internal/ 与 cmd/ 下除 session.go、final_off_test.go 外无其它代码依赖旧字段;docs 中也无残留引用旧"识别完成"行上的 preview_runes/diff_runes(docs/17:921 提到的「识别完成」是 level_peak 的历史条目,与本项无关,仍成立)。
4.3 【测试】异步日志带来的时序
- 断言改为
waitFor 等待 识别对照 落日志,写法正确;注意该日志现在异步产生(可能在会话结束后才出现,也可能与下一轮日志交错)——后续若有测试按"会话结束即已有该行"的强假设,需同样用 waitFor。
五、质量地图(head 全量)
- 版本质量问题共 844 条(未做全量展开)。按文件类型看,绝大多数集中在 前端 JS(
cmd/voiceime-app/frontend/*.js) 与既有 Go 大函数上:
- 高严重度以 High/Security:XSS(
innerHTML) 为主(hotkey.js、wizard.js 多处),属存量,不在本次变更范围。
- Medium 以"圈复杂度过高 / 高耦合 / 循环依赖 / 函数过长"为主,集中在
app.js、wizard.js、hotkey.js。
- 本次 diff 只触及
internal/session/session.go,对该文件的新增/修复项已在前两节逐条甄别。
- 建议:把质检基线收敛到"自 base 以来被改动文件的变更集",否则 844 条存量噪声会淹没增量信号(本次就出现大量"行号位移导致的伪修复/伪新增")。
六、结论与建议
- 改动定性:正确的性能修复——把 167 ms 的纯度量开销移出
pack_ms 关键路径;纯内部重构 + 日志口径调整;无接口破坏、无新增文件;文档与测试均已同步。可以合入。
- 务必顺手修掉的 1 处:
pendingFinalText/pendingHasFinal 跨轮残留(§4.1)。它是本次重构新引入的、窄路径上的度量污染,修法很小(会话起点重置或合并进 pendingPreview)。
- 可接受的 2 处:
finishAndInject 复杂度升到 12(新增 #1);settlePendingPreview 的分离 goroutine(有界、必关流)。建议给分离 goroutine 明确生命周期边界/在 Close() 里收敛。
- 噪声提示:
issues_diff 的 6 修复 / 7 新增里,大部分是行号位移造成的同一批函数重复识别(deliver、Close 区域、recognize),并非真实增删;唯一真实新增是高耦合的下降(修复#5)与 finishAndInject/度量收尾带来的复杂度上升(新增#1)。评估时请以函数为单位、而非以行号为单位。
- 运维:留意
识别对照 新日志行的 grep 口径替换(§4.2)。