LogReader 生命周期:reset 不清缓存与跨片段不归并

作者:

在

结论:LogReader.reset() 只重算路径清单、并不清 __lrs;原类探针确认先读 first、把 identifier 改成 second 后调用 reset(),迭代仍返回 first 的缓存而未读取第二文件。sort_by_time=True 只在该文件内部排序,多个文件按 logreader_identifiers 列表顺序拼接、不做跨片段归并:假事件探针在两个文件内分别给出 30,10 与 20,启用排序后整体输出 10,30,20。以下为固定快照 c8fb906 的静态阅读与隔离探针,未读取真实路段、未执行进程回放。

关键函数与数据契约

解码围绕「单文件一次读入 + 惰性片段加载 + 事件包装」展开。下表列出关键函数、源码事实与插件化含义。

函数 源码事实 风险与插件化含义
save_log() 把每个事件重新序列化并 b"".join() 成完整缓冲;按输出后缀可选 bz2/zstd 压缩,再直接 open(..., "wb") 写目标 序列化异常发生在打开目标前;写入异常可能留下被截断的旧目标;无隐私净化、哈希核验、原子替换或断电持久化
decompress_stream() / _LogFileReader.__init__() FileReader 一次 read() 整个文件;先按 .bz2/.zst 扩展名,否则按 BZh9/zstd 魔数决定解压 后缀优先于魔数;错后缀会进入对应解压器并失败;压缩输入、解压输出与事件列表可能同时占内存
损坏事件分支 遍历 Cap’n Proto 事件时若抛 capnp.KjException,发出 RuntimeWarning,保留异常前已加入的事件 消费者仍可得到合法前缀,但没有返回「日志已截断」的结构化状态
CachedEventReader which() 首次计算后缓存 union 名称,普通字段访问交给原事件;__reduce__ 用于跨进程 只是读取加速包装,不验证事件 schema、有效位或字段语义
_LogFileReader.__iter__() 默认逐个返回包装事件;only_union_types=True 时调用 which(),判别失败则跳过 「仅 union 类型」会静默丢掉无法判别的事件,不是修复坏日志;sort_by_time 仅文件内排序
LogReader._get_lr() / __iter__() 按索引首次构造 _LogFileReader 后保存在 __lrs;多文件按标识列表顺序拼接 已加载事件列表长期留存;无跨片段归并
LogReader.reset() 重新计算 logreader_identifiers,没有清 __lrs 改标识后迭代仍返回旧缓存;源码内仅构造函数调用它
run_across_segments() / from_bytes() 前者用进程池 imap 按片段调用并聚合;后者以字节缓冲构建单文件读取器 存在序列化/内存成本;from_bytes(b'') 会触发空缓冲回退,不是可用空日志

解码与迭代的边界

「能解析」并不等于「完整」。坏 union 过滤会静默丢事件;dat=b'' 因 if not dat 被当作未提供而尝试打开路径。下表的受控输入在原函数分支下可复现。

输入 / 操作 实际结果 边界
压缩后缀与实际格式不一致 后缀优先,错后缀进入对应解压器并失败 compress=False 即使后缀是 .zst 也写原始字节
dat=b'' 被当作未提供,尝试打开路径 不是可用的空日志表示
坏 union + only_union_types=True 判别失败的事件被跳过 静默丢失,不是修复
两个文件分别给 30,10 与 20 启用排序后整体输出 10,30,20 确认没有跨片段时间归并
reset() 后同索引 返回旧缓存,未读取新文件 路径清单已变但事件缓存未失效

损坏事件分支保留异常前已加入的事件并发 RuntimeWarning,但没有返回「日志已截断」的结构化状态;消费者仍可得到合法前缀,因此完整性检查必须比较预期段、时间跨度、消息数与内容摘要。同理,only_union_types=True 会静默丢掉无法判别的事件,这不是修复坏日志。

故障传播与现有测试

六项导出隔离探针用原 save_log()、假消息和真实 bz2/zstandard 库验证:未知后缀直写拼接字节;bz2/zstd 后缀能解压回原字节;compress=False 可产出后缀与实际格式不一致的文件;序列化失败时现有目标保持原状;模拟写入失败时现有目标被 wb 截断并仅留下两字节。它未测试真实 Cap’n Proto 消息、断电持久性或大文件内存峰值。

该导出函数的调用方包括 run_process_on_route.py、进程回放测试与 PlotJuggler 桥;PlotJuggler 明确用 compress=False 写临时 .rlog。已有 test_logreader.py 验证重复迭代只构造一次 _LogFileReader,并覆盖文件内排序与坏 union 过滤。

并行与容错侧还有两点:run_across_segments() 用进程池 imap 按片段调用函数并聚合结果,from_bytes() 以字节缓冲构建单文件读取器;并行处理函数与事件包装存在序列化/内存成本,from_bytes(b'') 还会触发空缓冲回退。CachedEventReader 与 which() 缓存只是读取加速,不验证事件 schema、有效位或字段语义。日志的输入与输出都不是内容摘要证明——「可解析」既不代表完整,也不代表与固定参考一致。

LogReader.first() 与 filter() 都会触发迭代,第一次调用可能下载/解压所需文件;重复调用使用缓存,但仍遍历事件。若读取的是远端来源,HTTP HEAD 探测成功与实际 FileReader.read() 完成也在不同阶段;任何解压/解析失败需要被单独归档,而不能被上层概括成「该路线不存在」。

硬件与插件化验收

回放输入插件应交付固定有序片段清单、内容摘要、压缩格式、schema 版本、消息条数和时间覆盖;解析器输出「完整/截断/坏 union/解压失败」的显式状态。单段读入应设内存与解压比上限,必要时改流式处理;在跨片段时间归并前校验时间域与单调性。

reset/热重载必须同时使路径清单与已载入事件缓存失效,或把不可变快照做成新实例,不能仅更新标识列表。日志导出应由受信任边界执行字段级隐私审查、目标路径约束、临时文件与原子发布,再记录文件摘要;读取方应按魔数或受信任元数据核对真实压缩格式,不只看扩展名。

常见问题

reset 之后会读到新的文件吗?

不会自动生效。reset() 只重算 logreader_identifiers,不清 __lrs;原类探针确认先读 first、改 identifier 为 second 后调用 reset(),实际迭代仍返回 first 的缓存。

sort_by_time=True 会跨文件排序吗?

不会。它只在该文件内部按 logMonoTime 排序;多个文件按标识列表顺序拼接,探针给出的整体输出 10,30,20 即说明没有跨片段归并。

only_union_types 能修好坏日志吗?

不能。它在迭代时调用 which(),凡 union 判别失败的事件都被跳过,是静默丢弃而不是修复;完整性检查必须比较预期段、时间跨度、消息数与内容摘要。

save_log 是原子写吗?

不是。序列化异常发生在打开目标之前;写入异常则可能留下被截断的旧目标。该路径没有隐私净化、哈希核验、原子替换或断电持久化。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/LOGREADER_DECODE_ITERATION_LIFECYCLE_DEEP_DIVE.md(单文件解码、惰性片段加载与迭代契约)
  • 项目 02 —— openpilot-research/LOG_SOURCE_AVAILABILITY_CACHE_DEEP_DIVE.md(日志来源发现与可用性,作为本文的补充阅读)
  • 项目 02 —— openpilot-research/URLFILE_RANGE_CACHE_INTEGRITY_DEEP_DIVE.md(远端字节与缓存完整性,支撑「HEAD 成功不等于内容一致」)

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注