结论: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 是原子写吗?
不是。序列化异常发生在打开目标之前;写入异常则可能留下被截断的旧目标。该路径没有隐私净化、哈希核验、原子替换或断电持久化。
延伸阅读
- URLFile 的 HTTP Range 与分块缓存:字节冲突从哪里来 —— 远端字节入口与本文的解压/解析衔接
- 进程回放的模型帧与时序参考:输入窗口、阈值与报告 —— 消费 LogReader 输出的下游
- 消息与日志契约:schema、有效位与时间基准 —— 事件包装之上应携带的语义
- 赛道日数据复盘:用 42 条赛道分段定位每一秒失分 —— 片段化日志在复盘中的用法
- 进程回放的容器调度与参数生命周期:回放链怎么被编排 —— 消费日志事件的上层回放调度
参考资料
- 项目 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
发表回复