按 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.pyJANK_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.swiftSwift 帧分析器同步去重与 Stutter(自定义阈值路径同样去重)。
ReportDerivedStatistics.swiftExcel 导出补 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 待测)。

开发记录