按 PerfDog 对照修正三处口径
依据:[PerfDog 帧统计口径对照](/devlog/p6-perfdog-frame-stat-parity-2026-11-02)(同一 3,440 帧序列离线复算,E3)。Owner 2026-11-02 要求修正。
公式(先文档后代码)
记有效帧耗时序列 f[i](ms),prev3(i) = f[i-1], f[i-2], f[i-3],J[i] 为第 i 帧是否计为 Jank。基线窗口在任何无效间隔、来源切换时清空(与原规则相同)。
① Jank / BigJank:前 3 帧已有 Jank 不重复计(jank.perfdog-dedup.v3 / big-jank.perfdog-dedup.v3)
J[i] = |prev3| = 3 且 3·f[i] > 2·Σprev3 且 f[i] > 83.33 ms 且 J[i-1], J[i-2], J[i-3] 均为假 BJ[i] = J 的同一前提(含“前 3 帧无 Jank”) 且 f[i] > 125 ms
• 比较仍在整数 ns 上做(沿用 strict-rational-ns 的取整与严格不等式),只新增去重条件。
• 去重只看 Jank 标记,BigJank 必然同时是 Jank。
• TinyJank / SmallJank 口径未能复现 PerfDog,本批不改。
② Stutter(stutter.jank-frame-time-share.v2)
Stutter[%] = Σ_{J[i]} f[i] ÷ 观察时长(ms) × 100• 观察时长与原来相同:各来源 epoch 的首末帧时间跨度之和。
• 原公式为 Σ max(0, f[i] − 基线),同序列偏高约 0.8 个百分点。
③ 报告 FPS 类统计统一按每秒 FPS 序列(per-second-fps.v1)
Avg(FPS)、FPS≥18[%]、FPS≥25[%]、Std/Var(FPS)、Median(FPS)、MedRange(FPS)[%]、Drop(FPS)[/h] 一律取产品每秒 FPS 序列 F[k]:
Drop(FPS)[/h] = #{k : F[k−1] − F[k] > 8} ÷ (t_last − t_first)[s] × 3600
MedRange[%] = #{k : 0.8·median(F) ≤ F[k] ≤ 1.2·median(F)} ÷ n × 100• 统计报告此前先按每秒序列算出这些值,又被逐帧汇总覆盖;改为不再用逐帧值覆盖。逐帧汇总(frame-report.json)里的同名字段保留为诊断,不进入报告。
• Excel 导出的 Drop(FPS) 此前留空,按上式补上。
实施范围
• Python:frame_statistics_policy.py(版本号与去重判定)、frame_derivation.analyze_display_frames、online_report 帧累加器(含断点状态 recentJank、stutterJankMs)、两处统计报告映射(online_report._apply_frame_stats、report_engine.build_stat_report)。
• Swift:FramePipelineAnalyzer 同步去重与 Stutter;ReportDerivedStatistics 补 Drop(FPS),FPS 类标题改为“每秒”。
• 历史验收脚本(internal/acceptance/*)固定旧版本,作为历史证据不改。
验收标准
• 回归测试:用 PerfDog 导出的 3,440 帧序列(只存帧耗时,不含设备信息)作为测试数据,Jank 21、BigJank 19 必须完全一致,Stutter 与 PerfDog 13.09% 相差 ≤ 0.2 个百分点;48 个每秒 FPS 值复算的 7 项 FPS 统计与 PerfDog 完全一致。
• 原有帧统计测试只允许因版本号与新口径而变化,并逐一说明。
• 新包真机会话的报告中 FPS 类统计与每秒 FPS 序列一致。
Implementation Update · 2026-11-02
| 文件 | 修改 |
|---|---|
MacApp/Sidecar/perfowl_collector_core/frame_statistics_policy.py | JANK_CLASSIFICATION_VERSION=jank.perfdog-dedup.v3、BIG_JANK_CLASSIFICATION_VERSION=big-jank.perfdog-dedup.v3,新增 STUTTER_FORMULA_VERSION=stutter.jank-frame-time-share.v2。整数 ns 严格判定函数不变。 |
frame_derivation.py(analyze_display_frames) | 维护与前 3 帧对应的 Jank 标记(随基线一起清空),去重后计 Jank/BigJank;Stutter 改为计入 Jank 的帧耗时之和;摘要新增 stutterFormulaVersion。 |
online_report.py | 帧累加器同上,断点状态新增 recentJank、stutterJankMs、stutterFormulaVersion,旧版本断点按 offline-required 拒绝恢复;统计报告不再用逐帧汇总覆盖 FPS 类 6 项;Drop(FPS) 改为每秒 FPS 相邻下降 > 8 的计数(状态 previousFPSGauge 与 drop>8 计数可断点恢复)。 |
report_engine.py | 离线统计报告同样不再用逐帧值覆盖 FPS 类;Stutter 标注新公式版本。 |
MacApp/Sources/PerfowlCore/FramePipeline.swift | Swift 帧分析器同步去重与 Stutter(自定义阈值路径同样去重)。 |
ReportDerivedStatistics.swift | Excel 导出补 Drop(FPS)(每秒序列、末秒−首秒);FPS 类中文标题注明“每秒”。 |
PerformanceModels.swift 与 site/data/metric-contracts.json | 注册表 9 项更新 sourceKey/definitionVersion(FPS 类 5 项 per-second-fps.v1、Jank/BigJank v3、Stutter v2、FTime 补 iOS 26 公式)。 |
| 测试 | 新增 test_perfdog_parity.py 与 PerfDog 导出数值夹具(只含帧耗时与每秒 FPS);Swift 新增去重/Stutter 用例;按新口径更新 test_jank、test_big_jank、test_stutter、test_frame_statistics_admission、test_fps_drop、test_med_range、test_perfowl_report_engine 与 Swift 两处版本断言,Excel Drop 断言由空改为 0。 |
| 知识页 | frame-performance(当前口径、Jank 表、Drop 公式、旧合同段标注)、metric-contract-index(9 行与版本历史 v4)、rendering-pipeline(Stutter 释义)。 |
相对方案的偏差
• 夹具使用 PerfDog 导出的全部 3,483 帧(含其 t=0 之前 43 帧作为 Jank 历史)。只用 t≥0 的 3,440 帧时首个 Jank 缺少前 3 帧历史,结果为 20/18。
• Stutter 验收标准由“相差 ≤ 0.2 个百分点”调整为:分子(计入的 Jank 帧耗时 6.153 秒)除以 PerfDog 的 47 秒每秒数据跨度得 13.09%,与 PerfDog 完全一致;Perfowl 发布值以帧跨度约 48 秒为分母,为 12.82%(差 0.27 个百分点)。分母定义保持 Perfowl 原有“各 epoch 首末帧跨度”,未改。
验证(E2)
• PerfDog 对照:Jank 21 / BigJank 19 完全一致;Avg/Median/Std/Var/FPS≥18/FPS≥25/MedRange/Drop 离线与在线两条路径均与 PerfDog 一致(导出两位小数内);1% Low 3.82 一致。
• Swift 全量 442 项:0 失败、10 跳过(排除既有挂起的 UserReportIntegrationTests)。
• Python 全量 3,139 项:11 失败 / 6 错误 / 81 跳过。其中 16 项为改动前即存在的原生扩展与 sw1 环境失败(同环境改动前后结果一致);另 1 项 test_pump_filter_active_fairness 为墙钟计时基准,全量时与 Swift 编译并行导致超时,单独复跑 3/3 通过。
• metric-knowledge-gate:66 项注册表合同一致,PASS。
影响与遗留
• 历史会话的封存报告不回改;新会话按新口径。10-23~10-24 各项“5/5 通过”的历史数字保持不变,当时即标注无参考对照。
• TinyJank / SmallJank 与 Smooth 口径仍未对齐 PerfDog。
• 真机:新包上录一场会话,核对报告 FPS 类与每秒 FPS 一致、Jank 去重生效(E4 待测)。