Skip to content

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 假设解析新格式文件。
  • 实验(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-checkpoint CLI 只复制检查点声明的前缀到新目录,复制同时重新哈希校验,检查点之后的尾段数据标记为 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 索引 ​

16. 继续阅读 ​

版本历史补记 ​

日期修订来源
2026-10-26补本月实验结论、失败路线与适用限制;早期阶段数字保留月度梳理

Perfowl · Performance Observer