外观
卡顿时间比(Hitch):动画时画面卡了多久
平均 FPS 和平均帧耗时会被静止时间拉平:一段 3 分钟的会话里有 10 秒在滑动,即使滑动时卡得明显,整场平均也可能看起来正常。卡顿时间比只看画面在动的时间,回答“动的时候,每秒有多少毫秒画面停在原地”。它对标 Apple 的 Hitch 思路(实际上屏比预期晚多少),但由 Perfowl 自行计算,不使用 Apple 的指标名,也不承诺与 Xcode Organizer、MetricKit 数值相等。
当前状态 · 2026-11-04(已验收)
- 算法:
perfowl-hitch.v1。Phase 2 校准有条件通过(验收记录):- 可控 Fixture 上 120 Hz、60 Hz 与 30 Hz 限帧注入的卡顿全部检出,时长与 Instruments Animation Hitches 逐次相同(分别录制,E4 原始数据 + E3 离线重算);
- 两个真实 App 只能按分布对比,差距在门槛内。
- 产品:采集端、会话报告与工作台实时曲线已接入。正式验收通过(2026-11-04,计入 59/70):内部包 256/257 上三段真机会话,在线报告与离线重算 8 项逐项相同,实时曲线与报告一致,静止时段不画点(E4)。整场零动画(目标 App A 自带周期动画,做不出来)与帧来源超限两条路径为单测(E2)。
- 适用范围:
- 正式验收:设备 A(120 Hz ProMotion)、iOS 26 的 FrameUpdate 帧来源。
- iOS 15(2026-11-04):改用专用规则
perfowl-hitch.ios15.candidate.v1,见第 4 节。设备 B(60 Hz)两轮校准注入 12/12 检出、误报 0;产品实现待真机复核,仍为候选。 - 其他版本待分版本能力表核验。
1. 定义 / 概念
| 指标 | metricID | 回答的问题 |
|---|---|---|
| 卡顿时间比 | ios.frame.hitch_time_ratio | 动画期间每秒卡了多少毫秒(主指标) |
| 卡顿次数 | ios.frame.hitch_count | 频繁短卡还是偶发长卡 |
| 最长卡顿 | ios.frame.hitch_max | 最严重的一次停了多久 |
| 动画时长 | ios.frame.animation_time | 分母:这次会话里画面在动的时间 |
单次卡顿:某一帧实际上屏的时刻,比按稳态节奏推算的预期上屏时刻晚了多少。晚不到半个刷新间隔的不计。
2. Scope 与 Unit / 单位
- scope:
foreground-device。FrameUpdate 是整块屏幕的合成帧,包含状态栏、键盘等系统界面;精确到目标 App 的归属尚未实现。 - unit:卡顿时间比
milliseconds-per-second(ms/s),卡顿次数count,最长卡顿milliseconds,动画时长seconds。
3. 系统服务 / 采集调用
- 系统服务与通道:
com.apple.instruments.server.services.activitytracetap,经 DTX 。 - 来源:iOS 26 的
com.apple.coreanimation/IOMFBServer/FrameUpdatesignpost。它与 FrameTime 共用同一条流,不新增采集通道。 - 采集调用:
pymobiledevice3的 DTX 内核负责传输。Perfowl 自研层负责解码:ActivityTraceFrameLifetimeRawAdapter解码逐帧字段(开始时刻、帧寿命、目标刷新间隔、上一帧上屏时刻、Stalls 标记);perfowl_collector_core/hitch_metrics.py的HitchAccumulator逐帧判定;- 在线报告在
_FrameAccumulator中把同一批帧行喂给它;离线帧报告analyze_display_frames与离线重算hitch-recompute使用同一实现。
- 字段解码见 FrameTime 的 FrameUpdate 一节。
4. 公式 / 计算方式
对链式相接的帧 i(上一帧上屏时刻字段等于前一帧的上屏时刻):
text
P(i) 实际上屏 = base_mct + FrameLifetimeInterval
B(i) 开始时刻 = base_mct
V(i) 目标刷新间隔(args2 低 32 位,经 mach timebase 换算为 ns)
Vx(i) = max(V(i-1), V(i), V(i+1)) 容忍刷新率切换
gap(i) = B(i) − P(i−1); M = 0.2 ms
起点类型 = continuous gap ≤ −M 上一帧上屏前已开始(连续动画)
paced −M < gap ≤ M 上一帧上屏时才开始(App 限帧)
idle gap > M 静止后重新开始(不判定)
L*(i) = 同 (V, 起点类型) 最近 16 个非 Stalls 帧 round(帧寿命 ÷ V) 的众数 × V(i)
E(i) = max(P(i−1) + Vx(i), B(i) + L*(i)) 预期上屏
H(i) = P(i) − E(i);起点不是 idle 且 H(i) ≥ 0.5 × V(i) 时记一次卡顿会话汇总:
text
动画时长 A = Σ(起点为 continuous 或 paced 的帧)P(i) − P(i−1)
卡顿时间比 = ΣH ÷ A (ms ÷ s)
卡顿次数 = #{H > 0}
最长卡顿 = max H限帧卡顿标注:起点为 paced 的卡顿另计“其中限帧卡顿”。画面确实停顿,但 Instruments 不报这类卡顿(2026-11-04 Fixture 对照)。Owner 决定照报并标注。与 Instruments 对数时用去掉这部分后的时间比(
instrumentsComparableRatioMsPerS)。恢复帧:长卡顿之后紧跟的一帧若也晚一个刷新间隔,单独计一次,与 Instruments 一致。
实时曲线(工作台“画面表现”卡片的“卡顿”,
metrics.csv的hitchTimeRatioMsPerS列):每次发布样本(约 1 秒)取上次以来新判定的帧,texthitchMs = 本窗口 ΣH hitchAnimationMs = 本窗口动画时长 卡顿时间比(本窗口)= hitchMs ÷ (hitchAnimationMs ÷ 1000)用比值而不用“本秒卡顿毫秒数”:主机有时会一次收到几秒的帧,按到达时刻直接加总会出现假尖峰,比值按动画时间归一,不受影响。报告里选中一段时间时,卡顿时间比 = Σ hitchMs ÷ Σ hitchAnimationMs,不是逐点平均。曲线比画面晚至少一帧,因为每一帧要等下一帧到达才能判定。算法标记
perfowl-hitch.v1.window-since-last-sample。分级参照:报告按 Xcode Organizer 的分档给颜色:≤ 10 良好、≤ 25 注意、≤ 50 严重、> 50 需立即处理。这只是参照,不是 Apple 的判定。
iOS 15:用 App 自己的画面提交判定(perfowl-hitch.ios15.candidate.v1)
iOS 15 的 FrameLifetime 是整屏合成帧。App 卡住时,屏幕会落回静止时的 20 Hz 节奏继续合成,帧寿命不会变长,所以上面的公式在 iOS 15 上会漏检:设备 B 上 6 次注入一次也没检出。
iOS 15 改用目标进程的 Core Animation 提交事件(com.apple.coreanimation / Transaction / Commit)。采集时只保留它的设备时间戳,挂在下一条 FrameLifetime 上(字段 appCommitTimesNs),只在 iOS 15 收集。
text
更新 u(k):目标进程的提交,间隔 ≤ 4 ms 的合为一次
Δ(k) = u(k+1) − u(k)
稳态 S = 最近 16 个 round(Δ/V) 的众数(每个间隔都计入)
H(k) = Δ(k) − S × V
记一次卡顿:H ≥ V/2,且 |Δ(k−1) − S×V| ≤ V/2(前一个间隔在稳态上),
且 Δ(k+1) ≤ Δ(k) − V/2(后一个间隔恢复变短)
Δ > 2 s:App 停止动画,不判定,并清空稳态
显示断流:App 仍在提交,但两次合成帧之间超过 150 ms,记一次,H = 间隔 − V
动画时长 = 参与判定的 Δ 之和- 为什么看前后两个间隔:从 60 Hz 切到 30 Hz 时,间隔会持续变长,或者先乱几帧再稳定,这是节奏变了,不是卡顿。只看单个长间隔,30 Hz 段会误报几百次;看前后两个间隔之后,误报为 0。
- 类型:iOS 15 没有渲染开始时刻,卡顿类型都记为
unknown。
5. Quality / missing
- 整场没有动画(A = 0):四项都为空,报告不显示卡顿项,不写 0。
- 帧缺少开始时刻、帧寿命或刷新间隔:该帧不判定,覆盖率下降;全场都无法判定时为
no-timing。 - 断链帧(上一帧上屏时刻对不上)、首帧、来源切换后的第一帧:不判定,也不计入动画时长。
- 带系统 Stalls 标记的帧照常判定,但不进入稳态寿命统计,避免把慢帧当成常态。
- 帧来源管理器未选用 ActivityTrace(如数据量超限
resource-limited)时,报告不使用这些帧,实时曲线同样置空,质量写管理器给出的原因。 - 实时曲线的空值:窗口内有帧但都不在动画中(或已确认静止)为
not-applicable-no-animation,工作台显示“— · 画面静止”;窗口内没有新判定的帧为missing-no-new-frame;目标不在前台为not-foreground;来源过期为stale-device-source。这些情况都不画点,不写 0。 - 会话最后一帧:判定要看下一帧,最后一帧一直挂起。最终汇总必须补判它,否则动画时长会少一帧。在线报告用不改变累加器的补判汇总(
settled_summary()),离线读完文件时补判,两者结果相同。 - 检查点保存有界状态(每个刷新间隔与起点类型 16 个整数、两帧、若干累计值,约 1.3 KB);版本不符时不恢复,按既有规则要求离线重算。
6. 证据与适用范围
| 层级 | 内容 |
|---|---|
| E4 | 设备 A、iOS 26,Fixture 100 秒校准 3 轮,Instruments Animation Hitches 单独录制 1 轮(原始数据) |
| E4 | 内部包 256/257,设备 A、目标 App A,三段会话(67 s、129 s 静止、88 s 操作):在线报告与离线重算 8 项逐项相同;实时曲线各窗口之和与报告一致 |
| E3 | 上述数据与两个真实 App 会话的离线重算;在线与离线结果一致 |
| E4 + E3(iOS 15) | 设备 B(iOS 15.3.1,60 Hz)两轮 Fixture 校准原始数据:注入 20/50/100/200 ms 与 30 Hz 段 50/100 ms,12/12 检出,单次误差 ≤ 2.2 ms;已知无卡顿的段误报 0;重渲染时 15 s 显示断流检出。第二轮是留出验证,规则未调整 |
| E2 | Sidecar 与 Swift 单测,含存档恢复、限帧、静止会话与报告读取 |
未覆盖:
- iOS 15–25 的
FrameLifetime字段映射; - 60 Hz 机型;
- “App 限帧低于屏幕刷新率”,因为 Fixture 请求低帧率时系统会把整块屏幕一起降频,测试 App 单独复现不了;
- 同一次操作的逐次对齐:同时录制 Instruments 会让 trace 失控增长,所以两边是分开录的。
7. 如何分析
- 先看动画时长:太短(几秒以内)时比值不稳定,以次数和最长卡顿为主。
- 卡顿时间比高、最长卡顿短:频繁的小卡,常见于持续重渲染、离屏渲染多的页面。
- 卡顿时间比不高、最长卡顿很长:偶发长卡,结合时间轴的 CPU、截图与备注找主线程阻塞。
- 限帧卡顿占比高:App 自己限帧(如 30 帧动画)时卡住了,Instruments 不会显示;排查方向与普通卡顿相同。
8. 与其他画面指标的关系
- Jank、Stutter 与帧体验:基于帧间隔的 PerfDog 口径,分母是已记录帧区间;卡顿时间比基于“预期上屏”,分母只算动画时间,两者不互相换算。
- FPS:静止时没有新帧,FPS 会下降但不是卡顿;卡顿时间比不受静止影响。
9. 方法与坑点
校准方法(确定性 Fixture、分开录制、iPhone 60 Hz 上限、Instruments 口径)见 性能采集实验 第 9 节。
10. 版本历史
| 版本 | 日期 | 变更 | 来源 |
|---|---|---|---|
| perfowl-hitch.v1 | 2026-11-04 | 首次发布:candidate v3 判定(限帧帧单独计稳态寿命)定稿;限帧卡顿照报并标注;接入在线报告、会话报告、Excel 与多次汇总;注册 4 个 metricID | 可行性研究、校准与实施记录 |
| perfowl-hitch.v1 | 2026-11-04 | 口径不变;在线汇总补判会话最后一帧,与离线重算一致(内部包 255 真机对照发现) | Validation / Implementation Update |
| perfowl-hitch.v1 | 2026-11-04 | 新增实时曲线:每个样本窗口按动画时长归一,报告区间按动画时长加权;metrics.csv 与导出 CSV 追加列 | Phase 3c 方案与实现 |
| perfowl-hitch.v1 | 2026-11-04 | 口径不变;实时曲线跟随帧来源选择,未选用时置空(内部包 256 真机发现) | Validation Update · 256 |
| perfowl-hitch.v1 | 2026-11-04 | 正式验收通过,计入 59/70(内部包 256/257 真机 E4) | Validation Update · 257 |
| perfowl-hitch.ios15.candidate.v1 | 2026-11-04 | 新增 iOS 15 专用规则:按目标 App 的 Core Animation 提交间隔判定,并检出显示断流;v1 在 iOS 15 上漏检注入卡顿 | 分版本能力确认记录 |