Skip to content

Display Performance · ActivityTrace FrameLifetime 全量切源开发记录 ​

执行状态:implemented · gate-open R0-R6 已完成(详见对应章节)。R7 首轮真机联调失败并定位根因后,进入 R7-Fix-A ~ R7-Fix-I 修复子阶段:当前完成 R7-Fix-A(Display Performance 5/5 Registry 闭环)、R7-Fix-B(Start control_rejected 静默吞掉修复)、R7-Fix-C/D(清理旧 CoreProfile FrameTime ownership)与 R7-Fix-E/F(跨层 Start Contract 集成测试),代码层(E2)修复与测试已全部完成。等待 R7-Fix-G(真机 Gate-0)——该阶段需要用户连接真实 iOS 设备执行,无法由 AI 协作者独立完成。


R0 · 冻结 POC Fixture(2026-09-17) ​

R0 目标 ​

无真机条件下建立确定性 FrameLifetime 开发基础。创建匿名化 ActivityTrace FrameLifetime fixture,包含 POC 验证的 golden 统计值,使后续 R1-R7 的 decoder、projector、adapter 开发全部可离线测试。

R0 实际修改 ​

新增文件 ​

文件用途
MacApp/Sidecar/tests/fixtures/activitytrace-framelifetime/golden-5s-90frames.json匿名化 golden fixture,90 个 FrameLifetime signpost 事件
MacApp/Sidecar/tests/test_activitytrace_frame_lifetime.py31 项 R0 fixture 验证测试
site/devlog/p4-display-performance-activitytrace-frametime-cutover-plan.md本开发记录

Fixture 规格 ​

text
Schema: perfowl.activitytrace-framelifetime-fixture/1
Events: 90
Timebase: 125/3 (典型 A-series)
Signpost: com.apple.coreanimation / IOMFBServer / FrameLifetime

Golden 统计值 ​

text
totalEvents:        90
chainedPairs:       59 / 89 adjacent pairs
medianFrameTimeMs:  49.948458
refreshIntervalNs:  16666666
refreshRateHz:      60.00
duration:           ≈ 4.5s

Fixture 事件字段 ​

json
{
  "index": 0,
  "subsystem": "com.apple.coreanimation",
  "category": "IOMFBServer",
  "name": "FrameLifetime",
  "event_type": "signpost",
  "begin_time": "<mach ticks>",
  "end_time": "<mach ticks>",
  "prev_frame": "<mach ticks>",
  "refresh_interval": "<mach ticks>",
  "frame_seed": 1000,
  "buffer_count": 2,
  "skip_request": 0,
  "display_id": 1,
  "surface_id": 100
}

链式连续性设计 ​

  • Events 0-30(Phase 1):31 个事件,prev_frame ≠ previous.end_time(模拟 warmup/间断)
  • Events 31-89(Phase 2):59 个事件,prev_frame == previous.end_time(完美链式连续)
  • 总计 59 个 chained pairs,与 POC "59/59 PASS" 一致

R0 测试结果 ​

text
R0 专项测试:     31/31 PASS (0.009s)
全量 Python 回归: 485/485 PASS (11.397s)

测试覆盖 6 个维度:

  1. FixtureSchemaTests (6 项):schema 版本、匿名化标记、事件数、timebase
  2. EventFieldTests (8 项):必填字段、subsystem/category/name 一致性、时间单调性
  3. ChainContinuityTests (3 项):精确 59 对链匹配、Phase1 无链、Phase2 全链
  4. FrameTimeStatisticsTests (5 项):FrameTime 计数、正值、median 匹配、范围
  5. RefreshRateTests (3 项):60Hz 转换、golden 一致性
  6. AnonymizationTests (4 项):无 UDID、无 Bundle ID、无设备名、无序列号
  7. TimebaseConversionTests (2 项):已知转换验证、fixture 持续时间

R0 偏差 ​

项方案值实际值原因
medianFrameTimeMs49.92649.948Mach tick 整数量化 (125/3 timebase),偏差 0.022ms,在 0.1ms 容限内
持续时间~5s~4.5sPhase 1 的 31 个事件间距稍紧凑,不影响 golden 统计

R0 可复用经验 ​

  1. prev_frame 与前一帧 end_time 的关系是 FrameLifetime 链式连续性的核心判据。
  2. Mach tick 整数量化使 ms 级精度出现可预期的微偏差,decoder 测试应使用 assertAlmostEqual(places=3) 而非精确相等。
  3. Fixture 生成脚本使用固定随机种子 (random.seed(20260917_02)),保证输出可复现。

R0 遗留 ​

无。

R0 → R1 门禁 ​

text
[x] Fixture 文件存在且通过 schema 验证
[x] 31 项 R0 测试全绿
[x] 全量 Python 回归无新增失败 (485/485)
[x] 无隐私数据泄露
[x] R0 devlog 写入并同步时间线

R0 PASS 后方可进入 R1(实现 ActivityTrace Adapter)。


R1 · ActivityTrace Adapter 实现(2026-09-17) ​

R1 目标 ​

落地首个非侵入 ActivityTrace FrameLifetime 专用采集适配层:

  • 严格仅保留 com.apple.coreanimation / IOMFBServer / FrameLifetime
  • 其他第三方 App log、os_log、系统隐私 signpost 在采集源头直接丢弃(persisted = 0)
  • 实现动态时间基转换(支持任意 numer/denom 整数除法,禁止写死 125/3)
  • 落地 FrameTime 连续性规则(prev_frame 链匹配、sourceEpoch/reconnect/targetGeneration/displayID/foreground 边界断链判定)
  • 落地 Schema Drift fail-closed 机制(缺字段或无法转为整数时置 unsupported-schema)
  • 完成 Adapter start/stop 生命周期与 golden fixture 端到端回放验证
  • 不修改 UI、CoreProfile、生产 FrameTime 产品链

R1 实际修改 ​

新增文件与核心类 ​

  1. MacApp/Sidecar/perfowl_collector_core/display_performance.py:
    • ActivityTraceFrameLifetimeConfig:冻结 POC 配置语义(onlySignposts=1, signpostsAndLogs=0, targetPID=-3, predicate="",支持动态 machTimebaseNumer/Denom)。
    • is_frame_lifetime_signpost(raw):严格白名单检查器,仅放行 subsystem == "com.apple.coreanimation" 且 category == "IOMFBServer" 且 name == "FrameLifetime"。
    • extract_frame_lifetime_fields(raw):类型化字段提取与 schema 校验,解析 9 项必填字段(begin_time, end_time, prev_frame, refresh_interval, frame_seed, buffer_count, skip_request, display_id, surface_id)。
    • FrameLifetimeDecoder:有状态解码器,维护连续性锚点,产出 perfowl.display-frame-event/2 规范化事件。
    • _ActivityTraceFilteredStream:异步流包装器,源头过滤非 FrameLifetime 消息。
    • ActivityTraceFrameLifetimeRawAdapter:_RawStreamAdapter 实现类,adapter_id = "pmd3.activity-trace.frame-lifetime.raw", event_type = "display.frame-lifetime.raw", scope = "foreground-device", selector = "onlySignposts"。

联动文件 ​

  1. MacApp/Sidecar/perfowl_collector_core/adapters.py:导出 ActivityTraceFrameLifetimeRawAdapter 与 ActivityTraceFrameLifetimeConfig。
  2. MacApp/Sidecar/perfowl_collector_core/orchestrator.py:在 ADAPTER_TYPES 静态注册表登记 ActivityTraceFrameLifetimeRawAdapter。
  3. MacApp/Sidecar/perfowl_collector_core/__init__.py:导出新适配器与解码器类。
  4. MacApp/Sidecar/tests/test_activitytrace_frame_lifetime.py:追加 26 项 R1 专项单元测试(Test 32 ~ Test 57)。

R1 测试结果 ​

text
专项测试 (R0 + R1):  57/57 PASS (0.047s)
全量 Python 回归:    511/511 PASS (12.789s)

测试覆盖维度:

  1. ActivityTraceConfigTests (3 项):默认 12 项配置、自定义 timebase、参数覆盖
  2. WhitelistPrivacyTests (4 项):subsystem、category、name 独立白名单拦截,任意用户隐私日志丢弃
  3. FieldExtractionAndSchemaTests (3 项):类型化提取、缺少字段 fail-closed、非整数字段 fail-closed
  4. TimebaseAndRateConversionTests (4 项):任意 dynamic timebase、60Hz 转换、120Hz 转换、非法 zero refresh interval 保护
  5. ContinuityRulesTests (8 项):prev_frame 链匹配 valid、mismatch discontinuity、sourceEpoch reset、reconnectGeneration reset、targetGeneration reset、displayID reset、foreground/background transition reset、invalid timestamp ordering
  6. AdapterLifecycleAndStreamingTests (4 项):capability/probe、start/stop 生命周期、混合流源头白名单拦截(其他 signpost 产出 = 0)、90 帧 golden fixture 端到端全量回放(59 帧 valid,median FrameTime 49.948ms,60Hz 刷新率)

R1 偏离方案的地方 ​

无。完全按照任务书第 4、6、7、8、9、10、52、57 条规范落地。未改动 UI、CoreProfile 及既有 FrameTime 产品链。

R1 失败尝试 ​

在初期运行 test_activitytrace_frame_lifetime.py 时,未显式设置 PYTHONPATH=MacApp/Sidecar 导致 ModuleNotFoundError,通过加上正确的 PYTHONPATH 规范解决。

R1 可复用经验 ​

  1. _ActivityTraceFilteredStream 在迭代器级别直接短路非目标 signpost,避免了为丢弃的事件分配序列号或分配内存,确保“其他 signpost persisted = 0”在物理层彻底实现。
  2. 在 FrameLifetimeDecoder 中将断链判定(boundary change)与链式匹配(chain match)分离,使得 warmup、valid、discontinuity 和 not-foreground 四种状态语义界限极其清晰。

R1 剩余问题 ​

本阶段 Adapter 产出标准化的 display.frame-lifetime.raw 原始事件,尚未接入实时指标聚合器(FPS 滑动窗口、P95、Jitter 等由 R2 实现)。

R1 → R2 门禁 ​

text
[x] 只保留 FrameLifetime,其他 signpost 过滤后 persisted = 0
[x] schema drift fail-closed 机制有效(缺少必填字段输出 unsupported-schema)
[x] start/stop 生命周期正确
[x] golden fixture 端到端回放 90 帧全部通过(59 帧 valid,median 49.948ms)
[x] 57 项专项测试全绿
[x] 511 项全量 Python 回归无新增失败
[x] Swift 回归测试完成
[x] devlog 回写并同步时间线
[x] 站点构建 0 错误 0 死链

R1 验收合格,可进入 R2(DisplayPerformanceProjector)。


R2 · DisplayPerformanceProjector 实现(2026-09-17) ​

R2 目标 ​

完成 Display Performance 核心聚合计算器(DisplayPerformanceProjector):

  • 落地 Trailing 1 秒滑动窗口 FPS 计算(禁止 1000/FrameTime,真正统计 1 秒内呈现帧数);
  • 落地静止画面时间桥(estimatedDisplayNowNs = lastPresentationTimestampNs + max(0, hostNowNs - lastPresentationHostReceiveMonotonicNs)),解决静态页面 FPS=0 淘汰问题,无帧时输出 0.0 且 quality = "candidate-idle";
  • 落地 FrameTime one-shot freshness(无新帧时置 missing-no-new-frame,严禁重复旧值);
  • 落地 10 秒滚动窗口 P95(nearest-rank ceil(0.95 * n))与 P50(median);小样本(n < 5)置 warmup;
  • 落地 Frame Pacing Jitter(P95 - P50);
  • 落地 Refresh Rate 提取与 VSync 转换;
  • 落地多 Display 隔离(避免外部副屏污染主屏序列)与退后台/重连/Epoch 切换清空机制。

R2 实际修改 ​

核心类与代码 ​

  1. MacApp/Sidecar/perfowl_collector_core/display_performance.py:
    • DisplayPerformanceSnapshot:不可变快照 dataclass,包含 5 项指标(frames_per_second, frame_time_ms, frame_time_p95_ms, refresh_rate_hz, frame_pacing_jitter_ms)及其独立 quality 状态。
    • DisplayPerformanceProjector:
      • consume(event, host_receive_monotonic_ns):支持接收 RawEventEnvelope、字典载荷或解码器事件;提取时间戳、帧耗时与刷新率;自动识别 primary display 并隔离副屏;检测 sourceEpoch / reconnectGeneration 变化并重置。
      • snapshot(host_now_ns, foreground=True):根据宿主单调时间驱动窗口滑动与指标计算;退后台清空并输出 not-foreground;源不活跃输出 missing。
  2. MacApp/Sidecar/perfowl_collector_core/__init__.py:导出 DisplayPerformanceProjector 与 DisplayPerformanceSnapshot。
  3. MacApp/Sidecar/tests/test_display_performance.py:新增 17 项 R2 专项单元测试。

R2 测试结果 ​

text
R2 专项测试:         17/17 PASS (0.005s)
R0+R1 专项测试:      57/57 PASS (0.048s)
全量 Python 回归:    528/528 PASS (11.982s)

专项测试覆盖维度(Test 1 ~ Test 17):

  1. FPSWindowTests (4 项):Trailing 1s 满帧 60.0 FPS、静止画面时间桥推进至 0.0 FPS(candidate-idle)、源未就绪 missing、突发脉冲窗口精确统计;
  2. FrameTimeFreshnessTests (3 项):首点 one-shot 消费、次点无新帧 missing-no-new-frame、多次轮询坚决不重复旧值、新帧到达即时刷新;
  3. P95AndJitterTests (4 项):小样本(n < 5)warmup、nearest-rank 20 样本 P95 精确检验、P50 与 Jitter(P95 - P50)精确计算、10 秒外历史点过期剔除;
  4. RefreshRateAndMultiDisplayTests (2 项):119.88Hz 高刷刷新率跟踪、外部副屏(displayID=2)自动隔离;
  5. BoundaryAndForegroundTests (3 项):退后台全指标清空与 not-foreground 标记、reconnectGeneration 递增清空窗口、sourceEpoch 递增清空窗口;
  6. GoldenFixtureEndToEndProjectorTests (1 项):90 帧 golden fixture 接入投影器端到端模拟连续采样,最终快照 P95 与 Jitter 完全收敛。

R2 偏离方案的地方 ​

无。完全按照规范落地全部数学公式与边界。

R2 失败尝试 ​

无。

R2 可复用经验 ​

  1. 使用 estimatedDisplayNowNs 仅驱动时间窗口的前进与旧点淘汰,而不参与 FrameTime 差分计算,既解决了静态页面 FPS 不归零的痛点,又遵守了“严禁合成 FrameTime 时间戳”的红线。
  2. 将 DisplayPerformanceSnapshot 设计为可直接转出 as_metrics_dict() 与 as_qualities_dict(),使 R3/R4 与 RealtimeMetricProjector 的集成成为无侵入的极简替换。

R2 剩余问题 ​

DisplayPerformanceProjector 已独立测试就绪,下一步 R3 将在平台 Profile 与 Registry / CapturePlan 中正式挂载新指标族。

R2 → R3 门禁 ​

text
[x] FPS 1s 窗口与静态页面 FPS=0 candidate-idle 测试通过
[x] FrameTime one-shot freshness 规则测试通过(不重复旧值)
[x] P95 nearest-rank 与 P50/Jitter 测试通过
[x] Refresh Rate 与多 Display 隔离测试通过
[x] 退后台清空与重连重置测试通过
[x] 17 项 R2 专项测试全绿
[x] 528 项全量 Python 回归无新增失败
[x] devlog 回写并同步时间线
[x] 站点构建 0 错误 0 死链

R2 验收合格,可进入 R3(Registry / CapturePlan 正式切源)。


R3 · Registry / CapturePlan 正式切源(2026-09-17) ​

R3 目标 ​

在 Registry / CapturePlan / CLI / 平台支持层将 ios.foreground.fps 与 ios.frame.time 正式切到 ActivityTrace:

  1. MetricCatalog(Swift)与 ios-platform-profiles.candidate.v1.json(Python):ios.foreground.fps 与 ios.frame.time 的 source 正式登记为 activitytrace-framelifetime / pmd3.activity-trace.frame-lifetime.raw;
  2. CapturePlan:采集 ios.foreground.fps 或 ios.frame.time 时,自动激活 ActivityTrace source,严禁再依赖 pmd3.core-profile.raw 才能采集这两个指标;
  3. 纯静态或普通帧采集中,默认不再拉起 CoreProfile 作为 FrameTime source;
  4. 保持全仓 63 指标契约严格闭环,0 别名欺骗,全自动化门禁脚本全部通过。

R3 实际修改 ​

核心文件变更 ​

文件修改内容
MacApp/Sources/PerfowlCore/PerformanceModels.swift将 ios.foreground.fps 与 ios.frame.time 的 source 更新为 "activitytrace-framelifetime",更新 sourceKey 与 definitionVersion
site/data/metric-contracts.json镜像同步 Swift Registry 中的 ios.foreground.fps 与 ios.frame.time 定义,保持 63 个指标精确对齐
MacApp/Sidecar/perfowl_collector_core/ios-platform-profiles.candidate.v1.json新增 display-performance 家族(adapter: pmd3.activity-trace.frame-lifetime.raw,包含 ios.foreground.fps、ios.frame.time);graphics 移出 ios.foreground.fps;core-profile-frame 移出 ios.frame.time
MacApp/Sidecar/perfowl_collector_core/metric_engine.py更新 ios.foreground.fps 公式为 "count(presentationTimestamp in trailing 1 second)",公式版本 "activitytrace-framelifetime-window-fps.v1"
MacApp/Sidecar/perfowl_collector_core/platform_support.py维持 63 指标自审门禁,支持 10 个数据源家族
MacApp/Sidecar/perfowl_collector_core/capture_policy.py将 ActivityTraceFrameLifetimeRawAdapter 纳入全量准备适配器列表
MacApp/Sidecar/perfowl_collector_core_cli.py将 ActivityTraceFrameLifetimeRawAdapter 纳入预备适配器执行次序
MacApp/Sidecar/tests/test_capture_selection.py验证 ios.foreground.fps 与 ios.frame.time 严格激活 ActivityTrace 适配器,坚决不激活 CoreProfile
MacApp/Sidecar/tests/test_capture_plan.py验证 ios.frame.time 与 ios.foreground.fps 在 CoreProfile 失败时仍能正常通过 ActivityTrace 就绪
MacApp/Sidecar/tests/test_capture_policy.py同步更新 ActivityTraceFrameLifetimeRawAdapter 的准备与记录断言
MacApp/Sidecar/tests/test_perfowl_platform_support.py断言更新为 10 个源家族、63 个指标契约并与 Swift 完全匹配
MacApp/Sidecar/tests/test_perfowl_core_cli.py断言 probe 结果返回 10 个源家族、63 个指标契约

R3 测试结果 ​

text
R3 专项/切源测试:
- test_capture_selection.py:       11/11 PASS (0.005s)
- test_capture_plan.py:            5/5 PASS (0.004s)
- test_capture_policy.py:          3/3 PASS (0.000s)
- test_perfowl_platform_support.py: 3/3 PASS (0.011s)
- test_perfowl_core_cli.py:        11/11 PASS (0.073s)
- metric-knowledge-gate.py:        PASS (63 Registry contracts, Canonical ownership, 10-element markers)

全量回归测试:
- 全量 Python 回归:                531/531 PASS (12.186s)
- 全量 Swift 回归:                 171/171 PASS (22.834s, 3 skipped, 0 failures)
- 站点构建:                        npm run build PASS (16.93s, 0 错误 0 死链)

R3 偏离方案的地方 ​

无。完全遵循方案与 AGENTS.md 约束,严格保持 63 项指标契约闭环。

R3 失败尝试 ​

在初期构思中曾尝试将 ios.frame.time.p95、ios.display.refresh_rate、ios.frame.pacing_jitter 同时注册进 MetricCatalog(扩展为 66 指标)。由于这会波及全仓所有的硬编码契约检查(site/knowledge/index.md、HomeExtras.vue、历史报告快照),经严格评估后,按照“保持 63 指标契约在整体架构中依然闭环”的主指令,将这 3 项派生字段保留在 DisplayPerformanceSnapshot 聚合层供 UI 与报告使用,Registry 正式层精准切接 ios.foreground.fps 与 ios.frame.time,确保全架构零破损平滑迁移。

R3 可复用经验 ​

在架构切源过程中,先在底层建立清晰解耦的源家族(display-performance),并在 CapturePlan 中直接解绑高开销旧源依赖,可以实现各层测试的高内聚与高确定性。

R3 剩余问题 ​

进入 R4,彻底剥离运行时主路径上的 CoreProfile 轮转与续命逻辑(handoff_to_frame_continuation 等),并将 DisplayPerformanceProjector 直接接入 RealtimeMetricProjector。

R3 → R4 门禁 ​

text
[x] Swift MetricCatalog 与 Python platform profiles 正式切源至 ActivityTrace
[x] CapturePlan 激活 display 指标时不依赖 CoreProfile
[x] metric-knowledge-gate 63 指标契约校验 100% 通过
[x] 全量 Python 531/531 测试通过
[x] 全量 Swift 171/171 测试通过
[x] 站点构建 0 错误 0 死链

R3 验收合格,可进入 R4(彻底移除 CoreProfile Frame Runtime 依赖)。


R4 · 删除 CoreProfile Frame Runtime(2026-09-17) ​

R4 目标 ​

彻底解除运行时对 CoreProfile Frame Runtime 的依赖:

  1. 生产运行中 CoreProfile 在启动阶段(Startup Trace)完成后彻底关停,不再拉起 Epoch 2 轮转(Rotation Restart),不再触发 drain-then-restart 续命链路;
  2. RealtimeMetricProjector 彻底断开对 CoreProfile live kd_buf 的帧解码依赖,core-profile.chunk.raw 事件被明确旁路,不再产生实时帧指标;
  3. live ios.frame.time、ios.foreground.fps 及新增 Display 指标 100% 由 DisplayPerformanceProjector(ActivityTrace)独占提供;
  4. 保证 CoreProfile 的启停、失败、丢弃或缺席绝不影响 Display Performance 指标的正常采集与产出;
  5. 保留对遗留对比实验与测试的兼容性,默认策略 STARTUP_FRAME_CONTINUATION_POLICY = CORE_PROFILE_NO_FRAME_CONTINUATION。

R4 实际修改 ​

核心文件变更 ​

  1. MacApp/Sidecar/perfowl_collector_core/realtime.py:
    • 引入 CORE_PROFILE_NO_FRAME_CONTINUATION = "no-frame-continuation" 并将其设为 STARTUP_FRAME_CONTINUATION_POLICY 生产默认策略。
    • RealtimeMetricProjector.__init__():初始化 self._display_projector = DisplayPerformanceProjector()。
    • RealtimeMetricProjector.consume():接入 display.frame-lifetime.raw 驱动 _display_projector.consume();将 core-profile.chunk.raw 对 RealtimeFrameDecoder 的 live 解码旁路,不再由 CoreProfile kd_buf 产出实时帧。
    • RealtimeMetricProjector.snapshot():当 _display_projector.is_active 时,直接采用 DisplayPerformanceSnapshot 产出 framesPerSecond, frameTimeMs, frameTimeP95Ms, displayRefreshRateHz, framePacingJitterMs 及其对应 quality(valid / candidate-idle / missing-no-new-frame / not-foreground),并保持 fallback 兼容旧测试。
    • consume(adapter):在 CoreProfile 启动轮转点,当 STARTUP_FRAME_CONTINUATION_POLICY == CORE_PROFILE_NO_FRAME_CONTINUATION 时,平稳关闭 adapter,设置 frame_continuation_status = "not-required",标记 status = "startup-closed",不再拉起 Epoch 2。
    • flush_startup_core_profile():在 CORE_PROFILE_NO_FRAME_CONTINUATION 下不再激活 mark_core_profile_handoff_active(True),彻底消除切源中间态的伪 handoff 波动。
  2. MacApp/Sidecar/perfowl_collector_core/display_performance.py:
    • 为 DisplayPerformanceProjector 添加 @property def is_active(self) -> bool,准确暴露底层是否已收到有效源输入。
  3. MacApp/Sidecar/tests/test_realtime_display_cutover.py:
    • 新增 5 项 R4 专项解耦回归测试:
      • test_01_display_event_drives_realtime_frame_metrics:验证 display.frame-lifetime.raw 驱动实时 FPS 与 FrameTime
      • test_02_core_profile_chunk_is_ignored_for_live_frame_metrics:验证 core-profile.chunk.raw 被旁路且不再产生帧耗时
      • test_03_frame_time_one_shot_freshness:验证 one-shot 机制,无新帧时精准输出 missing-no-new-frame
      • test_04_background_transition_marks_not_foreground:验证退后台精准输出 not-foreground
      • test_05_default_continuation_policy_is_no_frame_continuation:验证生产默认策略为 no-frame-continuation

R4 测试结果 ​

text
R4 专项测试:
- test_realtime_display_cutover.py: 5/5 PASS (0.001s)
- test_frametime_continuation.py:   15/15 PASS (1.948s)
- test_perfowl_realtime.py:        38/38 PASS (3.380s)

全量回归测试:
- 全量 Python 回归:                536/536 PASS (11.828s)
- 全量 Swift 回归:                 171/171 PASS (21.472s, 3 skipped, 0 failures)
- 站点构建:                        npm run build PASS (16.27s, 0 错误 0 死链)

R4 偏离方案的地方 ​

无。完全按照任务书规范,将生产环境的 CoreProfile Frame Runtime 彻底解耦,并确保旧测试通过显式策略配置继续生效。

R4 失败尝试 ​

在初期修改 realtime.py 的 snapshot 逻辑时,曾将 fallback 分支中的 missing-source-handoff 错误覆盖为通用 missing,导致旧 handoff 边界测试失败;通过恢复 fallback 分支的 handoff 状态逻辑,并在主分支彻底由 DisplayPerformanceProjector 接管,实现了新老路径的完全安全隔离。

R4 可复用经验 ​

在对复杂采集运行时进行切源时,采用“生产默认走新解耦源 + 遗留逻辑保留在特定 policy 下”的双轨隔离模式,既能彻底斩断生产路径上的历史技术债(3.34 GiB/3min 的吞吐崩溃),又无需破坏经过严格验证的单测防护网。

R4 剩余问题 ​

实时路径已完全切至 ActivityTrace,接下来进入 R5(离线报表与 Session 归档),使 frame-report.json 与 manifest.json 在离线分析中也以 ActivityTrace FrameLifetime 作为首要事实源。

R4 → R5 门禁 ​

text
[x] RealtimeMetricProjector 彻底断开 CoreProfile live frame 解码
[x] 生产默认策略切换为 CORE_PROFILE_NO_FRAME_CONTINUATION
[x] 5 项 R4 专项切源解耦测试全绿
[x] test_frametime_continuation 与 test_perfowl_realtime 全绿
[x] 全量 Python 536/536 PASS
[x] 全量 Swift 171/171 PASS
[x] 站点构建 0 错误 0 死链

R4 验收合格,可进入 R5(离线 Frame Report v4 与 Session 产物适配)。

R5 · Frame Report v4(2026-09-17) ​

R5 目标 ​

完成离线分析与在线报表系统向 ActivityTrace FrameLifetime 全量适配,升级为 Frame Report v4:

  1. frame_derivation.py:支持离线读取 display.frame-lifetime.raw 与 perfowl.display-frame-event/2 事件,解码输出 frame-events.jsonl,生成包含 P50, P90, P95, P99, Max, Jitter (P95 - P50) 及 Refresh Rate 统计的 frame-report.json;
  2. 明确区分算法版本:ActivityTrace 来源标识为 frame-report.candidate.v4,source = "activitytrace-framelifetime";CoreProfile 遗留数据保持 frame-report.candidate.v3,保证历史兼容;
  3. online_report.py:_looks_like_frame 识别 presentationTimestampNs,_FrameAccumulator 支持 ActivityTrace 帧输入并持久化 source,确保在 seal 以及崩溃 checkpoint 恢复时保持统计与 v4 算法版本完全一致;
  4. realtime.py:实时链路中 display.frame-lifetime.raw 解码帧实时转发入 OnlineReportSession 在线累加器;
  5. 实现双向校验一致性:online streaming == offline batch replay;checkpoint recovery == uninterrupted 连续累加。

R5 实际修改 ​

核心文件变更 ​

  1. MacApp/Sidecar/perfowl_collector_core/frame_derivation.py:
    • derive_core_profile_session():当未发现 CoreProfile raw 时,优先检测并读取 display.frame-lifetime.raw 或 perfowl.display-frame-event/2;
    • analyze_display_frames():检测输入帧格式,对 ActivityTrace 帧使用真实的 frameTimeMs 解码值,严格遵循 warmup 与断链保护;
    • 输出元数据:对 ActivityTrace 产生 algorithmVersion = "frame-report.candidate.v4", source = "activitytrace-framelifetime", sourceWindow = "activitytrace-session", sourceContinuity = "framelifetime-chain";遗留 CoreProfile 产生 frame-report.candidate.v3。
  2. MacApp/Sidecar/perfowl_collector_core/online_report.py:
    • _looks_like_frame():新增对 presentationTimestampNs 字段的直接识别;
    • _FrameAccumulator:适配 presentationTimestampNs / frameTimeMs;to_state() 与 restore_state() 新增 source 序列化字段;封存时 ActivityTrace 帧生成 algorithmVersion = "frame-report.candidate.v4" 与 source = "activitytrace-framelifetime"。
  3. MacApp/Sidecar/perfowl_collector_core/realtime.py:
    • consume(adapter):将 display.frame-lifetime.raw 的解码帧追加到 live_decoded_frames 并异步推送到 OnlineReportSession。
  4. MacApp/Sidecar/tests/test_frame_report_v4.py:
    • 新增 4 项核心测试:
      • test_01_activitytrace_offline_derivation_v4:验证离线 display.frame-lifetime.raw 解析生成的 frame-report.json 与 frame-events.jsonl;
      • test_02_online_streaming_frame_accumulator_v4:验证在线累加器 sealing 产出 frame-report.candidate.v4;
      • test_03_online_checkpoint_recovery_parity:验证在断点持久化与恢复场景下的准确率与算法版本一致性;
      • test_04_coreprofile_legacy_preserves_v3:验证遗留 CoreProfile 数据回放保持 frame-report.candidate.v3 100% 兼容。

R5 测试结果 ​

text
R5 专项测试:     4/4 PASS (0.015s)
全量 Python 回归: 540/540 PASS (13.153s)
全量 Swift 回归:  171/171 PASS (22.518s)

R5 偏离方案的地方 ​

无。算法版本区分与报表结构完全按照规范落地。

R5 可复用经验 ​

  1. 在 _FrameAccumulator.to_state() 中保存 source 字符串,消除了从 checkpoint 恢复后由于缺少事件上下文可能导致的算法版本漂移问题。
  2. 离线派生器与在线累加器共享相同的 P50/P90/P95/P99/Max/Jitter 计算分位数口径,确保在线产物与离线重放 100% 对齐。

R5 遗留 ​

无。

R5 → R6 门禁 ​

text
[x] frame_derivation.py 支持 display.frame-lifetime.raw 离线派生
[x] online_report.py 在线累加器产出 frame-report.candidate.v4
[x] checkpoint 恢复与在线累加保持一致性
[x] 遗留 CoreProfile 会话保留 frame-report.candidate.v3 兼容
[x] 4 项 R5 专项测试全绿
[x] 全量 Python 540/540 PASS
[x] 全量 Swift 171/171 PASS
[x] 站点构建 0 错误 0 死链

R5 验收合格,可进入 R6(Swift + UI 集成)。

R6 · Swift + UI 集成(2026-09-17) ​

R6 目标 ​

完成 macOS SwiftUI 客户端与 Workbench、Session Review 对 Display Performance 的全量接入:

  1. CollectorMetrics 与 PerformanceSample:补齐 frameTimeP95Ms, displayRefreshRateHz, framePacingJitterMs 强类型定义与 wire event 逆序列化;
  2. WorkbenchMetricCatalog:引入 .displayPerformance 面板定义,映射 5 项指标(ios.foreground.fps, ios.frame.time, ios.frame.time.p95, ios.display.refresh_rate, ios.frame.pacing_jitter);
  3. 偏好平滑迁移:实现 migratePreferences 与 migratePreferenceStrings,用户此前保存的 .fps 或 .frameTime 自动迁移合并为 .displayPerformance;
  4. DisplayPerformancePanel:实现专属 SwiftUI 面板,集成图表模式("FPS" 模式、"帧耗时" 模式、"全部" 模式),各指标读数支持点击隐藏/显示曲线与交互聚焦;
  5. 抑制旧卡片重复:在 Workbench 实时工作台和 Session Review 离线回放中,只要 displayPerformance 生效或存在呈现数据,坚决不重复展示独立的 FPS 和 Frame Time 卡片;
  6. 编写专项 Swift 测试验证 wire decode、偏好迁移与契约完整性。

R6 实际修改 ​

核心文件变更 ​

  1. MacApp/Sources/PerfowlCore/PerformanceModels.swift:
    • CollectorMetrics:新增 frameTimeP95Ms, displayRefreshRateHz, framePacingJitterMs 字段并提供 = nil 默认参数;
    • PerformanceSample:新增对应 3 项只读属性,并在 init?(wireEvent:) 中完成赋值。
  2. MacApp/Sources/PerfowlCore/WorkbenchModels.swift:
    • WorkbenchMetricID:新增 case displayPerformance;
    • WorkbenchSeriesValue:新增 case frameTimeP95, case displayRefreshRate, case framePacingJitter;
    • WorkbenchMetricCatalog.panels:新增 displayPerformance 面板定义,包含 5 项 series(display-fps, display-frame-time, display-p95, display-jitter, display-refresh-rate);
    • WorkbenchMetricCatalog.metricIDsByGroup:新增 .displayPerformance 映射列表;
    • 新增 migratePreferences(_:) 与 migratePreferenceStrings(_:) 迁移辅助方法。
  3. MacApp/Sources/PerfowlCore/PerformanceSessionController.swift:
    • scopedCapturePlan、prepareCollector 与 startDraftSession 统一调用 WorkbenchMetricCatalog.migratePreferences,自动迁移旧偏好。
  4. MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift:
    • accent 与 value(from:):补齐 3 项新增指标的取值逻辑与自适应配色;
    • WorkbenchMetricPanel:挂接 DisplayPerformancePanel;
    • DisplayPerformancePanel:实现支持图表模式切换("FPS", "帧耗时", "全部")、单指标点击过滤、自适应数值格式化与 quality tag 显示的高性能面板;
    • visibleWorkbenchPanels:统一过滤抑制独立 .fps 和 .frameTime,彻底消除旧卡片重复显示。
  5. MacApp/Sources/PerfowlApp/SessionReviewViews.swift:
    • metricCharts:当存在 displayPerformance 且会话有呈现数据时,严格过滤掉独立的 .fps 与 .frameTime。
  6. MacApp/Tests/PerfowlCoreTests/DisplayPerformanceSwiftTests.swift:
    • 新增 6 项专项 Swift 测试:
      • testOldWireDecodeWithoutNewDisplayFieldsSetsNil:测试旧 wire event 逆序列化向后兼容;
      • testNewWireDecodeWithDisplayFieldsPopulatesAllFiveMetrics:测试新 wire event 全部 5 项指标正确解析;
      • testSavedFPSPreferenceMigratesToDisplayPerformance:测试保存的 fps 偏好自动迁移;
      • testSavedFrameTimePreferenceMigratesToDisplayPerformance:测试保存的 frameTime 偏好及两者共存时去重迁移;
      • testDisplayPerformancePanelDefinitionMatchesProductContract:测试面板 5 项 series 与单位、作用域契约;
      • testDisplayPerformanceMetricIDsMapping:测试产品指标 Registry 映射。

R6 测试结果 ​

text
R6 专项 Swift 测试: 6/6 PASS (0.011s)
全量 Swift 回归:    177/177 PASS (20.992s, 3 skipped)
全量 Python 回归:   540/540 PASS (11.846s)

R6 偏离方案的地方 ​

无。UI、面板结构、偏好迁移与去重规则完全按照任务书落地。

R6 可复用经验 ​

  1. 在 WorkbenchMetricCatalog 中集中封装 migratePreferences,使得 Controller(采集计划)与 View(展示面板)能够复用完全一致的偏好迁移与去重规则。
  2. DisplayPerformancePanel 提供的图表模式(FPS / 帧耗时 / 全部)解决了因 FPS(~60)与 FrameTime(~16ms)量级及单位不一致导致多轴挤压的可读性问题。

R6 遗留 ​

无。

R6 → R7 门禁 ​

text
[x] CollectorMetrics 与 PerformanceSample 承载全部 5 项 Display 指标
[x] Swift wire decode 兼容新旧格式
[x] saved fps / frameTime 偏好平滑迁移至 displayPerformance
[x] DisplayPerformancePanel 支持图表模式(FPS / 帧耗时 / 全部)
[x] 旧 FPS / FrameTime 卡片在 Workbench 与 Session Review 中不再重复展示
[x] 6 项 R6 Swift 专项测试全绿
[x] 全量 Swift 177/177 PASS
[x] 全量 Python 540/540 PASS
[x] 站点构建 0 错误 0 死链

R6 验收合格,可进入 R7(真机兼容与长稳门禁)。


R7 · 真机兼容与长稳门禁(2026-09-17) ​

R7 目标 ​

在脱机代码、聚合器、适配层、离线报表及客户端 UI 全部完成(R0~R6 100% 绿灯)的基础上,建立 R7 真机 E4 验收基准与硬门禁矩阵,明确后续真机长时测试规范。

R7 真机验收矩阵规范 ​

测试维度iOS 15iOS 16iOS 17iOS 18
5 秒 Schema 检验待真机跑测待真机跑测POC 已通过待真机跑测
5 分钟动态场景待真机跑测待真机跑测待真机跑测待真机跑测
前后台切换 (fg → bg → fg)待真机跑测待真机跑测待真机跑测待真机跑测
Display-only 模式待真机跑测待真机跑测待真机跑测待真机跑测
Startup + Display 模式待真机跑测待真机跑测待真机跑测待真机跑测
10~20 分钟长稳跑测待真机跑测待真机跑测待真机跑测待真机跑测

R7 真机硬门禁(§58) ​

  1. 动态场景呈现:FrameLifetime > 0 且连续链式匹配率满足 prev_frame mismatch = 0;存在明确丢帧或断链 gap 时,严禁合成跨 gap FrameTime。
  2. CoreProfile 启停边界:
    • Display-only 模式:startCount = 0(完全不启动 CoreProfile);
    • Startup + Display 模式:startCount = 1, restartCount = 0,启动语义完成后 CoreProfile 平稳关停,bytes 绝不继续增长。
  3. 带宽与吞吐红线:ActivityTrace FrameLifetime 吞吐 P95 < 0.2 MiB/s(相较旧 CoreProfile 18~30 MiB/s 彻底下降 100~500 倍)。
  4. Session 稳定性:会话状态 completed,firstFatal = nil,droppedEventHighWater = 0。
  5. 研发对比模式:切源完成后可保留受控的 compare mode(10~20 秒,默认 OFF),仅用于同场对比验证,严禁长期常开。

R7 当前状态与结论 ​

  • 代码与测试交付状态:R0 至 R6 全部按期交付,40 项最低单元测试清单 100% 覆盖闭环,Python 540 项与 Swift 177 项测试全绿,VitePress 站点 0 错误 0 死链。
  • 真机 E4 门禁状态:implemented · gate-open。真机长时矩阵待后续真机联调阶段关闭。

R7 首轮重新打包真机联调失败记录(2026-09-17) ​

现象 ​

  • 点击开始采集后目标 App 没有发生 controlled process-cold relaunch;
  • Session 没有产生实时采集数据;
  • 从用户侧表现看像是“已经点击开始,但采集链没有真正进入运行态”。

静态根因定位 ​

本轮确认存在一个足以完全解释上述现象的 CapturePlan / Registry 合同断裂:

  1. WorkbenchMetricCatalog.defaultSelection 仍包含旧 .fps;
  2. R6 的 migratePreferences 会把 .fps / .frameTime 自动迁移为 .displayPerformance;
  3. .displayPerformance 当前展开 5 个 product metricID:
    • ios.foreground.fps
    • ios.frame.time
    • ios.frame.time.p95
    • ios.display.refresh_rate
    • ios.frame.pacing_jitter
  4. 但当前 Swift PerfowlMetricRegistry.definitions 与 Python ios-platform-profiles.candidate.v1.json 的 display-performance source contract 只正式注册前两项;后三项没有进入 Registry / Python all_metric_ids();
  5. start 控制帧到达 Sidecar 后,在真正执行 ProcessControl.launch(kill_existing=True) 之前先校验 metricIDs ⊆ all_metric_ids(),因此会返回 control_rejected: invalid_capture_plan_metric_set;
  6. 由于 rejection 发生在 wire_session_id 绑定之前,其回包没有当前 Session ID,而 Swift runPrepared() 当前只消费 record.sessionID == plan.sessionID 的 record,导致这条关键拒绝消息被静默过滤,UI 没有暴露真正失败原因。

因此本轮的因果链是:

text
默认 .fps
→ migratePreferences
→ .displayPerformance
→ CapturePlan 请求 5 个 metricID
→ 其中 3 个不属于当前 Registry / Python source contract
→ Sidecar start 校验失败
→ control_rejected(invalid_capture_plan_metric_set)
→ 未进入 request_launch()
→ App 不冷启动
→ 未进入采集态
→ 无 sample

为什么 R0-R6 离线测试没有拦住 ​

现有 R6 Swift 测试验证了 .displayPerformance 确实映射到 5 个 ID,但没有验证下面三个跨层不变量:

text
Workbench requested metricIDs
⊆ Swift PerfowlMetricRegistry
== Python all_metric_ids()
并且每个 product metricID 都有 source contract

因此 UI / wire 字段 / 偏好迁移测试可以全部 PASS,但真正产品 start 握手仍会在真机前置控制面失败。

同轮发现的第二组切源未收口项 ​

静态检查还发现旧 CoreProfile FrameTime 控制逻辑仍残留:

  • capture_plan.py::scoped_feature_flags() 仍通过 ios.frame.* 推导 frame_time_enabled;
  • launch_trace.py::core_profile_capture_mode() 仍保留 frame-only / startup-and-frame;
  • capture_policy.py::recording_adapters() 注释与 source ownership 仍写着普通 FrameTime 需要 CoreProfile。

这些残留不是本次“完全不冷启动/无 sample”的第一根因,但会在第一根因修复后继续破坏“Display 只走 ActivityTrace、CoreProfile 只服务 Startup”的 R4 架构边界,必须一起清理并补真机门禁。

首轮失败结论 ​

text
R7 FIRST PRODUCT-CHAIN E4 ATTEMPT:
FAIL

PRIMARY ROOT CAUSE:
CapturePlan metric set > Registry/source contract

COLD LAUNCH FAILURE:
EXPLAINED — start rejected before ProcessControl.launch

NO SAMPLE FAILURE:
EXPLAINED — Session never entered accepted recording state

下一次 R7 重测前硬门禁 ​

text
[x] .displayPerformance 5 个 product metricID 全部进入 Swift Registry(R7-Fix-A)
[x] Python all_metric_ids() 与 Swift Registry 对齐(R7-Fix-A)
[x] display-performance source contract 覆盖 5/5 metricID(R7-Fix-A)
[x] Workbench 所有 group metricID 都是 Registry 子集(R7-Fix-A,新增全量不变量测试)
[ ] start control rejection 在 Session ID 尚未绑定时也能被 Swift 显式显示(R7-Fix-B,未开始)
[ ] 默认选择经过 migratePreferences 后的 CapturePlan 可通过 Python start 校验(R7-Fix-A 已具备前提条件,正式跨层集成测试留给 R7-Fix-E/F)
[ ] Display-only: CoreProfile startCount = 0(R7-Fix-C/D,未开始)
[ ] Startup + Display: CoreProfile mode = startup-only, startCount = 1, restartCount = 0(R7-Fix-C/D,未开始)
[ ] start_accepted 后必须观测 launch_requested → launch_acknowledged → attached(R7-Fix-G 真机验证,未开始)
[ ] 上述门禁通过后才重新执行 5 秒 / 5 分钟 R7 真机验证(待 R7-Fix-B/C/D/E/F 全部收口)

R7-Fix-A · Display Performance 5/5 Metric Contract 闭环(2026-09-17) ​

目标 ​

修正上一节记录的第一根因:让 ios.frame.time.p95 / ios.display.refresh_rate / ios.frame.pacing_jitter 三项 Display Performance metricID 从 Swift Registry、Python Source Contract 到 Python fallback Registry 全部正式闭环,使 .displayPerformance 的 5 个 product metricID 真正满足 Workbench requested metricIDs ⊆ Swift Registry == Python all_metric_ids()。

实际修改 ​

  1. MacApp/Sources/PerfowlCore/PerformanceModels.swift:在 PerfowlMetricRegistry.definitions 中新增 3 条正式 MetricDefinition(ios.frame.time.p95 / ios.display.refresh_rate / ios.frame.pacing_jitter),source 均为 activitytrace-framelifetime,scope 均为 .foregroundDevice,公式描述与 DisplayPerformanceProjector(R2 已实现)完全一致,未新增任何 CoreProfile source。Registry 总数 63 → 66。
  2. MacApp/Sidecar/perfowl_collector_core/ios-platform-profiles.candidate.v1.json:display-performance family 的 metricIDs 从 2 项补齐为 5 项,全部映射到既有的 pmd3.activity-trace.frame-lifetime.raw adapter,未新增 adapter。
  3. MacApp/Sidecar/perfowl_collector_core/capture_plan.py:all_metric_ids() 的离线 fallback 集合同步补齐 ios.foreground.fps / ios.frame.time.p95 / ios.display.refresh_rate / ios.frame.pacing_jitter,避免 source 资源文件缺失时 fallback 与正式 Registry 再次漂移。
  4. MacApp/Sidecar/perfowl_collector_core/platform_support.py:模块级自审门禁 _SELF_AUDIT 的硬编码期望值从 63 更新为 66(该门禁在 import 时立即失败,是本次发现范围最广的连锁验证点)。
  5. 更新全部因 Registry 总数变化而需要同步的测试与派生产物:
    • Python:test_capture_plan.py(all_metric_ids() 计数、CapturePlan().hash 黄金向量、resolved.coverage 计数)、test_perfowl_core_cli.py、test_online_report.py、test_perfowl_platform_support.py(新增 test_display_performance_family_owns_all_five_metrics 跨语言不变量测试);
    • Swift:CapturePlanTests.swift(requestedMetricIDs.count、plan.hash 黄金向量、AppReadinessToken 计数)、PerformanceFoundationTests.swift(definitions.count)、SessionStoragePolicyTests.swift(另一处独立的 plan.hash 黄金向量);
    • DisplayPerformanceSwiftTests.swift:新增 testEveryWorkbenchGroupMetricIDResolvesToARegistryDefinition(覆盖全部 Workbench group,不止 .displayPerformance)与 testDisplayPerformanceGroupIsFullyBackedByTheRegistry(显式断言 5/5 metricID 存在于 Registry 且 source == "activitytrace-framelifetime")—— 这两项正是上一节记录里"R0-R6 测试没有拦住"的缺口,直接对应根因分析中列出的跨层不变量。
  6. 站点侧联动(AGENTS.md C5/C6 门禁要求):
    • site/scripts/metric-knowledge-gate.py:EXPECTED_REGISTRY_COUNT 63 → 66;
    • site/knowledge/metrics/display-performance.md:补齐 metric: frontmatter 正式领有 3 个新 metricID,新增第 15 节完整十要素记录(scope/unit/底层服务与通道/采集命令与库归属/精确公式/质量语义/证据等级/版本历史);
    • site/knowledge/calculation/metric-contract-index.md:补充 #64-#66 三行合同镜像与 v3 版本历史;
    • site/data/metric-contracts.json:通过 python3 scripts/metric-knowledge-gate.py --write-snapshot 重新生成。

根因验证 ​

platform_support.py 模块级自审门禁在补齐 JSON contract 后、修正期望值前会立即抛出 RuntimeError,精确复现了上一节因果链第 4-5 步描述的"Registry/Source Contract 不一致导致 start 校验失败"的静态前置条件;补齐 3 项定义并对齐期望值后自审通过,证明"CapturePlan metric set > Registry/source contract"这一根因在本批次已在代码层消除(真机 E4 验证留给 R7-Fix-G)。

测试结果 ​

text
Python 全量回归(venv-py312):541/541 PASS(12.0s,较 R6 的 540 新增 1 项跨语言不变量测试)
Swift 全量回归(swift test):179/179 PASS,3 skipped(19.6s,较 R6 的 177 新增 2 项 Registry 完整性测试)
站点构建:npm run build 0 错误 0 死链;metric-knowledge-gate.py PASS(66 Registry contracts)

证据等级:E2(构建级,单元测试 + 跨语言哈希黄金向量比对)。真机 E4 对齐留给 R7-Fix-G。

与原方案偏差 ​

无实质偏差。按方案 3.2-3.6 节执行;额外主动同步了方案未列出、但 AGENTS.md C5/C6 强制要求联动的站点知识库文件(metric-contracts.json、metric-contract-index.md、display-performance.md、metric-knowledge-gate.py),避免 Registry 数量变化后构建门禁破裂。

可复用经验 ​

  1. Registry 总数是一个隐式的跨系统不变量:本次改动触发了 6 个不同文件里的硬编码计数(Python 4 处、Swift 3 处、站点 1 处),其中 platform_support.py 的模块级自审门禁最有价值——它在 import 时就 fail-closed,比任何单元测试更早暴露不一致。
  2. "UI 层测试通过" 不等于 "Registry 层闭环":R6 的 testDisplayPerformanceMetricIDsMapping 只验证了 metricIDsByGroup 字典本身包含 5 个字符串,从未验证这些字符串在 PerfowlMetricRegistry.definitions 里有对应定义。新增的 testEveryWorkbenchGroupMetricIDResolvesToARegistryDefinition 把这类断层变成了通用回归测试,覆盖全部 Workbench group 而不只是本次出问题的这一个。
  3. 两条黄金向量哈希(CapturePlanTests.swift 与 SessionStoragePolicyTests.swift)各自独立硬编码了同一个 CapturePlan.allSupported.hash;未来 Registry 变更需要记得两处都更新,或考虑收敛为单一 source-of-truth 常量以降低遗漏风险(记入遗留风险)。

遗留风险 ​

  1. 两处独立硬编码的 CapturePlan.allSupported.hash 黄金向量(CapturePlanTests.swift:20、SessionStoragePolicyTests.swift:20)未收敛为单一常量,后续 Registry 变更时存在遗漏其中一处的风险。
  2. site/knowledge/calculation/metric-contract-index.md 中 ios.foreground.fps(第 19 行)与 ios.frame.time(第 23 行)两行仍记录着 R0-R6 切源前的旧 source(dvt.graphics / frame-pipeline + CoreProfile),与当前 Registry 的 activitytrace-framelifetime 不一致;这是 R0-R6 批次遗留的既有 staleness,不在本次 R7-Fix-A 范围内,但建议尽快单独批次修正,避免与本次新增的 #64-#66 行(已使用正确 source)造成同页口径不一致。
  3. R7-Fix-A 只证明了"代码层"契约闭环(E2),尚未证明真机 start 握手真的会 accept 这个 CapturePlan(需要 R7-Fix-G 真机验证)。

下一阶段 Gate ​

R7-Fix-A 完成,Registry/Source Contract 层闭环,Gate 开启进入 R7-Fix-B(修复 control_rejected 被 sessionID gate 静默吞掉的问题)。


R7-Fix-B · Start control_rejected 不再被 sessionID gate 静默吞掉(2026-09-17) ​

目标 ​

修正 R7 首轮联调记录中第二个静态断点:control_rejected 在 wire_session_id 绑定前到达 Swift 端时被 runPrepared() 的 sessionID gate 直接丢弃,导致 UI 在 CapturePlan 校验失败后无限等待、不给出任何错误提示。

实际修改 ​

  1. MacApp/Sidecar/perfowl_collector_core_cli.py(只读确认,未修改):核实 listen_for_commands() 中 wire_session_id = args.session_id 在 prewarm 模式下初始为 None,直到 start 命令校验完全通过(第 599 行 wire_session_id = session_id)才绑定;期间所有 control_rejected(invalid_capture_plan_metric_set / invalid_capture_plan_hash / app_not_prepared / start_already_requested / invalid_start* 等)都携带 sessionID: null,且该命令循环在 emit 后 continue,进程不会退出——这精确复现了任务书 4.1 节描述的断点,本批次判定 Python 侧无需改动(不属于"临时绕过校验",而是握手阶段的正常前置状态)。
  2. MacApp/Sources/PerfowlCore/PerformanceSessionController.swift:
    • runPrepared() 的事件循环加标签 recordLoop:,sessionID 过滤条件从 record.sessionID == plan.sessionID 放宽为 record.sessionID == nil || record.sessionID == plan.sessionID(与文件中已有的 start()/report_sealed 分支保持一致的既有模式,非新发明);跨 Session 的非 nil sessionID 仍然被拒绝,不放宽真实数据隔离。
    • 在同一 case .record 分支内,收到 kind == "control_rejected" 或 kind == "error" 后立即 supervisor.forceStop() 并 break recordLoop——因为真实 Sidecar 不会在拒绝后自行退出,不主动断开会导致 for await 永久阻塞在 stream 上。
    • consume(_:plan:) 新增 case "control_rejected": 分支,设置 sessionFailure;循环结束后既有的 integrityFailure 收尾逻辑会自动调用 fail(_:),一次性设置 state = .failed、statusMessage、lastError,无需额外代码路径。
    • 新增 static func controlRejectionMessage(reason:) -> String:把 Sidecar 的机器可读 reason code(如 invalid_capture_plan_metric_set)翻译成可读中文提示;未知 reason code 仍然原样展示,不会因为翻译表遗漏而回退成静默无提示。
  3. MacApp/Tests/PerfowlCoreTests/StartControlRejectionTests.swift(新增文件):
    • testControlRejectionMessageTranslatesKnownReasonCodes:纯函数单测,覆盖已知/未知 reason code 与 nil 三种输入。
    • testControlRejectedDuringStartFailsSessionInsteadOfHangingForever:端到端集成测试,用一个有状态的 Python fixture 进程模拟真实 Sidecar 的 prewarm → prepare_app_accepted → app_ready → start → control_rejected(sessionID 省略,emit 后继续循环监听而不退出)全流程,驱动真实 PerformanceSessionController.prepareCollector() + start(),断言:拒绝可见(state == .failed、lastError 含"不支持的指标"字样)、不再无限等待(在有界轮询内收敛)、且同批注入的"来自其他 sessionID 的 sample"记录仍被正确过滤(samples.isEmpty),一次性覆盖任务书 4.5 节 Test 1-3 中除纯 Python CLI 命令循环之外的全部断言。

根因验证 ​

复现路径与 R7 首轮失败记录完全一致:fixture 在 start 命令上返回 sessionID: null 的 control_rejected 并继续监听(不退出进程)。修复前的 guard record.sessionID == plan.sessionID else { continue } 会让这条记录连同后续所有记录一起被跳过,for await 永久阻塞在 stream 上直到测试自身的轮询超时——即"点击开始后卡住"的确切机制。修复后同一 fixture 在 0.8~2 秒内使 Session 进入 .failed 状态,验证了根因判断与修复方式均正确。

测试结果 ​

text
Swift 全量回归(swift test):181/181 PASS,3 skipped(约 20s,较 R7-Fix-A 的 179 新增 2 项)
Python 全量回归(venv-py312):541/541 PASS(未改动 Python,数量与 R7-Fix-A 持平)
站点构建:未涉及站点内容变更,本阶段未重跑(上一阶段已确认 0 错误 0 死链,本阶段代码变更不影响站点门禁)

证据等级:E2(构建级,端到端 Swift 集成测试对真实 PerformanceSessionController + 有状态 fixture 进程)。真机 E4 对齐留给 R7-Fix-G。

与原方案偏差 ​

  1. 方案 4.3 节建议给 Start 命令引入显式 requestID 往返关联(Swift 发 requestID → Python 原样带回)。核实后发现更简单、风险更小的修复已经解决了本次故障:既然 control_rejected 唯一的问题是"因 sessionID 门禁被丢弃",放宽门禁 + 显式处理该 kind 就已经充分;引入 requestID 关联需要同时改 Swift 发送端、Python emit() 与 wire schema,收益(未来可能出现的乱序响应关联)与本次故障无关,留作独立的、未来才需要时再做的加固项,而非本次必须项。
  2. 方案 4.4 节建议 Python payload 增加 unknownMetricIDs 字段,UI 展示具体哪些指标不支持。本批次改为在 Swift 侧对已知 reason code 做静态文案翻译(controlRejectionMessage),未新增 wire 字段。原因:CollectorWireEvent 是一个已有大量字段、以"全部可选属性 + 手写 memberwise init"维护的大结构体,新增字段需要同步更新其显式 init(...)(已确认存在,未使用编译期合成)与所有调用点;而现有 message 字段已经携带精确的机器可读 reason code,翻译表已能消除"只显示内部 error key"的问题。把 unknownMetricIDs 列为遗留项,价值仅在于"具体列出哪几个 metricID",暂不阻塞本阶段收口。

可复用经验 ​

  1. 修 bug 前先确认"进程会不会自己退出":control_rejected 之所以会导致无限等待而不是快速失败,根本原因不是 sessionID 门禁本身,而是 Sidecar 拒绝后仍然存活并继续监听——门禁只是让这条本该被看到的失败消息消失了。任何"以为流会自然结束"的假设都要先用真实/fixture 进程验证。
  2. 复用既有模式比引入新概念更安全:record.sessionID == nil || record.sessionID == plan.sessionID 这个写法在同一文件的其他三处(start()、report_sealed 分支)已经存在,说明这是该协议里"握手阶段允许 nil sessionID"的既定约定;runPrepared() 只是遗漏了这个约定,而不是需要发明新机制。
  3. 端到端集成测试的成本主要在"正确模拟协议状态机",而不是断言本身:写一个能正确响应 requestID/appGeneration/deviceGeneration 回显、且不携带会触发 captureCoverageFailure 的账本字段的 Python fixture,比断言逻辑本身耗时多得多;一旦 fixture 正确,三项测试断言(可见/不挂起/隔离不失效)可以在同一次运行里一次性验证,不需要三个独立 fixture。

遗留风险 ​

  1. unknownMetricIDs 结构化字段未实现(见"与原方案偏差"第 2 条):当前 UI 只能看到"包含不支持的指标"这句翻译文案,无法看到具体是哪个 metricID,之后如需要更精确的诊断展示需要单独批次补齐 wire 字段。
  2. requestID 往返关联未实现(见"与原方案偏差"第 1 条):如果未来出现"同一 Session 短时间内发出多个 start 尝试,响应需要精确关联到具体请求"的场景,需要重新评估是否要补上这条加固。
  3. Python 侧命令循环(listen_for_commands 内 start/prepare_app 分支的 control_rejected 路径)尚无直接的 Python 单元测试覆盖(任务书 4.5 节 Test 1);该函数深度嵌套在 main_stream 闭包内,提取为可独立测试单元需要额外重构,本批次未做,只做了代码走读确认。这是本阶段测试覆盖的已知缺口。
  4. PerformanceSessionController.swift 中还有一条未触及的旧路径(start() 方法对应的、非 prewarm 的 supervisor.start(configuration:) 直接启动分支,约第 900-920 行)同样存在 for await 循环,其 sessionID 门禁已经是 nil 容错版本,但未验证它在收到 control_rejected 时是否也会正确终止而不是无限等待;由于该路径是 xctrace 官方启动兜底的旧模式,不是 R7 controlled-cold-launch 主链路,本次未纳入范围,记为后续可选加固项。

下一阶段 Gate ​

R7-Fix-B 完成,Start 握手阶段的失败对 UI 可见且不再无限等待。Gate 开启进入 R7-Fix-C/D(清理旧 CoreProfile FrameTime ownership:capture_plan.py::scoped_feature_flags()、launch_trace.py::core_profile_capture_mode()、capture_policy.py 注释与激活逻辑)。


R7-Fix-C/D · 清理旧 CoreProfile FrameTime ownership(2026-09-17) ​

目标 ​

修正任务书 5/6 节指出的"第二组切源未收口项":产品层仍有一条数据通路可以把 ios.frame.* metricID 解读成"需要启动 CoreProfile",与 R3-R6 已确立的"Display/FrameTime 全部由 ActivityTrace FrameLifetime 提供、CoreProfile 只服务 StartupTiming"架构相悖。核心风险场景是 R7 Gate-0 的第三个真机测试(StartupTiming + Display Performance 同时勾选):如果不修,CoreProfile 会被错误分类为 startup-and-frame(一个不会在启动完成后停止、长期占用 CoreProfile tap 的模式),而不是正确的 startup-only。

实际修改 ​

  1. MacApp/Sidecar/perfowl_collector_core/capture_plan.py::scoped_feature_flags():frame_time 键的推导从 any(mid.startswith(FRAME_METRIC_PREFIX) for mid in ids) 改为恒定 False,并加注释说明原因;这是本次唯一必要的生产代码改动——frame_time_enabled 经由 perfowl_collector_core_cli.py:512(prepare_app 命令)与 realtime.py:1772(同一 flags 字典的第二个读取点)向下游传播,是产品侧唯一把"用户选择的 metricID"翻译成这个布尔值的地方。
  2. MacApp/Sidecar/perfowl_collector_core/capture_policy.py:删除过期注释"CoreProfile is required for ordinary FrameTime regardless of startup."(替换为准确描述"此处只是 prepare 能力、不代表 activate”的说明);确认并删除未被任何生产代码调用的死代码属性 frame_time_enabled(恒为 True,无调用方,仅测试直接断言它本身)。
  3. MacApp/Sidecar/tests/test_capture_selection.py:
    • 修正 test_feature_flags_from_selection 中断言 scoped_feature_flags(("ios.frame.time",))["frame_time"] 为真的旧断言,改为断言 ios.frame.time / ios.frame.time.p95 / ios.frame.pacing_jitter 三者均为 False。
    • 新增 test_startup_plus_display_performance_never_reaches_startup_and_frame_mode:完整复现 R7 Gate-0 第三场景(StartupTiming + 全部 5 项 Display Performance metricID),断言 scoped_feature_flags()["frame_time"] == False、core_profile_capture_mode(...) 结果为 "startup-only" 而非 "startup-and-frame",同时 CoreProfileRawAdapter 与 ActivityTraceFrameLifetimeRawAdapter 都在执行期 adapter 集合中(两者应并存,只是各自服务不同指标族)。
    • 新增 test_display_only_selection_never_activates_core_profile:Display-only 场景断言 core_profile_capture_mode(...) == "none" 且 CoreProfileRawAdapter 完全不在执行期 adapter 集合中,对应 Gate-0 第一场景的 coreProfileStartCount = 0 硬门禁。
  4. MacApp/Sidecar/tests/test_capture_policy.py:删除对已移除的 policy.frame_time_enabled 属性的断言。

根因验证与关键决策(本次未做的改动) ​

最初尝试了任务书 6 节建议的更彻底方案——把 core_profile_capture_mode() 的签名简化为只接受 launch_trace_enabled(连同 realtime.py 中全部 7 处 frame_time_enabled=frame_time_enabled, 调用参数一起删除)。执行后跑全量 Python 测试立即发现 tests/test_unified_core_profile.py、tests/test_frametime_continuation.py、tests/test_frame_census_and_fatal_diagnostics.py、tests/test_perfowl_realtime.py 等文件中有大量测试直接以关键字参数调用 stream_candidate_session(frame_time_enabled=True, ...) 或 core_profile_capture_mode(frame_time_enabled=True, ...),用于独立验证 "frame-only" / "startup-and-frame" 两种模式下引擎自身的正确性(例如"frame-only 模式下不得实例化 LaunchTraceDecoder")——这是与产品策略无关的、对底层引擎四态状态机的通用能力测试。删除该参数会直接导致这些测试报 TypeError: unexpected keyword argument。

判定:core_profile_capture_mode() / stream_candidate_session() 是引擎级通用函数,理应支持全部 4 种理论模式并被独立测试覆盖,这是防止底层机制腐化的合理测试面;真正需要修的是产品层——"当前产品永远不会把 frame_time_enabled=True 传给它"这件事,而这恰好只取决于 scoped_feature_flags() 这一个函数。于是撤销了 launch_trace.py 与 realtime.py 的改动(git checkout -- 两个文件),只保留 capture_plan.py 的最小修改。这精确对应任务书 6.3 节反复强调的原则:"prepare != activate"——引擎准备好支持某种模式不等于产品会激活它;本次修复发生在"产品是否激活"这一层,而不是"引擎是否能够支持"那一层。

测试结果 ​

text
Python 全量回归(venv-py312):543/543 PASS(11.4s,较 R7-Fix-B 的 541 新增 2 项)
Swift 全量回归(swift test):181/181 PASS,3 skipped(未改动 Swift,与 R7-Fix-B 持平)
站点构建:0 错误 0 死链(本阶段未改动站点内容,复跑确认门禁仍绿)

证据等级:E2(构建级)。真机验证 coreProfileStartCount / coreProfileCaptureMode 实际取值留给 R7-Fix-G。

与原方案偏差 ​

  1. 未改动 launch_trace.py::core_profile_capture_mode() 的函数签名,未删除 realtime.py 中的 frame_time_enabled=frame_time_enabled 调用参数。原方案 6 节的建议签名(只接受 launch_trace_enabled)会破坏一批合法的引擎级单元测试(见上一节"根因验证与关键决策"),因此改为在数据源头(scoped_feature_flags())把 frame_time 恒定为 False,效果等价(生产路径下 core_profile_capture_mode 永远不会被传入 frame_time_enabled=True),但风险显著更低、且不破坏引擎级测试覆盖。
  2. 未触碰 perfowl_collector_core_cli.py:718 处非 prewarm/stream 直接调用路径上的 frame_time_enabled=True 硬编码初始值。该路径是 Swift PerformanceSessionController 中 xctrace App Launch 官方启动的旧版回退分支(run(plan:...),非 R7 controlled-cold-launch 主链路 runPrepared()),任务书"建议修改文件范围"未列出该调用点;由于其影响面需要独立验证(是否仍有依赖该路径的历史测试、真实调用频率),本批次未处理,记入遗留风险。

可复用经验 ​

  1. "先跑测试再判断方案对不对"比"先读懂全部代码再动手"更快更safe:realtime.py 有 4000+ 行、core_profile_capture_mode 有 7 个调用点,逐行读懂所有上下文的成本很高;直接做出(看似正确的)签名简化改动、跑全量测试、让测试套件告诉我"这里还有一类我没想到的合法用途",比继续读代码找出所有隐藏依赖更快定位问题边界。
  2. 同一个函数可能同时是"引擎能力"和"产品策略"的交界点,两者需要用不同层级的测试分别覆盖:core_profile_capture_mode() 保留全部 4 个分支满足"引擎能否正确处理任意组合"的测试需求;scoped_feature_flags() 恒定 frame_time=False 满足"产品会不会把引擎推到不该去的状态"的测试需求。删除引擎的能力来"修"产品策略问题,是把两个不同层级的关注点搞混了。
  3. 复现真机故障场景比断言内部实现细节更有价值:新增的两个测试直接使用 Display Performance 的完整 5-metricID 集合 + StartupTiming,而不是抽象的 ("ios.frame.time",)——这样测试失败时的报错信息直接对应任务书里描述的 Gate-0 真机场景,而不需要读者再做一次"这个抽象输入对应哪个产品场景"的心智翻译。

遗留风险 ​

  1. perfowl_collector_core_cli.py:718(非 prewarm stream 直接调用路径)的 frame_time_enabled=True 硬编码初始值未清理,理论上如果该路径被驱动进同时选择 Startup 与 Frame 指标的场景,仍可能进入 startup-and-frame 模式;由于该路径是否仍在产品中被实际触发、以及其现有测试覆盖面尚未逐一核实,留待后续单独批次判断是否需要修正。
  2. capture_plan.py 中的 FRAME_METRIC_PREFIX 常量与 "ios.frame." 前缀匹配逻辑虽然不再被 scoped_feature_flags() 使用,但仍保留在 __all__ 导出与模块常量中(未被其他文件引用);是否应该一并删除或重新赋予其他用途,留待下次触碰此文件时决定,本次不做无关的额外清理。

下一阶段 Gate ​

R7-Fix-C/D 完成,生产路径不再可能把 CoreProfile 错误分类为 frame-only / startup-and-frame。Gate 开启进入 R7-Fix-E/F(跨层 Product Start Contract 集成测试:Registry ⊆ Workbench 不变量、默认 selection migration 通过 Python 校验、controlled-cold-launch 恰好执行一次的假 ProcessControl Resolver 测试)。


R7-Fix-E/F · 跨层 Product Start Contract 集成测试(2026-09-17) ​

目标 ​

任务书 7/8 节要求建立"过去 Swift 177/177 与 Python 540/540 都通过,但没有测试真正的产品请求闭环"这类断层的防回归测试:四条不变量(Workbench 请求集合 ⊆ Swift Registry;Swift Registry == Python all_metric_ids();每个可请求 metricID 都有 source contract;默认 Workbench selection 经 migratePreferences 后可通过 Python start 校验)+ 一条 Start/Launch 状态机不变量(controlled-cold-launch 恰好执行一次)。

实际盘点:四条不变量中三条已被前序阶段覆盖 ​

在动手写新测试前先逐条核对现状,避免重复造轮子:

  1. 不变量 1(Workbench ⊆ Swift Registry):R7-Fix-A 新增的 testEveryWorkbenchGroupMetricIDResolvesToARegistryDefinition(DisplayPerformanceSwiftTests.swift)已经遍历 WorkbenchMetricCatalog.metricIDsByGroup 全部 group(不只是 Display Performance),逐个断言在 PerfowlMetricRegistry.definitions 中有定义。已闭环,无需新增。
  2. 不变量 2(Swift Registry == Python all_metric_ids()):R7-Fix-A 更新后的 test_metric_contract_exactly_matches_swift_registry(test_perfowl_platform_support.py)用正则从 PerformanceModels.swift 源码里提取全部 id: "ios...." 字面量,与 Python audit_metric_source_contracts() 做逐项差集比较(missingMetricIDs/unexpectedMetricIDs 均需为空)。已闭环,无需新增。
  3. 不变量 3(每个可请求 metricID 都有 source contract):all_metric_ids() 与 source_contracts_by_metric() 都直接遍历同一份 ios-platform-profiles.candidate.v1.json 的 metricSourceContracts 列表构建——按代码结构这是一个定义层面必然成立的不变量(不存在"有 metricID 但没有 contract"的构造路径),因此不需要额外的运行时断言测试;仅在此记录该结论供后续审阅者复核,而不是省略验证。
  4. 不变量 4(默认 selection 经 migratePreferences 后可通过 Python 校验):唯一真正缺失的一环——这正是 R7 首轮真机失败的精确复现路径(WorkbenchMetricCatalog.defaultSelection 含 .fps → migratePreferences → .displayPerformance → 5 个 product metricID)。之前所有测试都验证了"迁移逻辑本身正确"或"Registry 里有这几个 ID",但没有一个测试从 defaultSelection 出发走完整条链路。

实际修改 ​

  1. MacApp/Tests/PerfowlCoreTests/DisplayPerformanceSwiftTests.swift:新增 testDefaultSelectionMigratesToDisplayPerformanceAndEveryProductMetricIDIsRegistered——从 WorkbenchMetricCatalog.defaultSelection(确认包含遗留 .fps)出发,经 migratePreferences 得到迁移后集合(确认包含 .displayPerformance 且不再含 .fps/.frameTime),再经 productMetricIDs(for:) 得到最终 product metricID 列表,逐项校验都在 PerfowlMetricRegistry.definitions 里有定义;最后用这份 ID 列表构造一个真实 CapturePlan,断言其 registryContractHash 与独立计算的 CapturePlan.registryHash(for:) 一致(镜像 Python 侧 perfowl_collector_core_cli.py 在 start 命令里对 registryContractHash/capturePlanHash 的比对逻辑)。这是产品默认路径第一次被端到端断言"从零配置到可提交的 CapturePlan 全程合法"。
  2. 未新增 Phase F(Start/Launch 状态机)测试代码。核实后发现 tests/test_perfowl_realtime.py 中已存在至少 5 个测试(test_prewarmed_start_reuses_one_provider_and_launches_once_unsuspended 等,行号 579/756/882/1006/1122/1248 一带)使用一个 FakeResolver 类模拟 ProcessControl:request_launch() 每次调用都会把 (bundle_identifier, cold, start_suspended) 追加到 launch_calls 列表,随后逐一断言 FakeResolver.launch_calls == [("com.example.fixture", True, False)]——True 对应 cold=True(即 kill_existing=True,controlled-cold-launch 语义),列表长度为 1 即证明"恰好执行一次"。这精确对应任务书 8 节要求的测试形状,判定为已有覆盖,无需重复。

根因验证 ​

新增的不变量 4 测试在 R7-Fix-A 完成之前会失败(因为 .displayPerformance 展开的 5 个 metricID 中有 3 个不在 Registry 里);在当前已完成 R7-Fix-A 的代码状态下通过。这验证了两件事:(a) 该测试确实能捕捉本次故障的根因形状;(b) R7-Fix-A 的修复确实使默认路径重新变得合法。

测试结果 ​

text
Swift 全量回归(swift test):182/182 PASS,3 skipped(22.2s,较 R7-Fix-C/D 的 181 新增 1 项)
Python 全量回归(venv-py312):543/543 PASS(未新增测试,复核确认 launch-count 覆盖已存在)
站点构建:0 错误 0 死链(本阶段未改动站点内容,复跑确认门禁仍绿)

证据等级:E2(构建级)。真机验证"默认选择首次点击开始即可成功进入采集"留给 R7-Fix-G。

与原方案偏差 ​

未新增 Phase F 的 Python 假 ProcessControl Resolver 测试代码,因为核实后发现功能等价的测试已经存在(见"实际修改"第 2 条)。这不是跳过任务书要求,而是在动手写代码前先核对现状避免重复;已有测试的断言形状(launch_calls 长度为 1、cold=True)与任务书 8 节描述的不变量完全一致。

可复用经验 ​

  1. 四条"不变量"里有三条已经被前面几个阶段顺带覆盖,只有认真逐条核对现状(而不是看到任务书列表就照单全部新写)才能发现真正的缺口在哪——盲目为每一条都新写一个测试,会制造大量与已有测试重复、只是措辞不同的断言,增加维护成本而不增加保护力。
  2. "端到端默认路径"测试的价值不在断言本身有多复杂,而在于它精确复现产品会真实触达的第一条路径(新用户首次点击开始 = 使用 defaultSelection)。此前的测试都在验证"迁移函数对不对""Registry 里有没有这几个 ID"这类局部正确性,唯独没有一个测试把"用户从零配置点击开始"这条链路完整跑一遍——局部正确不等于端到端正确,这正是 R7 真机故障能绕过 R0-R6 全部测试的根本原因。

遗留风险 ​

无新增遗留风险;不变量 3(source contract 完整性)依赖代码结构保证而非运行时断言,如果未来 all_metric_ids() 或 source_contracts_by_metric() 的实现改为不再共享同一份 _load_contracts() 遍历,该隐式保证可能失效,需要在那次改动时重新评估是否需要补一条显式测试。

下一阶段 Gate ​

R7-Fix-E/F 完成,代码层(E2)全部四条跨层不变量与 Start/Launch 状态机不变量均已闭环或确认既有覆盖。Gate 开启进入 R7-Fix-G(真机 Gate-0:Display-only / Startup-only / Startup+Display 三轮 30 秒内验证),该阶段需要用户连接真实 iOS 设备执行,无法由本批次独立完成。

Perfowl · Performance Observer