Skip to content

Display Performance 技术演进与实验 ​

2026-10-26 · 本月实验结论与当前范围 ​

本页早期切源路线保留,下面9月阶段门不是当前验收进度。当前正式55/66、11项暂停;显示事实与派生项已有各自专项证据,仍不等于完整独立物理精度校准。

  • 静止空值与恢复(10月16日,E4限定):自有场景五新轮,5,290实际FrameLifetime、658发布样本,两动画段最小有值率100%,恢复最大估计572.952ms;静止三次预定界面取样全保留。静态段停CADisplayLink与周期UI,没有完整零帧水位就保守留空,不能用主机计时补0。早期“candidate-idle可直接出0”的设计不作当前保证。专项结果。
  • 参考从刺激走到上屏:CADisplayLink回调、sleep、GPU提交只能证明请求或负载;Metal actual presented也必须覆盖系统统计全部端点与前驱。局部正常段吻合不解决完整集合额外/缺失问题。平均FPS和Smooth各自重审参考资格,未沿用前项“无参考”结论。平均FPS五轮 · Smooth五轮。
  • 严格阈值与完整分母:用原生整数/有理数判断8FPS、40ms、100ms等边界;占比N与事件率的完整epoch时长T分别冻结,未知/断点不补0、不跨代。120个源健康槽不当成120个独立摘要。Drop合同 · 帧派生Canonical。
  • 排查次序:先确认目标前台与刺激执行,再看源census、接收/设备时钟年龄、decoder、实际消费者及Stop封存;本地全消费只能证明本地段,不证明设备无漏帧。失败首停,所有尾部/不利点和旧attempt保留。

操作步骤、避免误判和每条证据限制见实验方法索引。本节是既有证据E1复盘,无新采集、不新增验收项。

Perfowl 当前使用 ActivityTrace / CoreAnimation FrameLifetime 作为 Display Performance 的基础事实源,但这个方案不是一开始就确定的。早期 FrameTime 已经能从 CoreProfile Presented Frame 得到真实逐帧时间戳,真正的问题是:怎样在不牺牲时间真实性的前提下,把长期采集从数十 MiB/s 的 KPerf/KDebug 重流量降到适合产品常开的水平。

这篇文章记录技术路线为什么一步步收敛到 FrameLifetime。原始实验细节仍保留在两份开发记录中:

1. 9月切源阶段快照(当前范围见页首) ​

text
当前新 Session Display source
→ ActivityTrace / CoreAnimation / IOMFBServer / FrameLifetime

Source POC
→ iOS 17 真机 E4

产品实现
→ R0-R6 + R7-Fix-A~F 已完成 E2

完整产品真机门禁
→ R7-Fix-G / H / I Pending

旧 CoreProfile Presented Frame
→ legacy replay / controlled comparison
→ 不再承担长期 Display 主采集

路线演进可以压缩成:

Display Performance 数据源技术演进路线

图:从“能拿到真实帧时间戳”进一步收敛到“真实、低吞吐、可连续产品化”的 FrameLifetime 路线。

2. 第一代方案:CoreProfile Presented Frame ​

Perfowl 早期逐帧链来自 CoreProfile 0x31800318 Presented Frame。这个方向的优点非常明确:

  • 有真实设备时间戳;
  • 可以建立逐帧间隔;
  • 不需要 App 内埋点;
  • 已经能驱动 FrameTime / Jank / Stutter 等上层算法。

因此问题并不是“这条时间戳是假数据”,而是 transport 成本。

一次约 5 分 53 秒的真机 Session 得到:

text
CoreProfile total           10.34 GiB
Epoch 2                     10.23 GiB
平均吞吐                    29.99 MiB/s
kd_buf records              171,561,893
Presented Frame             3,981
Presented Frame 占全部记录  约 0.00232%

也就是说,大多数经 DTX 传回 Host 的数据并不是我们最终需要的 Presented Frame。

阶段判断:时间真实性成立,但长期 source architecture 不成立。

3. csd=0:关闭调用栈仍解决不了问题 ​

第一直觉是“大量数据可能来自 stack sampling”。因此 CoreProfile Frame-only 配置把 callstack depth 收到:

text
csd = 0

但真实吞吐仍长期保持在约 20~33 MiB/s 的量级。

结论:调用栈不是主放大器。继续在 Host 侧删 Raw 或减少 UI 发布频率都无法减少 device → Mac 已经发生的传输。

4. Exact DebugID:29.99 → 18.76 MiB/s,但仍不可接受 ​

下一步把 KDebug filter 从较宽的 subclass:

text
0x31800000

收窄到精确 Presented Frame:

text
0x31800318

结果确实有改善:

text
约 29.99 MiB/s
→ 约 18.76 MiB/s

但仍然是数 GiB / 数分钟的级别。Census 继续观察到大量不属于 0x3180 的 perf-thread、Mach scheduler/info 等记录。

这说明:kdf2 可以筛目标 KDebug,但无法把 KPerf/KDebug trigger 伴生流诚实拆开。

因此 exact filter 只被记录为“部分改善”,没有升级为最终生产方案。

5. ta / tk 为什么没有继续穷举 ​

随后审计 DTKPKperfConfiguration 的 action / trigger 配置:

text
csd
kdf2
ta
tk
rp
bm

历史真机已经验证:

text
ta=[[3]]
→ NEGATIVE

omit ta
→ NEGATIVE

改变 tk
→ 风险高;当前证据更像 trigger-kind 选择,而不是简单降采样旋钮

最终没有找到一个同时满足以下条件的候选:

text
有协议证据
+ 尚未被 E4 证伪
+ 单变量可控
+ 合理预期降低 device→DTX 流量
+ 不改变 Presented Frame 真实语义

所以项目停止“为了继续实验而枚举私有值”。

可复用经验:不知道字段语义时,穷举私有配置不是性能优化方法;没有证据支持的候选应该停。

6. Apple Animation Hitches:很强的 Ground Truth,但不是实时轻量 source ​

Xcode 的 Animation Hitches / Frame Lifetimes 能直接描述:

  • Frame lifetime;
  • Built-In Display;
  • VSYNC;
  • presentation -time;
  • expected / actual lifetime 等高级语义。

因此它非常适合作为“Apple 自己怎么理解显示事实”的老师。

但审计 HitchesTap.framework 后发现其主要路径包含:

text
parseKTraceFile
processKTraceFile
executeStop

同时 Xcode 模板明确:

text
supportsDeferredMode  = true
supportsImmediateMode = false

2 秒真机 bounded probe 也得到:

text
hitches.recording start accepted
running messages = 0
running payload  = 0 B

结论:Hitches 保留为短时 Ground Truth / Stop 后分析老师,不承担 Perfowl 实时 FrameTime UI。

7. graphics.coreanimation.immediate / deferred:低带宽,但只有聚合 FPS ​

CoreAnimation 专用 DVT channel 很有吸引力,因为名字上看起来更接近显示管线。

5 秒真机测试:

text
startSamplingAtTimeInterval: accepted
notification                 5
raw data                     0
approx payload               440 B

样本形态稳定为:

text
IOGLBundleName
FramesPerSecond
XRVideoCardRunTimeStamp

.immediate 和 .deferred 都大约 1Hz,且没有:

text
presentation-time
swap-id
frame-seed
per-frame timestamp

结论:这是非常轻的 CoreAnimation FPS gauge ,但不满足逐帧 FrameTime 合同。低带宽与逐帧真实性必须分开验收。

8. Xcode Frame Lifetime Modeler 给出了最关键的方向证据 ​

继续逆向 coreanimation-frame-lifetime-modeler.dtac 后,发现 iOS frame-info-event 明确包含:

text
arg1 = swapID
arg2 = earliest-time
arg3 = presentation-time
arg4 = requested-time

这是一个重要转折:Apple 自己的 Frame Lifetime 模型存在直接携带 presentation-time 的原始语义。

所以新的研究目标从:

“有没有另一个 Presented Frame tick?”

转成:

“有没有低带宽实时 source,能交付 presentation-time 或已经模型化的 FrameLifetime?”

9. 通用 KTrace 能力存在,但没有独立实时 KDebug DVT channel ​

Xcode 私有框架确认存在:

text
DTKTraceTapConfig
DTKTraceTapTriggerConfig
DTKPKDebugCodeSet
setKdebugFilter
createTriggerKDebug

说明 Instruments 内部当然有精细 KTrace 能力。

但设备可见 capability 最终仍收敛到 CoreProfile 家族,没有发现一个独立的 ktracetap / kdebugtap DVT service 可以绕过 CoreProfile heavy stream。

结论:“Apple 内部有这个类”不能自动推出“远端设备公开了独立低带宽 channel”。

10. CoreProfile .deferred 也不是设备端零传输 spool ​

为了验证 service 名称里的 deferred 是否意味着“先在设备录,Stop 后再传”,做了 2 秒真机 byte census:

text
运行中 2s
DTX payload ≈ 11.33 MiB

Stop 后 2s
DTX payload ≈ 16.18 MiB

运行期已经持续收到 MiB 级 binary 数据。

结论:CoreProfile .deferred 不能作为低 DTX FrameTime source。service 名称不能代替 wire census。

11. GPU / Metal Presentation Info:Trace 有,实时 Counter service 没有 ​

GPUPlugin 中能找到:

text
frames.cpuTimestamps
frames.gpuTimestamps
frames.drawableTimestamps
frames.desiredPresentTimestamps
_agxps_trace_frame_set_presentation_info

但这些符号与:

text
_loadKTrace
containerFromKtraceFile
executeStopOnItinerary
AGXTraceConfiguration

一起出现。

Perfowl 已知实时 com.apple.instruments.server.services.gpu contract 仍然是 counters / dynamic schema,而不是逐帧 presentation callback。

结论:GPU trace 中“存在 presentation 信息”不等于 GPU Counter service 实时返回这些信息。

12. QuickTime / Valeria 屏幕视频 PTS:能看画面,但不是 Display truth ​

USB 屏幕视频流即使能提供 media sample PTS,也描述 encoder / delivery timeline,而不是 CoreAnimation compositor 的 presentation timeline。

此外常见镜像链路在 120Hz / ProMotion 场景也无法完整表达真实的动态刷新率与 presentation 节奏。

所以:

text
Video PTS
≠ Display Presentation Timestamp

它适合截图、视频和人工视觉上下文,不进入 Canonical FrameTime。

13. Full Animation Hitches 太重,Standalone Frame Lifetimes 仍值得研究 ​

一次 20 秒完整 Animation Hitches 真机录制曾产生约:

text
.trace                 ≈ 3.3 GiB
trace-data.atrc        ≈ 3.3 GiB
temporary .ktrace      ≈ 3.3 GiB
finalize                > 120s 未完成

因此完整模板不能直接嵌入 Perfowl。

但单独 Frame Lifetimes Instrument 的 Host smoke 能够独立运行,说明 Apple 的显示模型可以从完整模板中拆出来。由于当时 xctrace 真机 Online 状态反复漂移,这条路线没有被错误判成 source failure,而是保持环境门禁未关闭。

这个实验留下另一个重要经验:

USB/Lockdown 可达、CoreDevice paired、xctrace 可录制是三个不同层级。

14. hitches.recording 最终为什么被排除 ​

通过真机 DVT 直接打开:

text
com.apple.instruments.server.services.hitches.recording

channel 本身是存在的。

但进一步逆向确认它实际归属:

text
DTActivityTraceTapServiceDelegate

而不是 DTKTraceTapServiceDelegate。

连续补全 bm、s2d、predicate 等有证据的配置后,start 仍被 server cancel;同时 DTDeviceKTraceSupport 没有暴露第二套独立 KTrace DVT source。

因此 hitches.recording 不再作为 Frame Lifetime KTrace 主源继续枚举。

这一步反而指向了最后一个值得验证的方向:

既然 Hitches 本身使用 ActivityTrace 辅助信息,那么通用 ActivityTrace 里是否已经存在 CoreAnimation 自己发出的 FrameLifetime signpost?

15. 最终突破:ActivityTrace / IOMFBServer / FrameLifetime ​

使用已知可工作的通用:

text
com.apple.instruments.server.services.activitytracetap

只采 signpost:

text
onlySignposts = 1
signpostsAndLogs = 0
predicate = ""
targetPID = -3

真机稳定出现:

text
subsystem = com.apple.coreanimation
category  = IOMFBServer
name      = FrameLifetime

事件直接带:

text
begin_time
frame_seed
refresh_interval
buffer_count
end_time
prev_frame
skip_request
display_id
surface_id

5 秒真机 byte census ​

text
elapsed                   5.001s
DTX messages              20
raw data                  193,136 B
approx total payload      193,830 B
throughput                0.03696 MiB/s
all decoded signposts     481
FrameLifetime rows        90

对比旧 CoreProfile exact Presented Frame:

text
18.76 MiB/s
→ 0.03696 MiB/s

约 507× 更低
约 99.80% reduction

3 秒连续链复测 ​

text
FrameLifetime rows               60
adjacent pairs                   59
prev_frame == previous.end_time  59/59
mismatch                         0

两种 FrameTime 算法得到相同结果:

text
end_time - prev_frame

vs

end_time[n] - end_time[n-1]

中位值约:

text
49.926 ms

这同时满足了此前一直无法同时满足的四个目标:

text
真实逐帧 presentation
+ 实时 delivery
+ 不需要 CoreProfile
+ 长期低 DTX 带宽

16. 从 POC 到产品:R0-R6 ​

发现 source 后,产品化没有直接“把 URL 换掉”,而是分层收口:

阶段技术意义
R0冻结匿名 Fixture,使后续 decoder/projector 可离线确定性开发
R1ActivityTrace FrameLifetime Adapter + schema / privacy / continuity gate
R2FPS / FrameTime / P95 / Refresh / Jitter projector
R3Registry / CapturePlan 正式切 source
R4Live Frame Runtime 退出 CoreProfile continuation
R5Online Report / Offline Replay 切到 Frame Report v4
R6Swift wire、Workbench、Session Review、偏好迁移与 Display 单卡集成

这也是为什么“source POC 成功”不能直接等于“产品已经切完”。

17. R7 首轮失败:局部全绿仍然可能端到端失败 ​

R0-R6 的 Swift / Python 单测全部通过后,第一轮真实产品测试却出现:

text
点击开始
→ App 没有 cold relaunch
→ 没有任何采集数据

根因不是 ActivityTrace source,而是产品合同断层:

text
Workbench .displayPerformance
→ 请求 5 个 metricID

当时 Registry / Python Source Contract
→ 只正式拥有其中 2 个

start validation
→ invalid_capture_plan_metric_set
→ 在 ProcessControl.launch 之前被拒绝

同时 control_rejected 又因为 Session ID 尚未绑定被 Swift gate 静默过滤,所以 UI 看起来像“什么都没发生”。

这次故障留下比 source 本身更重要的工程经验:

局部组件正确,不代表产品请求闭环正确。

18. R7-Fix-A~F 做了什么 ​

5/5 Metric Contract ​

补齐:

text
ios.foreground.fps
ios.frame.time
ios.frame.time.p95
ios.display.refresh_rate
ios.frame.pacing_jitter

在 Swift Registry、Python Source Contract 与 Workbench 请求集合中的完整闭环。

Start rejection 可见 ​

握手阶段允许 sessionID == nil 的 control_rejected 被 Controller 正确消费,并让 Session 失败而不是永久等待。

产品策略与引擎能力分开 ​

底层 CoreProfile engine 可以保留 frame-only / startup-and-frame 理论模式供历史测试,但产品 scoped_feature_flags() 不再把任何 Display metric 翻译为“需要 CoreProfile Frame”。

新增跨层不变量 ​

text
Workbench metricIDs ⊆ Swift Registry
Swift Registry == Python contracts
默认 selection → migration → CapturePlan 合法
controlled-cold-launch 恰好调用一次

19. 当前仍未关闭的门禁 ​

现在的证据状态应明确分层:

层当前状态
iOS 17 FrameLifetime source POCE4
Decoder / Projector / Registry / Report / UIE2
Display-only 产品真机 start/stopPending
Startup-only CoreProfile 短时生命周期Pending 本轮 R7 Gate-0 复核
Startup + Display 并发 ownershipPending
5–20 分钟 ActivityTrace 长稳Pending
iOS 15 / 16 / 18Pending
60 / 120Hz / ProMotion 动态刷新Pending

因此当前最准确的说法不是“还在迁移”,也不是“全平台生产 E4”,而是:

当前代码主架构已经切到 ActivityTrace FrameLifetime;底层 iOS 17 source 已 E4,产品端到端与跨版本 E4 仍在继续关闭。

20. 这条路线最值得复用的经验 ​

  1. 先区分“值真实不真实”和“source 是否适合长期产品化”:CoreProfile Presented Frame 时间本身有价值,但 transport architecture 不适合长期开启。
  2. 低带宽不能替代逐帧真实性验收:CoreAnimation immediate/deferred 很轻,但只有聚合 FPS,所以不能变成 FrameTime。
  3. service 名称不是协议:.deferred、hitches.recording 都必须通过真实 wire 行为判断。
  4. Trace 里存在字段不等于实时 service 会返回字段:GPU Presentation Info 是典型例子。
  5. 媒体 PTS 不是 Display truth:视觉辅助数据和 canonical presentation timestamp 需要严格分开。
  6. 遇到私有字段未知时停止盲猜:没有 evidence-backed candidate 时,停止枚举比继续试值更专业。
  7. 端到端产品合同需要自己的测试:UI、Registry、Python source、launch state machine 必须有跨层不变量,而不能只依赖每层单测。

21. 继续阅读 ​

版本历史补记 ​

日期修订来源
2026-10-26补本月实验结论、失败路线与适用限制;早期阶段数字保留月度梳理

Perfowl · Performance Observer