外观
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 / FrameUpdate2,053 条。逐帧来源因此始终primary-insufficient-evidence,帧耗时列 190 行全空。 - 直接用 pymobiledevice3 11.3.1
ActivityTraceTap(与产品相同配置:onlySignposts=1、predicate="")在设备 A 上抓 150 秒、Owner 期间滑动页面:FrameUpdate1,046 条(IOMFBServer1,035 +IOMFBServer.Stalls11),全部来自 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)、卡顿/平滑度公式。
验收标准
- 单元:真实格式串与原始元组样例解析正确;链式相接为
valid、断链discontinuity、超 200 mssource-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,待一并整理。
Validation Update · 2026-11-02:内部包 239 构建与同址安装(E3)
- 来源:main
c2564f06,Developer ID 签名构建 112.2 秒,来源门禁passed,包内提交号与 main 一致。 - 校验:冻结核心 107 个模块与 CLI 和源码一致(含本批 FrameUpdate 解码),严格签名、DMG 与安装后文件树一致,安装后
probeready;启动器只剩/Applications/Perfowl.app。 - 产物:
MacApp/dist/Perfowl-239-macos-x86_64-internal.dmg,146,214,104 B,SHA-256a109ba64741d56fa3664cd4420eaf6dc57eccf02307a65bd15ab95abe6f8fb5d;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 下保持缺失。