外观
Session 架构演进与实验
2026-10-26 · Stop、封存、报告统计与换机
本月经验:界面Stop成功、Collector停止、队列排空、Store持久化和结果窗ready是不同状态。静帧实验曾完整采集但关闭残留计数竞态使交付门失败;终止时应先解除订阅并完成排空,再取最终pending快照,不能靠忽略计数修成成功。静帧五轮与关闭修复。
原生报告R1–R3:每Session独立原生窗、共同时间轴/游标/区间、备注阶段与CSV/PDF已在227内部版实施;只有Store finalize成功才ready,错误保存不能发成功通知。历史Session只读,备注另存,窗口任务隔离、关闭取消迟到发布。403项Swift中10项跳过/0失败,最终227聚焦4项及真实历史UI/导出有E2/E3-host;不是新增手机E4。实现与资源证据。
资源与统计边界:合成1Hz一小时最终版首屏1.325493秒、游标状态至Canvas提交P95 10.918042ms,不是显示器呈现延迟;强制冷缓存、多窗总峰值/主线程剖析尚开。1Hz采样均值与Collector源序列/逐帧Stat可能不同,不能合并同名值当同一口径;用户CPU比较尚未完成,源码一致不证明系统精度。报告指南。
换机与保全:Git保存源码/脱敏合同,不保存用户Session、原Raw、秘密及DMG。旧证据未迁不阻止新机独立试用,但不能声称历史比对完整;同终态Session全树和收据SHA核同后才谈可替代副本,Raw/失败/引用证据不按年龄删。新电脑须重新核架构/签名/依赖/手机,不把原机ARM通过外推所有Mac。
本节E1复盘,正式55/66不变;实验方法索引汇总判断方法。
1. 当前结论先放前面
- 时间轴从"曲线各自按墙上时间重算"演进为"所有指标/截图/标注共用一条
SessionTimeline派生坐标",且显式区分valid/estimated/degraded/missing四种质量状态,不静默重排或补 0。 - CoreProfile 原始数据的存储策略从"整场持续落盘"改为默认
derivedOnly(不落盘完整 blob,仅内存驻留供实时解码),E4 近 5 分钟真机验证显示长会话磁盘占用下降约 98%(约 51 倍)。 - 截图采集从"每秒启动一次子进程"改造为"Session 期间维持一条独立长连接",随后又经历一轮 E4 真机 follow-up 修复(Worker→Swift 二进制传输完整性、持久化格式改为 HEIC),修复本身的 E4 验证仍待真机执行。
- 存储格式与回放路径的多轮(累计 100+ 轮)确定性重放验证证明"格式变更前后派生结果语义一致",但这类 E2 证据不能替代真机断电/长稳等 E4 场景的持久化证明。
2. 最初工程问题是什么
Session Review 能力从零开始时面对三个独立问题:(1)怎样在不改写原始事件顺序的前提下,为回放、跨指标游标和窗口筛选建立一个统一的时间坐标;(2)怎样让长时间连续采集(尤其是 CoreProfile 这类高吞吐 source)不会把本地磁盘写爆;(3)怎样在不影响主性能采集链路的前提下,独立获取足够高频的界面截图用于人工复盘。
3. 第一代方案与为什么当时合理
- 时间轴:
p4-session-review-timeline.md(2026-09-04)交付的第一代方案是"优先 host monotonic receive time,旧归档0哨兵统一回退 wall clock",并冻结一条关键约束:一个 Session 只能选一种时钟,不能在同一条时间轴里混用 monotonic 锚点与 wall-time fallback——这在当时是合理的,因为混用会在缺失字段处产生难以解释的时间断层。 - 存储:早期实现里 CoreProfile binary chunk 持续原样追加到
core-profile-chunks.bin,这是最简单直接的"先能跑起来"方案。 - 截图:早期实现每秒调用一次
pymobiledevice3 developer dvt screenshot,即每次截图单独建立一次 DVT 连接,这在验证"能不能截到图"阶段是合理的最小实现。
4. 暴露出的真实问题
- 存储:FTI-003/FTI-004 记录的现象是,启用 FrameTime 持续采集后 5 分钟长测即产生约 10 GiB 的 Raw 文件,磁盘 I/O 持续数十 MiB/s,长会话场景下不可持续。
- 截图:30 分钟长测下每秒重新建连导致约 1800 次进程启动/销毁、1800 次 DVT 重复建连与临时文件 I/O,并强制写入约 680 MiB 截图文件——即使用户根本没有勾选保存截图序列。
- 回放确定性:随着存储格式演进(legacy 内联 → 外置 blob),需要证明格式变更不会让同一份 Raw 输入在不同版本上产出不同的派生结果。
5. 候选方案 A:CoreProfile 存储分级(derivedOnly vs full)
- 假设:多数场景不需要保留完整的 CoreProfile 原始二进制 blob,只要实时解码用得到内存中的原始 chunk 即可;调试场景可以显式选择保留完整 blob。
- 配置:新增
SessionStoragePolicy(coreProfileRawRetention: derivedOnly | full),derivedOnly为产品默认,不生成core-profile-chunks.bin;capturePlanHash、CollectorReadiness、prewarmGeneration的输入严格排除storagePolicy,切换策略不触发 Collector 重启。 - 实验(E4,Build 58):真实 Session 运行 280.123 秒(4 分 40 秒),Session 总量从修复前同类约 5 分钟 Session 的 10.11 GiB 降至约 203.24 MiB,磁盘占用下降约 98.0%(约 51 倍);
coreProfilePersistedByteCount=0、core-profile-chunks.bin未创建,同时设备侧仍实际观察到约 3.75 GB / 850 chunks 的 CoreProfile 数据流过,证明"不落盘完整 blob"不等于"不采集"。 - 判定:候选方案 A 通过近 5 分钟真机验证,采纳为产品默认;30 分钟长测与
full调试模式的真机对照仍是开放门禁,批次因此保持implemented · partial,未提前关闭全部 E4 门禁。
6. 候选方案 B:截图独立长连接
- 假设:截图不需要每次重新建连,一条 Session 生命周期内维持一条独立 DVT 长连接、固定 1 秒节拍抓图,即可同时满足"顶部实时预览"和"按需持久化"两个需求,且不影响其他性能指标采集。
- 配置:新增独立子进程
screenshot_worker.py,与 Swift 端PersistentScreenshotWorker(后续演进为绑定ScreenshotImageEncoder的实现)之间用定长前缀二进制 IPC([HeaderLength][Header JSON][PNGLength][Raw Bytes])通信;persistScreenshotSequence字段不进入CapturePlan.hash,勾选与否不触发重新 Prepare App;Python 端watch_parent守护线程与 Swift 端生命周期绑定,防止僵尸进程。 - 实验(Build 59,E2):Python 411/411、Swift 153/153 全量测试通过;签名包交付并完成
codesign --verify --deep --strict验证。 - E4 follow-up 暴露的问题:首轮真机复测发现两个偏差——(1)未勾选 Screenshot 时顶部只显示单张图片原地替换,没有形成实时序列;(2)勾选 Screenshot 后落盘文件里 10/10 张虽有合法 PNG 文件头但 IDAT CRC 校验错误,真机证据显示这是 Worker→Swift 的二进制传输损坏,不是设备真的返回黑屏。
- 判定:长连接架构本身("1 秒 1 帧、独立进程、绝对隔离")被验证有效并保留;但初版的"预览与持久化共享同一份数据路径 + PNG 逐字节传输校验不足"被判定为需要返工的实现细节,进入候选方案 C。
7. 候选方案 C:预览/持久化分离 + IPC 完整性校验 + HEIC 持久化
- 假设:Bug A 的根因是"未勾选时没有为预览单独维护一份序列";Bug B 的根因是二进制传输缺少严格的保序与完整性校验。
- 配置:
- 在既有单线程采集循环内,对每一次有效截图(无论是否勾选保存)都编码一份 900px/Q60 缩略图进入内存的
liveScreenshotPreviewItems(previewHistoryMaxCount=3600/previewHistoryMaxBytes=96MiB双上限),修复 Bug A; - 为 Worker→Swift 的二进制 IPC 增加严格保序与四步完整性校验,修复 Bug B;
- 持久化格式从原始 PNG 改为 HEIC(1800px/Q75),本机无 HEIC 编码器时降级 JPEG,不降级 PNG;历史已落盘的 PNG Session 仍可通过与格式无关的
isDecodableImage(ImageIO 解码)校验读取,不强行按 PNG 假设解析新格式文件。
- 在既有单线程采集循环内,对每一次有效截图(无论是否勾选保存)都编码一份 900px/Q60 缩略图进入内存的
- 实验(E2):新增
ScreenshotImageEncoderTests覆盖 HEIC/JPEG 二选一不退化 PNG、编码产物自解码、拒绝不可解码输入等场景;回归测试识别出两处历史测试 fixture 用"仅 PNG 文件头 + 1 字节"伪造图片,在新的 ImageIO 校验下被正确拒绝(这正是本轮要修的问题本身,不是误报的回归),已替换为真实可解码的最小 PNG fixture。 - 判定:代码实现与 E2 回归已完成(Sidecar 430/430、Swift 167/167 通过)。该修复版本自身的 E4 真机验证(5 分钟/30 分钟场景)截至最新已整理的 devlog 时间线尚未执行,原因是当时环境没有可用的真实 iOS 设备——这是与"Bug A/B 已通过 E4 发现"不同的另一个开放门禁,不能因为发现问题的那轮 E4 已完成,就认为修复方案也已经过 E4 验证。
8. 候选方案 D:Raw checkpoint 与显式尾段恢复
- 假设:长会话中途崩溃或被杀进程时,应该能从最后一个已确认的 Raw 前缀恢复,而不是丢弃整个会话。
- 配置:
RawSessionRecorder.checkpoint()在 Raw writer 锁内同步 Raw 与 blob,原子发布独立的raw-checkpoint.json;recover-checkpointCLI 只复制检查点声明的前缀到新目录,复制同时重新哈希校验,检查点之后的尾段数据标记为partial并记录被排除的字节数,不猜测、不自动补齐。 - 实验(E2):全量 Python 264/264 通过,覆盖真实子进程
os._exit无清理退出、同步失败保留旧 checkpoint、前缀损坏、截断、符号链接、已有/嵌套目标等场景;300 MiB 输入、20 轮对照下 checkpoint 模式相比无 checkpoint 额外开销约 +1.35%(耗时中位)。 - 判定:候选方案 D 采纳为周期 checkpoint 的实现基础,但只覆盖 Raw 层——Metric 引擎的 checkpoint、崩溃后周期性自动调度、满盘持续采集恢复、真实断电场景,在这批次结束时仍是开放门禁。
9. 候选方案 E:存储格式变更下的确定性重放验证
- 假设:把 Raw/blob 存储格式从 legacy 内联改为外置 blob 后,同一份原始输入应当能在两种格式下产出语义一致的派生结果(metrics-derived、frame-events、frame-report、report-stat、startup-report、launch-spans 六类)。
- 实验(E2):首批 80 轮(新旧格式各 20 轮 × 两份样本)+ 追加 40 轮,AB/BA 交替、独立进程执行,六类产物语义哈希全部一致;大样本(约 267 MB)场景下外置格式 finalizer 耗时中位从约 2363–2557ms 降至约 583–625ms,进程峰值 RSS 同步下降约 47.5%;小样本(约 18.5 MB)场景下外置格式耗时中位反而略增约 1.6%。
- 判定:候选方案 E 证明了"格式迁移不改变确定性重放结果",支撑了后续把外置 blob 格式作为默认存储路径的决策。但这组证据的证明范围明确限定在"同一份本地 Raw 输入、多轮独立回放",不代表真机断电、拔线或磁盘故障下的数据持久化保证——那是完全不同强度的证据,仓库现有 devlog 中尚未见到对应的 E4 实验。
10. 被证伪路线与停止条件
- "文件级原子替换 = 整场事务":
p4-r3-format-replay-20-rounds-result.md等批次明确记录"不宣称目录持久化或物理断电耐久"——目录级发布不是单次原子操作,这条路线在设计阶段就被排除,没有进入候选方案。 - "Raw checkpoint 可以当作 Metric 引擎断点续算":候选方案 D 的实验明确排除了这个理解——checkpoint 只是 Raw 前缀凭证,恢复后仍需显式
finalize重建 Metric,不复用崩溃时的指标文件。 - "预览与持久化共享同一份数据路径":候选方案 B 的初版把两者耦合在一起,导致 Bug A(未勾选时无预览序列);候选方案 C 判定这条耦合路径需要拆分,不再作为架构方向。
11. 最终方案为什么胜出
当前架构(时间轴统一坐标 + CoreProfile 存储分级 + 截图独立长连接 + 预览/持久化分离 + Raw checkpoint)在每一步都遵循同一个原则:先用 E2 自动化证据证明设计本身自洽,再用有限范围的 E4 真机验证暴露实现细节问题,再针对性修复,而不是在证据不足时就把状态标成"完成"。这与仓库 AGENTS.md 的诚实红线直接对应,代价是多个子系统(Screenshot 修复版本的 E4、30 分钟长测、Metric checkpoint、断电恢复)在本文写作时仍处于 partial/开放状态,但避免了把"代码写完+单元测试通过"包装成"生产可用"。
12. 从 POC 到产品化又踩了什么坑
- 存储分级的 E4 验证一开始只覆盖了近 5 分钟场景,30 分钟长测被刻意留作独立门禁,而不是从 5 分钟结果外推——这是刻意的证据边界控制,不是遗漏。
- 截图长连接架构的 Bug A/B 都是在"E2 全绿之后"的 E4 真机复测才暴露的,说明二进制 IPC 和多路数据流(预览 vs 持久化)这类问题很难在没有真实设备网络往返延迟和真实 PNG/HEIC 编解码路径的情况下被单元测试完全覆盖。
- 两处历史测试 fixture 用"伪造的最小 PNG 数据"这一捷径,在引入真正的 ImageIO 格式校验后被正确拒绝——这提醒了一个通用教训:测试 fixture 本身的真实性也需要跟着产品的校验强度同步升级,否则修复级校验上线时会先"误伤"这些走捷径的旧测试。
13. 当前仍未关闭的门禁
- CoreProfile 存储分级:30 分钟长测与
full调试模式真机对照。 - 截图预览/持久化分离 + HEIC 持久化修复版本:5 分钟/30 分钟场景的 E4 真机验证。
- Raw checkpoint:崩溃后周期性自动调度、满盘持续采集恢复、真实断电场景的 E4 证据。
- 存储格式确定性重放:1 GB 级与 60 分钟级规模场景尚未覆盖。
- Developer ID、公证、ticket stapling 与 GPL-3.0 法务合规,在 Session 架构范围内继续开放。
14. 可复用工程经验
- 策略字段要显式排除出 hash 计算:
storagePolicy/persistScreenshotSequence不进入capturePlanHash,使得"运维/用户侧调整存储策略"和"是否需要重新武装采集源"两件事彻底解耦,避免每次调整存储偏好都触发重量级的重新准备流程。 - 失败状态要能传播、不能被掩盖:
SessionWriter.append()用一个failed位把 Raw/Metric/Report 三层绑定成一次要么整体成功、要么整体失败的操作,避免"Raw 写成功了但 Metric 没跟上"这种介于两者之间的模糊状态被误判为可恢复。 - E2 证明设计自洽,E4 暴露实现细节:多个子系统的经验都表明,E2 自动化测试能证明"逻辑设计正确",但二进制传输完整性、真实设备息屏/黑屏边界、真实长时间资源占用这类问题,仍然需要专门的 E4 真机场景才能发现,两者不能互相替代。
15. 原始 devlog 索引
p4-session-review-timeline.md:Session Review 时间轴基础。p4-session-review-store.md:归档索引与回放加载。p4-session-review-ui.md:历史会话界面。p4-core-profile-storage-policy-impl.md:CoreProfile 存储分级实现(Build 58)。p4-screenshot-persistent-connection-plan.md:截图长连接架构(Build 59)。p4-build59-screenshot-e4-followup-repair-plan.md:Build 61 综合 E4 后续修复方案(Screenshot Bug A/B/HEIC、FrameTime、GPU Counter)。p4-r3-format-replay-20-rounds-result.md/p4-r3-seal-consistency-result.md:存储格式确定性重放验证。p4-r3-checkpoint-recovery-result.md:Raw checkpoint 与显式前缀恢复。p6-r1-r6-continuous-development-result.md:R1-R6 连续开发收口(缩略图缓存、增量 checkpoint、GPU Counter 新鲜度等)。- 完整开发记录时间线见
site/devlog/index.md。
16. 继续阅读
版本历史补记
| 日期 | 修订 | 来源 |
|---|---|---|
| 2026-10-26 | 补本月实验结论、失败路线与适用限制;早期阶段数字保留 | 月度梳理 |