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,格式串固定为:
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:
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)、卡顿/平滑度公式。
验收标准
• 单元:真实格式串与原始元组样例解析正确;链式相接为 valid、断链 discontinuity、超 200 ms source-gap;原 FrameLifetime 用例全部不变。
• 真机(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.py | sourceSignpost=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,待一并整理。