Skip to content

iOS 26 帧耗时来源:FrameLifetime 并入 FrameUpdate ​

问题 ​

Owner 在设备 A(iOS 26,120 Hz 高刷屏)上测目标 App A,帧耗时(FTime)整场为空,卡顿/平滑度/1% Low 只来自启动后约 1–1.6 秒的 CoreProfile 片段。

诊断证据(E3) ​

  • 190 秒会话的 ActivityTrace 采集普查:收到 70,147 条 signpost,FrameLifetime 命中 0 条;同期 com.apple.coreanimation / IOMFBServer / FrameUpdate 2,053 条。逐帧来源因此始终 primary-insufficient-evidence,帧耗时列 190 行全空。
  • 直接用 pymobiledevice3 11.3.1 ActivityTraceTap(与产品相同配置:onlySignposts=1、predicate="")在设备 A 上抓 150 秒、Owner 期间滑动页面:FrameUpdate 1,046 条(IOMFBServer 1,035 + IOMFBServer.Stalls 11),全部来自 backboardd,event_type=Event,格式串固定为:
text
base_mct=%{signpost.generator:base_mct}llu  args1=…#llx args2=…#llx contributors=…P
FrameLifetimeInterval=%{name=FrameLifetimeInterval,signpost.generator:begin_end_32b}llu
FrameRender=… FrameReady=… FramePrevious=%{name=FramePrevious,signpost.generator:event}llu FrameGPUExecute=…
  • 原始消息元组:base_mct 与 FramePrevious 为 8 字节 uint64(Mach 绝对时间 ticks);FrameLifetimeInterval 为 4 字节无符号整数(相对 base_mct 的结束偏移);contributors 为 data 类型。
  • 链式验证:每一条事件的 FramePrevious 都恰好等于上一条事件的 base_mct + FrameLifetimeInterval,1,045 对相邻事件 1,045 对精确相接、0 断链(包括 11 条 Stalls 事件,它们是链中的普通帧)。base_mct 本身可以比上一条小(流水线并行),但呈现时刻严格递增。
  • 帧间隔分布:P5 / P50 / P95 = 8.34 / 8.34 / 16.68 ms,与 120 Hz 高刷屏一致;静止时出现长达约 10 秒的间隔。
  • 事件中没有刷新间隔字段(旧 refresh_interval 不再提供)。

公式(先文档后代码) ​

对 iOS 26 FrameUpdate:

text
presentationMach(i) = base_mct(i) + FrameLifetimeInterval(i)        // ticks
prevMach(i)         = FramePrevious(i)
FrameTimeMs(i)      = (presentationMach(i) − prevMach(i)) × numer ÷ denom ÷ 1e6
  • 与 iOS 17 FrameLifetime 的 end_time − prev_frame 是同一含义:本帧呈现时刻减上一帧呈现时刻;整数 ticks 先换算 ns(向下取整),再保留 6 位小数 ms。
  • 连续性:prevMach(i) 必须精确等于上一条已解码事件的 presentationMach,否则 discontinuity;会话/重连/目标代际变化或首帧为 warmup(与原规则相同)。
  • 长间隔门:旧规则为“> 12 个刷新间隔即 source-gap 置空”。FrameUpdate 没有刷新间隔,改用固定 200 ms(即 60 Hz 的 12 个刷新间隔),超过即 source-gap、FrameTime 为空;不补零、不复用旧值。算法版本 frameupdate-presentation-gap.v1。
  • 刷新率(displayRefreshRateHz):FrameUpdate 不提供刷新间隔,该来源下刷新率保持缺失,不从帧间隔反推。
  • P95 帧窗:沿用同一呈现间隔与 200 ms 长间隔门,其余窗口规则不变。
  • 范围:foreground-device(backboardd 合成器帧,不是目标进程私有帧),与原 FrameLifetime 相同。

实施范围 ​

  • display_performance.py:白名单增加 com.apple.coreanimation / IOMFBServer(.Stalls) / FrameUpdate;新增 FrameUpdate 字段解析(按格式串说明符与消息非文字元组逐一对齐,含 data 占位);解码器在刷新间隔未知时用 200 ms 门;输出增加 sourceSignpost,标准化事件族名仍为 FrameLifetime 以复用下游契约。
  • p95_frame_window.py:对 sourceSignpost=FrameUpdate 的事件,用 200 ms 门代替 12×刷新间隔。
  • 不改 CoreProfile 回退、FPS(sysmontap)、卡顿/平滑度公式。

验收标准 ​

  1. 单元:真实格式串与原始元组样例解析正确;链式相接为 valid、断链 discontinuity、超 200 ms source-gap;原 FrameLifetime 用例全部不变。
  2. 真机(E4):设备 A 上 60 秒以上、含滑动的会话,帧耗时在滑动段逐秒有值,逐帧来源被选中(atHealthy=true),静止段允许为空;卡顿/1% Low 统计覆盖整场而非启动 1 秒。

Implementation Update · 2026-11-02 ​

文件修改
MacApp/Sidecar/perfowl_collector_core/display_performance.py白名单接受 IOMFBServer / IOMFBServer.Stalls 下的 FrameUpdate,保持原短路读取顺序;新增 _extract_frame_update_fields,按格式串说明符与消息非文字元组逐一对齐(data 占位),映射为 begin_time=base_mct、end_time=base_mct+FrameLifetimeInterval、prev_frame=FramePrevious、refresh_interval=0;解析失败为 unsupported-schema。解码器在刷新间隔为 0 时以 200 ms 为长间隔门,输出 sourceSignpost。
MacApp/Sidecar/perfowl_collector_core/p95_frame_window.pysourceSignpost=FrameUpdate 且刷新间隔为 0 的事件,用 200 ms 代替 12×刷新间隔。
MacApp/Sidecar/tests/test_ios26_frame_update.py新增 7 项:白名单、真机记录的相接样本、链式 valid / 断链 discontinuity / 200 ms 边界与 source-gap、残缺消息、逐帧来源合格判定、P95 帧窗。
MacApp/Sidecar/tests/test_display_dispatch_equivalence.py冻结的白名单参考实现同步加入 FrameUpdate(读取顺序断言不变)。
知识页frame-time 增加 iOS 26 说明与版本行;display-refresh-rate 注明 iOS 26 下刷新率缺失。
  • 偏差:标准化事件族名仍写 FrameLifetime,以复用下游已有契约;真实来源以新增字段 sourceSignpost 区分。
  • 复杂度:每条 FrameUpdate 解析为一次线性遍历(19 个元组),不增加缓存或额外进程;被拒的其他 signpost 走原有短路路径,读取次数不变。

验证(E2) ​

  • 新增 7 项与帧/显示相关 17 个模块共 290 项:全部通过。
  • Python 全量 3137 项:10 失败 / 6 错误 / 81 跳过;失败与错误全部在 test_native_direct_archive、test_native_raw_graph_normalization、test_sw1_p1_continuation_admission 三个模块。撤下本批改动在同一环境复跑这三个模块,结果完全相同(9F2E、1F3E、1E),属本工作目录未构建原生扩展的既有环境差异,与本批无关。

遗留 ​

  • 真机 E4:新包上 60 秒以上含滑动会话,确认逐帧来源被选中、帧耗时滑动段逐秒有值。
  • 启动阶段若画面完全静止、主来源在启动追踪结束前未合格,仍会转入 CoreProfile 回退并可能因资源上限失败;当前分段账本不允许之后再切回主来源。另立项。
  • 实时元数据中 frameTimeMs 的来源标签仍写 activitytracetap.FrameLifetime,待一并整理。

Validation Update · 2026-11-02:内部包 239 构建与同址安装(E3) ​

  • 来源:main c2564f06,Developer ID 签名构建 112.2 秒,来源门禁 passed,包内提交号与 main 一致。
  • 校验:冻结核心 107 个模块与 CLI 和源码一致(含本批 FrameUpdate 解码),严格签名、DMG 与安装后文件树一致,安装后 probe ready;启动器只剩 /Applications/Perfowl.app。
  • 产物:MacApp/dist/Perfowl-239-macos-x86_64-internal.dmg,146,214,104 B,SHA-256 a109ba64741d56fa3664cd4420eaf6dc57eccf02307a65bd15ab95abe6f8fb5d;238 安装包与回滚包在私有归档,回滚已核对。
  • 未完成:公证暂缓(Gatekeeper exit 3);真机 E4 待 Owner 录含滑动的 60 秒以上会话。

Validation Update · 2026-11-02:239 真机复测(E4,设备 A / 目标 App A) ​

Owner 在 239 上录一场约 104 秒、含多次滑动与停顿的会话:

项目结果
逐帧来源FrameUpdate 主来源全程选中(atHealthy=true,0 次回退,只有一个分段 0–104.3 秒)
帧事件127,150 条 signpost 中 9,392 条 FrameUpdate 通过白名单(修复前同类会话为 0)
帧耗时逐秒104 行中 101 行有值(valid),3 行为 source-gap(>200 ms 的静止长间隔,按规则留空);首行预热
P95 帧耗时100 行有值
独立对照同场 sysmontap 每秒 FPS 合计 9,330 帧,与 FrameUpdate 9,392 帧相差 0.7%(两个独立来源)
帧统计覆盖帧报告覆盖 103.0 秒(修复前约 1.2 秒):平均 FPS 96.9、1% Low 10.6、最小帧耗时 8.34 ms
采集连续性CPU/内存逐秒 0 缺值,sysmontap 来源无 >1.5 秒断档,界面最大出队等待 6.1 ms
  • 结论:iOS 26 帧耗时来源修复真机验收通过(E4,单设备单目标 App、120 Hz)。
  • 仍为候选:120 Hz 可变刷新下的卡顿计数(本场 Jank 74 / BigJank 9)与平滑度沿用原公式,尚未与 PerfDog 同场对照,不作为已校准结论。刷新率在 iOS 26 下保持缺失。

Perfowl · Performance Observer