返回系列

记录复盘:RecordManager 为什么要保存 pre、record、post 和 scan

解释 RespiraScope 的记录系统为什么不只保存用户点击 Start 到 End 的区间,而要保留前后辅助点、扫描子区间和离线复算结果。

背景问题

实时监测页面能告诉你“现在发生了什么”,但工程调试经常还需要复盘“刚才那一段为什么这样”。比如一段扫描期间出现咳嗽,实时 BPM 变乱;或者 Record End 后发现峰谷标记偏晚,需要用离线滤波重新判断。

如果记录系统只保存用户点击 Record Start 到 Record End 中间的点,会出现两个问题:离线滤波缺少边界上下文,扫描子区间也无法和完整记录区分。RespiraScope 的 RecordManager 因此把记录拆成 pre、record、post 和 scan。

RespiraScope 记录复盘界面

设计方案对比

方案优点问题项目判断
只保存 Start 到 End文件小,逻辑简单离线滤波边界不稳定,缺少上下文不够可靠
保存全程实时数据信息完整文件大,用户真正记录区间不清楚不适合交互复盘
保存记录区间 + 前后辅助点文件可控,滤波上下文足够数据结构更复杂当前采用
直接保存截图最直观不能重新滤波、不能集成、不能计算指标只适合展示

当前方案的核心是:用户关心的记录范围和算法需要的计算上下文不是同一个范围。把它们区分开,后续分析才不会混乱。

当前方案

RecordManager 维护四类缓存:

  • raw_pre_buffer、filter_pre_buffer、peak_pre_buffer、valley_pre_buffer:记录开始前的辅助缓存。
  • raw_record_queue、filter_record_queue:实际捕获的记录数据。
  • peak_record_queue、valley_record_queue:记录期间的实时事件。
  • scans 和 active_scan:用户在记录期间标注的扫描子区间。

当用户点击 record/start,后端会把开始前的 pre_points 拷进记录队列。点击 record/end 后,后端继续等待 post_points,再做离线滤波。

pre 辅助上下文 record 用户真正选择的记录区间 post 结束后辅助点 scan #1 Record Start Record End

为什么这样选

离线滤波需要上下文。Butterworth 双向滤波和 Gaussian 平滑都受边界影响。如果只把用户选择区间拿去滤波,Start 附近和 End 附近的结果更容易失真。pre_points 和 post_points 的作用就是给离线滤波提供缓冲,但保存结果时仍能标明哪些点只是辅助。

scan 的设计解决另一个问题:一次记录里可能有多个业务动作,比如多次扫描、咳嗽标注或设备操作。scan 不应该拆成多个 record,因为完整呼吸上下文仍然有价值;也不应该只靠前端颜色标记,因为保存文件和外部系统也需要知道 scan 范围。

记录结束为什么直接离线复算

早期设计可以让前端拿到记录数据后再调用 /applyFilter。现在 /record/end 会直接等待 post 辅助点,并返回本次记录的离线滤波结果。这减少了前端编排复杂度,也避免外部系统忘记携带辅助点。

/applyFilter 仍然保留,用于外部系统对任意历史 raw_data 做离线复算。也就是说:

接口适合用途
/record/end当前正在进行的记录结束,并返回本次离线结果
/applyFilter外部系统已有历史数据,想单独复算
/record/save把当前完整记录写入 JSON 文件

代价和边界

记录结构比简单数组复杂。保存文件里会有 segments、scans、scan_indexes、record_start_sequence、capture_start_sequence 等字段。复杂度是有意引入的,因为它换来的是复盘时的可解释性。

当前记录仍然是内存缓存,不是长期数据库。MAX_RAW_RECORD_POINTS 和 MAX_MARKER_RECORD_POINTS 对记录长度有保护,适合分钟级片段,不适合作为全天候连续归档系统。

关键函数表

函数或类作用设计点
RecordManager.start_record()开始记录并拷贝 pre buffer明确记录区间和捕获区间
RecordManager.stop_record()结束用户记录区间并进入 post 捕获为离线滤波补上下文
RecordManager.start_scan()开始扫描子区间允许一次记录中标注多个业务片段
annotate_points()给点标注 segment 和 scan_indexes保存文件可直接解释每个点
raw_points_for_filter()提取可离线滤波的 raw 点避免把元数据混入算法输入
BreathProcessSystem.stop_record_and_filter()结束记录并离线复算后端统一完成复盘逻辑
write_file()写 JSON 文件使用 UTF-8 和缩进,便于人工检查

读源码建议

读 record_manager.py 时先看三个状态:recording、post_recording、record_complete。然后看 start_record() 和 stop_record(),理解 record_start_sequence 与 capture_start_sequence 的区别。最后看 build_record_data(),它决定了保存文件和 /record/end 结果为什么有这些字段。