Skip to content

卡顿时间比(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 / FrameUpdate signpost。它与 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 秒)取上次以来新判定的帧,

    text
    hitchMs          = 本窗口 Σ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 显示断流检出。第二轮是留出验证,规则未调整
E2Sidecar 与 Swift 单测,含存档恢复、限帧、静止会话与报告读取

未覆盖:

  • iOS 15–25 的 FrameLifetime 字段映射;
  • 60 Hz 机型;
  • “App 限帧低于屏幕刷新率”,因为 Fixture 请求低帧率时系统会把整块屏幕一起降频,测试 App 单独复现不了;
  • 同一次操作的逐次对齐:同时录制 Instruments 会让 trace 失控增长,所以两边是分开录的。

7. 如何分析 ​

  1. 先看动画时长:太短(几秒以内)时比值不稳定,以次数和最长卡顿为主。
  2. 卡顿时间比高、最长卡顿短:频繁的小卡,常见于持续重渲染、离屏渲染多的页面。
  3. 卡顿时间比不高、最长卡顿很长:偶发长卡,结合时间轴的 CPU、截图与备注找主线程阻塞。
  4. 限帧卡顿占比高:App 自己限帧(如 30 帧动画)时卡住了,Instruments 不会显示;排查方向与普通卡顿相同。

8. 与其他画面指标的关系 ​

  • Jank、Stutter 与帧体验:基于帧间隔的 PerfDog 口径,分母是已记录帧区间;卡顿时间比基于“预期上屏”,分母只算动画时间,两者不互相换算。
  • FPS:静止时没有新帧,FPS 会下降但不是卡顿;卡顿时间比不受静止影响。

9. 方法与坑点 ​

校准方法(确定性 Fixture、分开录制、iPhone 60 Hz 上限、Instruments 口径)见 性能采集实验 第 9 节。

10. 版本历史 ​

版本日期变更来源
perfowl-hitch.v12026-11-04首次发布:candidate v3 判定(限帧帧单独计稳态寿命)定稿;限帧卡顿照报并标注;接入在线报告、会话报告、Excel 与多次汇总;注册 4 个 metricID可行性研究、校准与实施记录
perfowl-hitch.v12026-11-04口径不变;在线汇总补判会话最后一帧,与离线重算一致(内部包 255 真机对照发现)Validation / Implementation Update
perfowl-hitch.v12026-11-04新增实时曲线:每个样本窗口按动画时长归一,报告区间按动画时长加权;metrics.csv 与导出 CSV 追加列Phase 3c 方案与实现
perfowl-hitch.v12026-11-04口径不变;实时曲线跟随帧来源选择,未选用时置空(内部包 256 真机发现)Validation Update · 256
perfowl-hitch.v12026-11-04正式验收通过,计入 59/70(内部包 256/257 真机 E4)Validation Update · 257
perfowl-hitch.ios15.candidate.v12026-11-04新增 iOS 15 专用规则:按目标 App 的 Core Animation 提交间隔判定,并检出显示断流;v1 在 iOS 15 上漏检注入卡顿分版本能力确认记录

Perfowl · Performance Observer