Skip to content

CoreProfile Presented Frame 降源吞吐优化与 C1 Exact Filter 实验方案 ​

执行状态:C1 已完成且判定 C1-THROUGHPUT-NOT-IMPROVED;C2 ta/tk Action Contract 审计已完成。高吞吐治理历史与证伪路线冻结在第 6 节,审计结论与后续路线冻结在第 7 节。


1. 实验 1 基线与背景问题 ​

在 Build 61 落地 Startup → Frame-only 串行 handoff(rotate-to-frame-only)后,通过受控真机 Session 8ddcdd42-864e-48f3-b03f-4b8826a9bb60 建立了第一份客观 Event Census 基线:

text
Collector duration:
353.1s ≈ 5分53秒

CoreProfile total:
10.34 GiB

Epoch 2 total:
10.23 GiB

平均 CoreProfile 吞吐:
29.99 MiB/s

kd_buf totalRecords:
171,561,893 条

uniqueDebugIDCount:
179 种

0x3180 subclass 记录数:
727,867 条(占全部 0.4243%)

Presented Frame (0x31800318):
3,981 条(占全部 0.00232%,占 0x3180 仅 0.5469%)

bytes / Presented Frame:
约 2.66 MiB / 帧

当前 Frame-only 的生效配置为:

  • csd = 0(调用栈深度为 0,已排除栈展开开销)
  • kdf2 = [830472192](即 0x31800000)

核心结论与技术矛盾 ​

  1. Subclass Filter 未起到预期拦截效果: 虽然配置了 kdf2=[0x31800000],但内核驱动输出了海量与 0x3180 完全无关的事件(例如 0x25000001, 0x25000002, 0x25020010, 0x25010014, 0x01400121, 0x01400122, 0x25010004 等)。
  2. 连接稳定性威胁: 高达近 30 MiB/s 的无用内核事件洪流对宿主 CPU/USB 通道形成巨大压力,并在会话末期引发共享 DTX 通道断开: firstFatalException: ConnectionTerminatedError: Connection closed (source: stream-target-monitor)。
  3. 降源迫切性: 若能将设备侧 filter 进一步收敛,从 subclass 级收敛为精确呈现帧 DebugID,理论上可过滤掉 99.5% 以上的不相关事件。

2. C1 Exact Filter 方案设计(严格单变量) ​

2.1 变更范围与单变量原则 ​

本阶段仅在 Frame-only 阶段(Startup → Frame-only handoff 后的 Epoch 2) 允许将 kdf2=[0x31800000] 切换为 kdf2=[0x31800318]。

  • 生产默认绝对不变:当未设置环境变量或显式指定时,生产默认严格保持 subclass (0x31800000);
  • 受控实验环境变量:PERFOWL_FRAME_FILTER_EXPERIMENT=exact-debug-id;
  • 严格 Fail-Closed:若环境变量设置为任何非 subclass / exact-debug-id 的值(如 candidate-2、拼写错误),立即抛出 ValueError 终止,严禁静默回退;
  • Startup Epoch 1 绝对隔离:冷启动阶段 launch_trace=True 时,无论环境变量为何,filter 始终强制锁定为 PRESENTED_FRAME_FILTER (0x31800000),绝不污染启动生命周期探测。

2.2 遥测与对比字段 ​

在 core-profile-census.json 与 manifest.json 中完整持久化以下字段,确保真机验证时事实可溯:

json
{
  "frameFilterExperiment": "exact-debug-id",
  "requestedFrameFilter": "0x31800318",
  "effectiveFrameFilter": ["0x31800318"],
  "filters": [830472984],
  "firstFatalSource": null,
  "firstFatalException": null
}

3. 单元测试验收(Test 1 ~ Test 8) ​

在 MacApp/Sidecar/tests/test_frame_census_and_fatal_diagnostics.py 中落地 8 项专项测试:

  1. Test 1 (test_01_frame_filter_experiment_resolution_default):默认或空环境变量返回 subclass;
  2. Test 2 (test_02_frame_filter_experiment_resolution_exact):exact-debug-id 大小写与空格兼容解析;
  3. Test 3 (test_03_frame_filter_experiment_resolution_invalid_fail_closed):非法值抛出 ValueError fail-closed;
  4. Test 4 (test_04_frame_core_profile_filter_values):subclass → 0x31800000,exact-debug-id → 0x31800318;
  5. Test 5 (test_05_core_profile_filters_startup_isolation):Startup 阶段强制锁定 0x31800000,不被实验污染;
  6. Test 6 (test_06_core_profile_filters_frame_only_selection):Frame-only 阶段精确响应 subclass 与 exact-debug-id;
  7. Test 7 (test_07_extract_effective_frame_filters_helper):effectiveConfig.tc[0].kdf2 十六进制字符串转换与防御解析;
  8. Test 8 (test_08_session_c1_subclass_and_exact_end_to_end):端到端会话流下 filter 配置传递、manifest 与 census 持久化核验,以及非法变量 fail-closed 拦截。

全部 454 项 Sidecar Python 测试与 167 项 MacApp Swift 测试均 100% 通过。


4. 强制执行门禁:当前阶段停止指令 ​

本方案严格遵守门禁规则:

  • 禁止自行推演或宣布 C1 成功;
  • 禁止一次性实现 candidate-2 或其他猜测性 filter;
  • 必须构建包含 C1 实验能力的交付包,并绝对停止等待用户 5 分钟真机验证。

5. 2026-09-17 Implementation Update · Launch Injection 架构修复 ​

5.1 Attempt #1 判定与根因分析 ​

真机测试 Session 4399abb7-34b5-4cde-bb48-2d3d47160eba 记录显示:

  • frameFilterExperiment: subclass
  • requestedFrameFilter: 0x31800000
  • effectiveFrameFilter: ["0x31800000"]

判定结论:Attempt #1 为 INVALID(未激活)。 根因:macOS 下从 Finder/Dock 双击启动 App 时,进程属于 launchd 托管,不会继承终端中的 export 环境变量;而 CollectorSupervisor 直接复制宿主环境,导致子进程未能感知终端设置的 PERFOWL_FRAME_FILTER_EXPERIMENT=exact-debug-id。

5.2 显式注入与防污染安全链路 ​

为彻底脱离终端环境变量依赖,建立强类型端到端控制管道:

  1. 类型定义与配置载体:
    • CollectorSupervisor.swift 定义 public enum FrameFilterExperiment: String, Sendable, Codable, Equatable { case subclass, exactDebugID = "exact-debug-id" };
    • CollectorLaunchConfiguration 扩展可选属性 public let frameFilterExperiment: FrameFilterExperiment?。
  2. 防污染安全环境构造:
    • 提取纯函数 CollectorSupervisor.environment(for:base:):
      • 若 configuration.frameFilterExperiment != nil,显式写入 PERFOWL_FRAME_FILTER_EXPERIMENT;
      • 若为 nil,显式执行 environment.removeValue(forKey: "PERFOWL_FRAME_FILTER_EXPERIMENT"),彻底清理宿主环境污染,保障生产默认。
  3. C1 实验构建注入:
    • PerformanceSessionController 增加 @Published public var frameFilterExperiment: FrameFilterExperiment? = .exactDebugID;
    • 在 start(plan:) 与 prewarm 中均通过 CollectorLaunchConfiguration 显式下发。
  4. 早期诊断与 UI 反馈:
    • Sidecar CLI 在 hello 发出后立即 emit frame_filter_experiment_configured;
    • stream_candidate_session 在 provider 租约建立后立即 emit 同款诊断;
    • Swift 端消费事件并实时展示 帧耗时实验配置:\(exp) (\(req)),在 Epoch 2 开始前即可肉眼确认配置生效。

5.3 验证结果与 20 秒真机预检门禁 ​

  • 单元测试:
    • Swift DeviceConnectionServiceTests.swift 补充 4 项配置注入与清理单测,171/171 项全绿;
    • Python test_perfowl_realtime.py 补充早期 wire event 断言,454/454 项全绿。
  • 真机预检要求:
    • 用户先运行 10~20 秒短 Session,核验 manifest.json 与 core-profile-census.json 中:
      1. frameFilterExperiment == "exact-debug-id"
      2. requestedFrameFilter == "0x31800318"
      3. effectiveFrameFilter == ["0x31800318"]
    • 确认三项全部生效后,再执行 5 分钟正式验证。

6. 高吞吐治理尝试与经验账本(长期保留) ​

本节不是临时实验记录,而是 CoreProfile / DTX 长稳优化的经验基线。后续任何 AI 或开发者在提出新的降吞吐方案前,必须先对照本节,禁止重复已证伪方向。事实与推测严格分开:只有真机 Session、effective config、Census 或历史 E4 证据支持的内容才记为“已确认”。

6.1 问题画像:FrameTime 的有效信息密度极低 ​

Build 61 后,FrameTime 已能在 Startup → Frame-only 串行 handoff 后持续采集,但长测出现新的主矛盾:为了得到极少量 Presented Frame (0x31800318) 时间戳,CoreProfile 通过 DTX 搬运了数 GiB 的 kd_buf 数据。

实验 1 基线 Session 8ddcdd42-864e-48f3-b03f-4b8826a9bb60:

指标结果
Collector duration353.1 s
Epoch 2 数据量10.23 GiB
平均吞吐29.99 MiB/s
kd_buf 总记录171,561,893
0x3180 subclass727,867(0.4243%)
0x31800318 Presented Frame3,981(0.00232%)
bytes / Presented Frame约 2.66 MiB
first fatalstream-target-monitor → ConnectionTerminatedError: Connection closed

这证明高吞吐主要发生在 device → Instruments/CoreProfile → DTX → Mac 的源传输链路,而不是产品 JSON、磁盘 Raw retention 或 FrameTime 后处理造成的膨胀。

6.2 尝试 A:仅减少宿主侧 Raw retention —— 不能解决根因 ​

历史 Raw Retention 优化可以显著减少磁盘占用,但不会减少 iPhone 已经通过 DTX 发送到 Mac 的字节数。因此以下方案只属于“存储优化”,不能作为本问题的降源修复:

  • derivedOnly / not-retained;
  • Host 收到 chunk 后丢弃;
  • 更快的 JSON writer / batch writer;
  • 压缩、checkpoint、减少分析状态落盘。

经验结论: 判断降吞吐是否有效,必须看 coreProfileObservedByteCount / Epoch MiB/s,不能看 Session 目录大小。

6.3 尝试 B:callstackDepth 128 → 0 —— 排除调用栈,但未降低主吞吐 ​

早期 Frame-only restart 曾漏传 callstackDepth,导致 pymobiledevice3 使用默认 csd=128。随后修正为:

text
callstackDepth = 0

真机 B 组 Session 6f4a5fd5-5ba5-4961-af95-6c70463b26b5 确认 Epoch 2 csd=0,但仍产生约 16.44 GiB Epoch 2 数据,平均约 33 MiB/s,并最终失败。

结论: csd=128 不是高吞吐主放大器;csd=0 应继续保留,但不能再把“关栈”作为本问题的主要优化方向。

6.4 尝试 C:Event Census —— 找到真正的信息密度问题 ​

为避免继续猜测,增加了只读 Event Census。实验 1 直接证明:

  • 0x31800318 只占全部 kd_buf 的约 0.00232%;
  • 大量主导事件甚至不属于 0x3180;
  • Top debugID 长期稳定集中在 0x25xxxxxx 与 0x0140xxxx。

典型高频项:

text
0x25000001
0x25000002
0x25020010
0x25010014
0x25010004
0x01400121
0x01400122
0x01400080
0x01400008
0x01400060

同时,171,561,893 × 64 B 与 Epoch 2 观测字节量基本相符,进一步证明巨量传输就是 kd_buf 本身,而非外围格式开销。

经验结论: Event Census 是此类私有 Instruments 配置实验的必要门禁;任何“减少了某类事件”的结论必须用完整 debugID 分布验证。

6.5 尝试 D:C1 Exact kdf2=0x31800318 —— 配置有效,但主噪声仍存在 ​

C1 的目标是把 Frame-only 的:

text
kdf2 = [0x31800000]

收窄到:

text
kdf2 = [0x31800318]

第一次 Attempt 4399abb7-34b5-4cde-bb48-2d3d47160eba 因 macOS GUI App 未继承终端环境变量,实际仍是 subclass,判定为 INVALID / NOT ACTIVATED。这次失败推动了 CollectorLaunchConfiguration → CollectorSupervisor → Collector Core 的显式实验配置注入链路,并增加 requested/effective 早期门禁。

真正有效的 C1 长测 Session:

text
e134a667-7e70-40a4-82df-3cb4c9582545

设备实际确认:

text
frameFilterExperiment = exact-debug-id
requestedFrameFilter = 0x31800318
effectiveFrameFilter = [0x31800318]
csd = 0

最终结果:

指标C1
Collector duration192.1 s
Epoch 2 duration178.1 s
Epoch 2 数据量约 3.34 GiB
Epoch 2 平均吞吐18.76 MiB/s
kd_buf 总记录54,545,945
Presented Frame1,546
有效 FrameTime1,545
Presented Frame 占全部约 0.0028%
droppedEventHighWater0
first fatalstream-target-monitor → ConnectionTerminatedError: Connection closed

相对 29.99 MiB/s 基线仅下降约 37%,远低于 ≥70% 的最低目标;而 0x25xxxxxx / 0x0140xxxx 仍是 Top debugID。

已确认结论:

  1. Instruments Server 接受并保留 exact 0x31800318;
  2. Presented Frame 与 FrameTime 在 exact filter 下仍然真实连续;
  3. kdf2 精确化能降低一部分流量,但不是控制主要 0x25 / 0x0140 洪流的配置面;
  4. C1 判定为 C1-THROUGHPUT-NOT-IMPROVED,不再继续枚举其他 debugID。

6.6 尝试 E:历史 ta 降噪 —— 两个最直接候选均已被 E4 证伪 ​

项目历史文档 p2-startup-kdebug-only-trigger-v1-plan.md 已做过同类 CoreProfile trigger 降噪真机实验,必须作为当前 C2 的前置证据复用。

历史默认主 trace:

text
tk = 3
ta = [[3], [0], [2], [1,1,0]]

当时设备窗口同样被:

text
0x0140
0x2500–0x2502

大量占据。

候选 E1:ta=[[3]] ​

真机结果:仍产生大量 0x0140/0x2500/0x2501,且 Startup 只形成 6 个阶段 Span。

判定:NEGATIVE。禁止重复。

候选 E2:完全省略 ta ​

真机总记录下降到 150,749 条,但 0x31CA 仍有 113,504 条,Startup 同样只形成 6 个 Span。

判定:NEGATIVE。禁止重复。

注意:这两次历史实验针对 Startup 语义,不等于已经证明 Frame-only Presented Frame 在所有 ta 组合下都无解;但它已经足以证明“简单认为 ta 某项就是 KPerf 噪声开关”是不可靠的,后续不得再次盲猜 ta=[[3]] 或 ta=[]/omit。

6.7 自动停止现象的当前证据边界 ​

多轮长测出现不同底层表现:

  • 有的 Session 在 macOS TCP 层观察到 peer RST;
  • 有的 Session TCP 正常收尾,但上层先出现 QueueShutDown;
  • C1 有效长测中 first fatal 仍是 stream-target-monitor 首先观察到共享 DTX Connection closed。

因此当前可以确认的是:

数分钟、数 GiB 级持续 CoreProfile 流量与共享 DTX 会话失效高度相关,并已反复复现;但不同 Session 的最底层关闭机制并不完全一致,不能统一描述成“TCP RST 根因”。

Transport Recovery 属于后续保险机制,不能代替降源;在降源实验完成前禁止通过自动重连掩盖 source load 问题。

6.8 已冻结的优化原则 ​

后续 CoreProfile 高吞吐治理必须遵守:

  1. 先 Source Census,再修改私有配置。
  2. requested config 与 effective config 双记录,设备 effective 为真值。
  3. 一次只验证一个最小假设,短测通过后才长测。
  4. 评价指标是 device→Mac 实际观测 MiB/s,而不是磁盘大小。
  5. 必须同时保护 FrameTime 真实性:不允许 1000/FPS、旧值 replay 或跨 epoch interval。
  6. 已证伪方向禁止重跑:Host retention、仅 csd、继续猜 kdf2 debugID、ta=[[3]]、省略 ta。
  7. 如果 ta/tk 合同最终证明无法把 Presented Frame 与海量 KPerf 伴生流拆开,应明确判定 CoreProfileSessionTap 不适合作为长时间 FrameTime source,并切换数据源研究,而不是无限枚举私有配置。

6.9 当前下一步 ​

C1 已完成并关闭。下一阶段只做 C2 CoreProfile Action Contract Audit:

text
重点:tc.ta / tc.tk / 主 trace 与 sampling trigger 的真实协议语义
目标:找到一个有源码/历史真机/Apple Instruments 静态证据支持、且尚未被历史 E4 证伪的最小单变量候选

在审计完成前,不新增真机穷举候选。


7. C2 CoreProfile Action Contract 审计结论 ​

审计目标不是为完成实验数量而强行找一个新的 ta/tk 组合,而是判断 tc.ta / tc.tk 是否存在一个有协议证据、尚未被历史 E4 证伪、并能保持 Presented Frame 真实性的最小单变量候选。审计来源包括:当前 pymobiledevice3 11.3.1、Perfowl 现有适配层、历史 Startup E4、当前 Xcode DVTInstrumentsFoundation 私有框架静态符号/反汇编,以及 C1 真机 Census。

7.1 pymobiledevice3 当前公开合同非常有限 ​

CoreProfileSessionTap 的默认主 trace 配置为:

text
tc[0] = {
    csd: 128,
    kdf2: filters,
    ta: [[3], [0], [2], [1,1,0]],
    tk: 3,
    uuid: ...
}
rp = 100
bm = 0

上游源码对 ta 只注释为 Actions,对 tk 只注释为 Kind,没有公开枚举或 action 数字语义。因此:

  • 不能从 pymobiledevice3 本身证明 [3] / [0] / [2] / [1,1,0] 分别是什么 sampler;
  • 不能仅凭 tk=3 的数字值自行命名 trigger 类型;
  • 任何直接删除某个 action 的方案若无额外证据,均属于猜测性私有协议枚举。

7.2 Xcode 私有框架确认:CoreProfile KDebug 路径属于 KPerf 配置体系 ​

当前本机 Xcode 的 DVTInstrumentsFoundation 静态证据明确包含:

text
DTKPKperfConfiguration
DTKPTriggerKDebug
DTKPTriggerTime
DTKPTriggerPMI
createTriggerKDebug:
createTriggerTime:
createTriggerPMI:
setKdebugFilter:
createKperfFilter
kperf_buffer_create_live
kperf_buffer_set_config
kperf_logging_start
kperf_sample_on
com.apple.instruments.server.services.coreprofilesessiontap

同时存在错误/诊断字符串:

text
Unsupported trigger kind: %u
Error allocating a Kdebug trigger action
Error allocating a PMI trigger action
Error allocating a Time trigger action
A kdebug trigger already exists. DTKPKperfConfiguration doesn't support creation of multiple kdebug triggers.

这把此前的“可能”提升为较强静态证据:

coreprofilesessiontap 的 KDebug 采集并不是独立于 KPerf 的纯 typefilter socket;Apple Instruments 内部使用 DTKPKperfConfiguration 管理 KDebug/Time/PMI trigger,并通过 kperf live buffer/logging 基础设施交付数据。

因此 C1 中即使 kdf2 精确到 0x31800318,仍然收到大量 0x25xxxxxx / 0x0140xxxx,与该内部架构是自洽的。

7.3 trigger kind 的内部枚举映射与 wire tk 边界 ​

对当前 Xcode 私有框架的三个 triggerKind 实现做最小反汇编,得到:

text
-[DTKPTriggerTime triggerKind]   -> 0
-[DTKPTriggerPMI triggerKind]    -> 1
-[DTKPTriggerKDebug triggerKind] -> 2

而 CoreProfile wire config 使用:

text
sampling trigger: tk = 1
main trace:       tk = 3

二者不能直接按同一个数字枚举解释。当前最合理的协议解释是 wire 值可能采用 internalKind + 1 编码:

text
wire tk=1 -> internal Time(0)
wire tk=2 -> internal PMI(1)
wire tk=3 -> internal KDebug(2)

该映射与现有使用形态高度吻合:sampling trigger 带 si 周期字段,而主 trace 带 kdf2 KDebug filter。

但目前尚未取得 Instruments Server 对 wire dictionary 的 parser 反汇编来直接证明 +1 映射,因此正式置信度标记为:

text
LIKELY,非 CONFIRMED

关键工程结论不依赖该最后一步完全确认:tk=3 不应被当作可随意降低的“采样等级”参数。 改成 1/2 很可能是在切换 trigger 类型,而不是在降低同一种 KDebug trigger 的噪声;这会破坏 Presented Frame source contract,因此不构成安全 C2 候选。

7.4 ta 的证据边界 ​

现有证据只能确认 ta 是 trigger actions 配置的一部分,不能把具体数组项安全映射到公开 action 名称。

已知两组结构:

text
main KDebug trace:
ta = [[3], [0], [2], [1,1,0]]

5ms / 50ms sampling auxiliary:
ta = [[0], [2], [1,1,0]]

唯一结构差异是主 trace 多了 [3],但这不能证明 [3] 就是 KDebug-only action 或 perf-noise action。历史 E4 已直接否定这种简单推断:

text
ta=[[3]]
→ 仍大量 0x0140 / 0x2500 / 0x2501
→ NEGATIVE

完全省略 ta
→ 记录总量下降,但仍有大量非业务记录且 Startup 语义残缺
→ NEGATIVE

所以当前没有任何一个单独 ta[i] 具备足够证据支持“删除它即可关闭 0x25/0x0140 洪流,同时保留 Presented Frame”。

7.5 高频 debugID 的语义进一步支持 KPerf-backed 判断 ​

历史 Perfowl 解码与公开 py-ios-device 输出已经确认若干代表项:

text
0x25010004 = PERF_THD_Data
             per-thread info / PID-TID attribution

0x25010014 = PERF_THD_CSwitch
             perf thread context-switch 类记录

0x01400080 = MACH_DISPATCH
             DBG_MACH / scheduler 类记录

C1 exact-filter Census 中这些类别仍持续高频出现,而它们并不属于 0x3180。这说明主导字节量不是 Presented Frame subclass 本身,而是 KPerf/KDebug trigger 运行时伴随产生/交付的 perf-thread、Mach scheduler/info 数据。

此外,历史 Startup 审计已经独立观察到:0x07 / 0x25 等 class 不在业务 kdf2 集合里仍会大量进入 CoreProfile,且此前已把“只收窄 lifecycle kdf2 可以解决压力”判为边际收益有限。C1 现在用 Frame-only exact debugID 再次验证了同一事实。

7.6 Action Contract 状态表 ​

配置当前值审计结论置信度是否值得继续真机枚举
csd0callstack depth;已排除主放大器CONFIRMED否
kdf2[0x31800318]KDebug filter;C1 已证可 exact,但不能滤掉主伴生流CONFIRMED否
tk3wire trigger kind;高度吻合 KDebug trigger,改变值可能切换 Time/PMI/KDebug 类型LIKELY否,除非获得 parser 级证据
ta[0][3]action;具体 sampler 名称未知;单独保留已 E4 失败UNKNOWN/NEGATIVE否
ta[1][0]action;具体语义未公开UNKNOWN无证据,不跑
ta[2][2]action;具体语义未公开UNKNOWN无证据,不跑
ta[3][1,1,0]action;具体语义未公开UNKNOWN无证据,不跑
rp100recording priorityCONFIRMED(上游注释)否
bm0buffer modeCONFIRMED(历史实验)只影响缓冲策略,不作为降源候选

7.7 C2 审计最终判定:当前不存在值得执行的 ta/tk 单变量真机候选 ​

本轮审计没有找到满足以下全部条件的候选:

text
有协议/静态证据
AND 尚未被历史 E4 证伪
AND 只改一个最小变量
AND 能合理预期降低 device→DTX 流量
AND 不改变 Presented Frame 的真实来源语义

因此不能为了“继续 C2”而制造一个猜测性的 candidate-1。

正式判定:

text
C2-ACTION-CONTRACT-AUDIT: COMPLETE
C2-TA/TK-CANDIDATE: NO-EVIDENCE-BACKED-CANDIDATE

7.8 对 CoreProfileSessionTap 作为长期 FrameTime source 的阶段性结论 ​

证据链现在为:

text
Host Raw retention
    NEGATIVE(不降 device→Mac 流量)

csd 128 → 0
    NEGATIVE(仍 20~33 MiB/s)

subclass kdf2 → exact 0x31800318
    PARTIAL only(29.99 → 18.76 MiB/s,仍数 GiB/数分钟)

ta=[[3]]
    NEGATIVE(历史 E4)

omit ta
    NEGATIVE(历史 E4)

改变 tk
    NOT SAFE:当前证据表明它是 trigger-kind 选择,而非同类 trigger 的轻量级旋钮

因此当前最有根据的阶段性判断是:

在已知 CoreProfileSessionTap / DTKPKperfConfiguration 合同下,没有找到能够把 Presented Frame 与 KPerf-backed 大流量伴生记录诚实拆开的配置。继续穷举 ta/tk 的收益已经低于风险。

这不是声称 Apple 内部绝对不存在未公开配置,而是说明现有代码、历史真机和当前 Xcode 静态证据不足以支持下一次真机私有枚举。

7.9 下一步路线冻结 ​

下一阶段停止 ta/tk/kdf2 穷举,转为两个独立方向:

  1. Frame timestamp 替代数据源研究(优先)
    • 搜索能够直接产出 display / compositor / presented-frame timestamp 的 Instruments/DVT source;
    • 目标是不占用 CoreProfile KPerf heavy stream;
    • 仍要求真实时间戳,不接受 1000/FPS。
  2. Transport Recovery(次级保险,单独立项)
    • 解决任意 DTX source Connection closed 时不应让整个 Session 无条件死亡的问题;
    • 必须记录 honest gap / sourceEpoch / reconnectGeneration;
    • 不得用重连掩盖高吞吐根因。

在找到新的 Frame timestamp source 前,当前 exact-debug-id 仅保留为实验事实,不提升为“吞吐修复完成”的生产结论。


8. FrameTime 替代源 POC 阶段记录 ​

8.1 Stage 1 · Animation Hitches Ground Truth 环境预检(2026-09-17) ​

目的:先使用 Apple xctrace 的 Animation Hitches 模板在真机上建立 Frame Lifetimes / Built-In Display / VSYNC Ground Truth,再据此反推可替代长期 CoreProfileSessionTap 的轻量 DVT source。

公开/本机静态依据:

  • Apple Animation Hitches 明确包含 Frame Lifetimes 与 Built-In Display 轨道;Built-In Display 展示真正出现在显示器上的帧与 VSYNC。
  • 当前 Xcode 的 instrument inventory 包含 Display、Frame Lifetimes、Hitches。
  • 当前 Xcode DVTInstrumentsFoundation 中存在独立服务名:
    • com.apple.instruments.server.services.hitches.recording
    • com.apple.instruments.server.services.graphics.coreanimation
    • com.apple.instruments.server.services.graphics.coreanimation.immediate
    • com.apple.instruments.server.services.graphics.coreanimation.deferred
  • py-ios-device 的 display 已源码确认仍调用 coreprofilesessiontap 并从 0x31800318 计算帧间隔,因此不是替代源,禁止把它重新包装成新方案。

环境预检方法:

text
xcrun xctrace list devices
pymobiledevice3 usbmux list
检查 Perfowl prewarm / tunnel 进程

结果:

  • pymobiledevice3 usbmux list 能看到 USB 真机,Perfowl prewarm Collector 也已连接其中一台 iOS 17 真机;
  • 但本轮 xcrun xctrace list devices 将全部 iPhone 列为 Devices Offline,因此当前时刻无法可靠启动真机 Animation Hitches trace;
  • 该结果属于 E3 环境阻塞,不能据此否定 Animation Hitches 数据源本身。

阶段经验:

  1. xctrace 与 Perfowl/pymobiledevice3 的“设备可达”不是同一状态,不能用其中一方在线推断另一方必然可录制;
  2. Ground Truth 路线必须把 xctrace device online 作为独立前置门禁;
  3. 在该门禁恢复前,不等待环境,继续通过本机 Xcode 私有框架 + DVT 动态服务探测完成协议 POC;
  4. 后续一旦 xctrace 真机恢复在线,再补 20~30 秒 trace 做 E4 对照,不需要重做前面的协议调查。

Stage 1 状态:

text
ANIMATION-HITCHES-GROUND-TRUTH:
BLOCKED-BY-XCTRACE-DEVICE-OFFLINE

ALTERNATIVE-SOURCE-POC:
CONTINUE

下一步:直接审计并探测 hitches.recording / graphics.coreanimation 的 channel、selector、启动配置与回包形态;若能以独立 DVT service 低流量得到 display/presentation timestamp,则进入最小 Adapter POC。

8.2 Stage 2 · HitchesTap 协议与实时性审计(2026-09-17) ​

目的:判断 com.apple.instruments.server.services.hitches.recording 是否能直接作为 Perfowl 的实时逐帧 FrameTime source,而不是仅作为停止录制后的离线分析源。

本机 Xcode 静态证据:

  • Instruments 自带独立插件 HitchesTap.framework,并存在 Hitches.instrdst、Animation Hitches.tracetemplate 与 coreanimation-frame-lifetime-modeler.dtac;
  • HitchesTap.framework 直接链接 SignpostSupport.framework,并显式引用:
    • SignpostFrameLifetimeInterval
    • SignpostFrameOverrunInterval
    • SignpostUpdateSequenceInterval
    • handleSignpostInterval: / handleSignpostIntervalBegin:
  • XRHitchesTapAgent 的自定义执行路径明确包含:
    • parseKTraceFile
    • processKTraceFile(...)
    • executeStop(on:)
  • processKTraceFile(...) 的 Swift 符号签名直接返回 SignpostFrameLifetimeInterval 集合;XRHitchesTapAgent 未发现对应的 Hitches 专用实时 executeStart 数据处理实现。
  • Hitches/CoreAnimation schema 明确存在真实逐帧模型字段:start、duration、display-id、lifetime-id (Frame #)、swap-id、frame-seed、hitch-duration、acceptable-latency、render-start、render-duration 等;这些字段非常适合作为 FrameTime 真值对照。

动态环境旁证:

  • USB Lockdown 能识别当前 iOS 17 真机,但直接通过裸 Lockdown 构造 DVTSecureSocketProxyService 返回 InvalidService;
  • Perfowl 当前 prewarm 则能通过其 iOS 17+ modern RSD/native route 建立 DVT,因此“裸 Lockdown DVT 探测失败”只说明路由不匹配,不能用于判定 hitches.recording 服务不存在。

阶段判定:

text
HITCHES-FRAME-LIFETIME-TRUTH:
STRONG-CANDIDATE

HITCHES-RECORDING-AS-REALTIME-FRAMETIME-SOURCE:
NOT-SELECTED / POST-STOP-KTRACE-PROCESSING

当前证据表明 Animation Hitches 的 Frame Lifetime 数据具有很强的逐帧真值价值,但 Hitches 特有处理主要发生在停止录制后的 ktrace/signpost 解析阶段。它适合作为后续 xctrace E4 Ground Truth Teacher,不适合直接承担 Perfowl 约 1 Hz UI 更新背后的实时 FrameTime 流。

阶段经验与冻结边界:

  1. “独立 DVT service 名”不等于“实时 callback source”;必须继续区分设备端录制、bulk/file spool 与实时 channel;
  2. Frame Lifetime schema 已证明 Apple 内部存在我们需要的 display/frame lifetime 真值,因此替代源研究方向成立;
  3. hitches.recording 不再作为第一实时 Adapter 候选,避免把离线 trace parser 错包装成实时源;
  4. 后续 xctrace 恢复在线后,只需要用 Animation Hitches 做 20~30 秒真值对照,不需要重新证明 schema;
  5. Stage 3 转向 com.apple.instruments.server.services.graphics.coreanimation(.immediate),重点验证它是否是轻量实时 DTTap callback,是否能产出 frame/display/presentation 级事件。

下一步:使用 Perfowl 自己的 iOS 17+ modern provider route 做 graphics.coreanimation.immediate 最小动态 channel POC;先只验证 channel 与已知 selector,不修改产品默认采集链路。

8.3 Stage 3A · CoreAnimation Frame-Info 真值发现与 DVT Channel 存在性真机探测(2026-09-17) ​

目的:在不启动任何新采集任务、不发送 setConfig:/start 的前提下,先回答两个问题:Apple Frame Lifetime 的 iOS 原始事件是否直接携带 presentation 时间;graphics.coreanimation(.immediate/.deferred) 是否确实是当前真机 Instruments Server 注册的可打开 DVT channel。

静态真值发现(E1):当前 Xcode coreanimation-frame-lifetime-modeler.dtac 的 iOS 规则 frame-info-event 明确消费 CoreAnimation KDebug subclass=0xca, code=0x0b, function=0,其原始参数定义为:

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

Apple modeler 使用设备 mach timebase 将 earliest-time / presentation-time / requested-time 转成 trace-relative 时间,并生成 coreanimation-lifetime-interval。因此新的目标不再只是寻找另一个“Presented Frame tick”,而是优先寻找能够直接获得 frame-info.presentation-time 的轻量实时源;它比当前 0x31800318 相邻时间戳做差更接近 Apple 自己的 Frame Lifetime 真值模型。

对应 binding 还明确依赖:

text
kdebug codes = 0x31,0xca
display-surface-swap

这同时说明 Frame Lifetimes 最终仍建立在 CoreAnimation/KDebug 原始事件上;后续必须验证新的 DVT service 是否已经在设备端做了筛选/模型化,不能仅凭 service 名字假设其天然低流量。

真机最小探测(E4):使用 Perfowl 现有 iOS 17+ 路由:

text
USB 真机 A
→ open_service_provider(use_native=true)
→ tunneld-native-rsd
→ DvtProvider
→ 仅 open_channel

本阶段没有调用 setConfig:、start、startSamplingAtTimeInterval:,打开后立即由 DVT owner 统一关闭连接,避免产生新的采集压力。

真机返回:

text
com.apple.instruments.server.services.graphics.coreanimation.immediate  OPEN
com.apple.instruments.server.services.graphics.coreanimation.deferred   OPEN
com.apple.instruments.server.services.graphics.coreanimation            OPEN
com.apple.instruments.server.services.hitches.recording                  OPEN

阶段结论:

text
COREANIMATION-FRAME-INFO-PRESENTATION-TIME:
CONFIRMED-IN-XCODE-MODELER (E1)

COREANIMATION-DVT-CHANNELS-REGISTERED-ON-IOS17:
PASS (E4)

REALTIME-PER-FRAME-PAYLOAD:
NOT-YET-PROVEN

阶段经验:

  1. graphics.coreanimation(.immediate/.deferred) 与 hitches.recording 已由真机证明是实际注册服务,不再只是 Xcode 静态字符串;
  2. Apple Frame Lifetime 的 iOS 原始 frame-info 本身直接包含 presentation time,这是当前替代源调查中比 0x31800318 更强的真值线索;
  3. 仅能打开 channel 仍不能证明它会实时输出逐帧消息,也不能证明低带宽;下一阶段必须分别验证 selector、消息形态与真实 bytes/s;
  4. 为避免再次制造高流量,Stage 3B 必须从 immediate 单一路径开始,短时、单 channel、先收首包/超时即停,禁止同时启动多个 CoreAnimation service。

下一步:反查 graphics.coreanimation.immediate 的 selector/config 合同,然后只在真机 A 上启动一个 5~10 秒最小流,记录首包 latency、消息数、总字节、payload 结构以及是否直接包含 presentation/swap/frame 字段;完成后先写阶段记录,再决定是否进入 Adapter POC。

8.4 Stage 3B · graphics.coreanimation.immediate 5 秒实时流真机验证(2026-09-17) ​

目的:验证 graphics.coreanimation.immediate 是否是可实时使用的低流量 source,并确认它是否直接提供逐帧 presentation/swap/frame timestamp。

方法(E4):

text
真机 A / iOS 17
→ tunneld-native-rsd
→ DvtProvider
→ open_channel(graphics.coreanimation.immediate)
→ startSamplingAtTimeInterval:(0.0)
→ 单 channel 收取 5 秒
→ stopSampling

本轮没有打开 CoreProfile,也没有同时启动 .deferred / Hitches;探针同时监听 DTX dispatch、notification 与 raw data,并对 payload 做最小计数。

真机结果:

text
startSamplingAtTimeInterval:  ACCEPTED
startError                    none
elapsed                       5.007 s
notification                  5
DTX dispatch                  0
raw data                      0
approx payload bytes          440 B

样本形态稳定为约 1 Hz 的轻量字典:

text
{
  IOGLBundleName: Built-In,
  FramesPerSecond: 19~20,
  XRVideoCardRunTimeStamp: ...
}

本轮没有观察到 presentation-time、swap-id、frame-seed、逐帧 timestamp 或等价 per-frame payload。

阶段判定:

text
COREANIMATION-IMMEDIATE-REALTIME:
PASS (E4)

COREANIMATION-IMMEDIATE-BANDWIDTH:
VERY-LOW / ~5 small notifications in 5s

COREANIMATION-IMMEDIATE-AS-FRAMETIME-SOURCE:
NEGATIVE — AGGREGATED FPS ONLY

经验总结:

  1. .immediate 的 channel 与 Graphics 风格 selector 已由真机确认,可作为低开销 CoreAnimation/FPS source;
  2. startSamplingAtTimeInterval: 0.0 并不意味着逐帧 callback,在当前真机上实际仍约 1 Hz 汇总;
  3. XRVideoCardRunTimeStamp 是采样运行时间戳,不能因为名字里有 TimeStamp 就当作每帧 presentation time;
  4. “低带宽”与“逐帧真实性”必须分别验收;本阶段只满足前者,不满足 FrameTime 合同;
  5. 不把 .immediate 包装成 FrameTime fallback,也不使用 1000/FPS 伪造 FrameTime。

下一步:先验证 .deferred 是否只是同一聚合源的延迟展示版本;若同样只有 FPS 汇总,则停止在 graphics.coreanimation 家族继续猜 selector,转向 Apple Frame Lifetime modeler 已确认的 0x31/0xca frame-info 原始事件,寻找能够只订阅该 CoreAnimation KDebug class/subclass 的轻量 recording/tap source。

8.5 Stage 3C · graphics.coreanimation.deferred 对照真机验证(2026-09-17) ​

目的:确认 .deferred 是否隐藏了 .immediate 没有暴露的逐帧数据,避免过早放弃 CoreAnimation 专用服务。

方法(E4):保持与 Stage 3B 相同条件,只把单一 channel 改为:

text
com.apple.instruments.server.services.graphics.coreanimation.deferred

仍使用 startSamplingAtTimeInterval: 0.0,只采 5 秒,并监听 dispatch / notification / raw data。

结果:

text
startSamplingAtTimeInterval:  ACCEPTED
elapsed                       5.005 s
notification                  5
DTX dispatch                  0
raw data                      0
approx payload bytes          440 B

payload 与 .immediate 同构:

text
IOGLBundleName = Built-In
FramesPerSecond = 19(首样本 0)
XRVideoCardRunTimeStamp = 约 1 秒递增

同样未出现 per-frame presentation-time / swap-id / frame-seed。

阶段判定:

text
COREANIMATION-DEFERRED-REALTIME:
PASS (E4)

COREANIMATION-DEFERRED-AS-FRAMETIME-SOURCE:
NEGATIVE — SAME AGGREGATED FPS CONTRACT

GRAPHICS.COREANIMATION-SAMPLING-FAMILY-AS-FRAMETIME:
CLOSED / DO-NOT-ENUMERATE-FURTHER

经验总结:

  1. .immediate / .deferred 在当前真机上的实时 sampling contract 均为低带宽 FPS 汇总,并非逐帧事件;
  2. 二者名字里的 immediate/deferred 不应被解释为“逐帧立即/延迟事件”,至少 startSamplingAtTimeInterval: 路径不是;
  3. 已有两次 E4 足以停止继续对 graphics.coreanimation 猜其他 interval 值或把采样频率强行调高;
  4. 下一阶段只研究 Apple modeler 明确依赖的 CoreAnimation frame-info 原始事件,以及有没有比 CoreProfileSessionTap 更轻的 KDebug/ktrace delivery source。

下一步:确认 frame-info 的完整 DebugID、Apple Instrument 对该输入表的采集配置,以及 Xcode 是否存在专门的 KDebug recording/tap 服务可以只采 0x31/0xca,再决定是否值得做下一次 5~10 秒真机流量实验。

8.6 Stage 4 · Frame Lifetime 原始输入与轻量 KTrace delivery 审计(2026-09-17) ​

目的:在 graphics.coreanimation 采样家族已由 E4 证伪为 1 Hz FPS 汇总后,继续追 Apple Frame Lifetimes 真值模型的真实输入,并判断是否存在一个独立于 CoreProfileSessionTap heavy KPerf 流的实时 KDebug/KTrace delivery source。

Frame Lifetime 原始事件合同(E1,当前 Xcode 静态证据):

coreanimation-frame-lifetime-modeler.dtac 的 iOS frame-info-event 明确消费:

text
KDebug:
class/subclass binding = 0x31 / 0xca
subclass              = 0xca
code                  = 0x0b
function              = 0

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

Apple modeler 使用设备 mach timebase 将 earliest-time / presentation-time / requested-time 转成 trace-relative 时间,并直接形成 coreanimation-lifetime-interval。因此 arg3 presentation-time 是目前找到的最强逐帧真值候选。按标准 KDebug 编码推导,该 frame-info DebugID 可能为 0x31CA002C,但在当前阶段只标记为 DERIVED / NOT-YET-E4-CONFIRMED,不得当作真机已确认常量。

Frame Lifetime modeler 的完整输入还包括:

text
kdebug 0x31/0xd1
kdebug 0x31/0xd0
kdebug 0x31/0xca
display-surface-swap
time-info
tick

这说明 Apple 的完整 Frame Lifetime 是多个输入表模型化的结果;若 Perfowl 仅需要 FrameTime = presentation[n] - presentation[n-1],理论上可以只寻找 frame-info.presentation-time 的 delivery,不必复制完整 Hitches/Frame Lifetime 分析栈。

通用 KTrace Tap 审计(E1):

当前 DVTInstrumentsFoundation 中确认存在独立内部类型:

text
DTKTraceTapConfig
DTKTraceTapTriggerConfig
DTKTraceTapServiceDelegate
DTKPKDebugCodeSet
DTKPKDebugCodes

并存在以下能力/selector:

text
addCodes:
setKdebugFilter:
createTriggerKDebug:
filteredToPid
collectionInterval
startSampling
startKtraceSession:useExisting:outputFileURL:

这证明 Apple Instruments 内部确实支持“按 KDebug code set + PID/filter 配置”的通用 KTrace Tap,而不仅是 CoreProfile 的固定配置。

但对当前 Xcode 全部 DVT service 字符串审计后,没有发现独立的 com.apple.instruments.server.services.ktracetap / ...kdebugtap channel。设备可见的相关服务仍主要是:

text
activitytracetap(.immediate/.deferred/.windowed)
coreprofilesessiontap(.immediate/.deferred/.windowed)
hitches.recording
processortrace

因此:

text
GENERIC-DTKTRACETAP-INTERNAL-CAPABILITY:
CONFIRMED (E1)

GENERIC-DTKTRACETAP-AS-INDEPENDENT-DVT-CHANNEL:
NOT-FOUND

activitytracetap 不能因为名字带 trace 就当成 raw KDebug source。其 DTActivityTraceTapConfig 公开字段集中于 predicate、logs、signposts、HTTP archive、subsystem/category 与 target PID,属于统一日志/activity/signpost 路线;当前没有证据支持它直接输出 frame-info KDebug,因此不作为下一次真机 FrameTime candidate。

display-surface-swap 路线审计(E1):

该表由 GPU tracing package 定义,字段非常接近真实显示链:

text
timestamp
swap-id
surface-id
framebuffer-index
desired-presentation-time
generation-time
hid-time

但其 producer com.apple.metalsystrace-surface-swap 的 binding 明确依赖:

text
device-display-info
kdebug codes "0x85,0x20" "0x31,0x80"
time-info

其中 0x31/0x80 正是当前 CoreProfile Presented Frame 路线涉及的 CoreAnimation KDebug 类别。也就是说 display-surface-swap 本身是很好的显示真值表,但若仍通过 CoreProfileSessionTap 获取底层 KDebug,不会自动解决 device→DTX 高吞吐问题。

正式判定:

text
DISPLAY-SURFACE-SWAP-AS-GROUND-TRUTH:
STRONG-CANDIDATE

DISPLAY-SURFACE-SWAP-VIA-COREPROFILE-AS-LOW-BANDWIDTH-SOURCE:
NEGATIVE / DO-NOT-REPEAT

当前动态环境:

  • xcrun xctrace list devices 当前仍把物理 iPhone 列为 Offline,因此 Animation Hitches Ground Truth 录制继续受阻;
  • 但 python3 -m pymobiledevice3 usbmux list 能看到 USB iOS 17 真机 小红茂,因此 Perfowl modern RSD/DVT 路线仍可继续做最小 channel POC;
  • 系统 Python 与 Perfowl 冻结的 pymobiledevice3 版本不同,后续动态 POC 必须使用 MacApp/.sidecar-build/venv-py312 / Perfowl bunded runtime,禁止用系统 pymobiledevice3 API 形态做结论。

Stage 4 经验总结:

  1. Apple 已明确给出真实 presentation-time 原始字段,FrameTime 真值方向成立;
  2. “Apple 内部有 DTKTraceTap”不等于“设备暴露独立 ktracetap channel”,两者必须分开;
  3. activitytracetap 是 Activity/Log/Signpost contract,当前不应拿来盲试 KDebug;
  4. display-surface-swap 很接近屏幕真实 swap,但它的输入仍依赖 KDebug;不要把模型化表误认为天然低带宽 source;
  5. 下一阶段不再枚举 graphics.coreanimation selector,也不把 display-surface-swap 重新塞回 CoreProfile;
  6. 剩余值得验证的方向收敛为:设备端 KTrace file/recording 路径是否能把高流量留在设备端并只批量传结果,以及是否存在可增量/短窗读取的 display-frame 数据;否则零侵入实时 FrameTime 替代源在当前 DVT 暴露面上可能不存在。

Stage 4 状态:

text
FRAME-INFO-PRESENTATION-TIME-CONTRACT:
CONFIRMED (E1)

GENERIC-KTRACE-INTERNAL-FILTERING:
CONFIRMED (E1)

INDEPENDENT-REALTIME-KDEBUG-DVT-CHANNEL:
NOT-FOUND

NEXT:
DEVICE-SIDE-KTRACE-RECORDING / BATCH-DELIVERY POC

8.7 Stage 5 · Hitches/KTrace 设备端录制与实时性 POC(2026-09-17) ​

目的:验证 Apple hitches.recording / KTrace recording provider 是否可以把高频 KDebug 留在设备端,仅以低带宽方式回传逐帧结果;如果只能 Stop 后整包处理,则只保留为离线 Ground Truth,不进入实时 FrameTime 候选。

静态合同(E1):

当前 Xcode HitchesTap.framework 明确包含:

text
ktraceFileURL
parseKTraceFile:
processFileWithPath:startDate:endDate:errorsOut:
executeStopOnItinerary:

DVTInstrumentsFoundation 同时存在:

text
com.apple.dt.ktrace_recording.provider
DVTKTraceSessionConfiguration
DTKTraceSessionCreator
startKtraceSession:useExisting:outputFileURL:
stopKtraceSessionWithError:

这说明 Animation Hitches 的底层确实支持设备/trace 级 KTrace file recording,而不是必须将全部 raw KDebug 持续实时发送给 Host。

2 秒真机动态探针(E4):

text
真机 A / iOS 17
→ Perfowl tunneld-native-rsd
→ DvtProvider
→ open_channel(com.apple.instruments.server.services.hitches.recording)
→ setConfig({})
→ start
→ bounded receive 2s
→ stop

结果:

text
channel: OPEN
setConfig: accepted
start: accepted
2s running messages: 0
2s running payload bytes: 0
stop: accepted

本轮未开启 CoreProfile,也没有观察到实时逐帧 callback。关闭 DTX 时出现的 channel cancellation 日志属于连接 teardown,不作为 Hitches source failure 证据。

Animation Hitches 模板 recording-mode 真值(E1):

对当前 Xcode Animation Hitches.tracetemplate 的 XRRecordingOptions 解档后得到:

text
recordingMode = 2
supportsDeferredMode = true
supportsImmediateMode = false
supportsWindowedMode = false

这条证据比服务名/方法名更强:当前 Apple Animation Hitches 模板本身明确不支持 Immediate/Windowed recording,只支持 Deferred。

因此正式判定:

text
HITCHES/KTRACE-FILE-AS-LOW-DTX-RECORDING:
SUPPORTED / STRONG (E1 + bounded E4)

HITCHES.RECORDING-AS-REALTIME-FRAMETIME-SOURCE:
NEGATIVE

ANIMATION-HITCHES-AS-GROUND-TRUTH/OFFLINE-REPORT:
KEEP

经验总结:

  1. 把 KDebug 留在设备端/trace 文件里可以避免“实时 raw KDebug 全量灌入 DTX”的架构形态,但它解决的是传输方式,不自动提供实时 UI;
  2. hitches.recording channel 能打开并接受 start/stop,不代表它会实时回调;本轮 2 秒为 0 message / 0 B,与 Host 侧 Stop 后 ktraceFileURL 解析合同一致;
  3. 当前 Xcode 模板明确 supportsImmediateMode=false,因此不应继续尝试通过猜 selector/config 强行把 Hitches 变成实时 tap;
  4. Hitches 仍是非常有价值的最终报告/真值老师,可用于校准 Perfowl 其他实时 source;
  5. 后续若产品允许“实时曲线无 FrameTime、Stop 后补齐逐帧 FrameTime 报告”,设备端 deferred KTrace 可以作为一个产品降级方案,但它不是当前目标的实时替代源。

Stage 5 状态:

text
DEVICE-SIDE-DEFERRED-KTRACE:
VIABLE FOR OFFLINE / POST-STOP

REALTIME FRAME CALLBACK:
NOT AVAILABLE VIA HITCHES TEMPLATE

NEXT:
SEARCH NON-KDEBUG REALTIME DISPLAY/PRESENTATION SOURCE

8.8 Stage 6A · coreprofilesessiontap.deferred 低传输假设真机证伪(2026-09-17) ​

目的:验证 CoreProfile 的 .deferred service 是否能够把 exact Presented Frame KDebug 保留在设备端,在运行期间避免 raw KDebug 经 DTX 持续回传,并于 Stop 后再批量交付。若成立,可作为“实时 UI 暂无 FrameTime、Stop 后补齐真实逐帧数据”的降流量候选。

方法(E4):

text
真机 A
→ Perfowl iOS 17+ modern native RSD route
→ com.apple.instruments.server.services.coreprofilesessiontap.deferred
→ csd = 0
→ kdf2 = [0x31800318]
→ ta = [[3],[0],[2],[1,1,0]]
→ tk = 3
→ start
→ bounded receive 2s
→ stop
→ bounded post-stop receive 2s

本轮仅验证 delivery 行为,不接入产品 Session,也不与其他 CoreProfile tap 并发。

真机结果:

text
运行中 2 秒:
message count = 9
DTX payload    = 11,881,321 B ≈ 11.33 MiB
其中 data      = 6
notification   = 3

Stop 后 2 秒:
message count = 10
DTX payload    = 16,970,540 B ≈ 16.18 MiB
其中 data      = 6
notification   = 4

运行期已经直接收到 MiB 级 binary KDebug/stackshot 数据,因此 .deferred 并不等价于“设备端只录文件、运行中零 DTX raw”。Stop 后还会继续 drain 大块 binary 数据。

阶段判定:

text
COREPROFILE-DEFERRED-AS-LOW-DTX-FRAMETIME:
NEGATIVE (E4)

COREPROFILE-DEFERRED-AS-DEVICE-SIDE-SPOOL:
FALSE FOR THIS CONTRACT

经验总结:

  1. DVT service 名称中的 .deferred 不能解释成“整段采集延迟到 Stop 后才传输”;真实 wire 行为必须用运行期 byte census 验证;
  2. 即使 kdf2 已精确为 0x31800318 且 csd=0,.deferred 仍在 2 秒内向 Host 推送约 11.3 MiB 数据,未改变 CoreProfile heavy-stream 本质;
  3. hitches.recording 的设备端 KTrace/file recording 与 coreprofilesessiontap.deferred 是两种不同合同,不能因为都叫 deferred 而混为一谈;
  4. 后续禁止把 CoreProfile .deferred 作为高吞吐治理方案重新测试;
  5. .windowed 若没有新的协议证据证明服务端会先模型化/过滤 Presented Frame,不再仅凭名字继续枚举,避免重复制造 KDebug 洪流。

下一步:继续验证非 CoreProfile 实时源:GPU/Metal presentation-info 是否仅来自 Stop 后 KTrace importer;同时评估 QuickTime/Valeria 屏幕流能否满足 Display FrameTime 真值与 120Hz 合同。若两者均失败,则实时、零侵入、低 DTX 的公开/可见 DVT source 当前可能不存在,产品候选将收敛为 Hitches deferred KTrace 的 Stop 后真实 FrameTime。

8.9 Stage 6B · GPU/Metal presentation-info 实时源审计(2026-09-17) ​

目的:判断 Instruments GPU/Metal 路径中的 frame presentation 信息是否来自一个独立低带宽实时 DVT source,还是 Stop 后从 KTrace/AGX trace 导入生成。

静态证据(E1):当前 Xcode GPUPlugin 同时包含:

text
com.apple.instruments.server.services.gpu
com.apple.instruments.server.services.gpu.counters.multisources
com.apple.instruments.server.services.gpu.counters.aps
requestDeviceGPUInfo
configureCounters:counterProfile:interval:windowLimit:tracingPID:
startCollectingCounters
stopCollectingCounters
flushRemainingData

Perfowl 当前也已实现同一 gpu counter control surface;其合同是 device-specific GPU counter schema/rows,不是 display presentation event。

GPUPlugin 虽然包含真正的 frame presentation 字段/接口:

text
frames.cpuTimestamps
frames.gpuTimestamps
frames.drawableTimestamps
frames.desiredPresentTimestamps
frames.desiredPresentDelays
frames.presentFlags
frames.drawableIOSurfaceIDs
_agxps_trace_frame_set_presentation_info

但这些符号与以下 Stop/import 路径同时出现:

text
_loadKTrace
containerFromKtraceFile:
initWithkTraceFile:
_importTrace:itinerary:
executeStopOnItinerary:
recordingStopped:
com.apple.ktrace
AGXTraceConfiguration

GPUPlugin 还直接链接 ktraceDT.framework。当前没有发现一个独立实时 selector 能把 frame_set_presentation_info 作为低带宽 DTX callback 输出;现有实时 remote service surface 是 GPU counters,而 frame presentation 容器属于 trace/import 模型。

阶段判定:

text
GPU-COUNTER-SERVICE-AS-FRAMETIME-SOURCE:
NEGATIVE

GPU-PRESENTATION-INFO-EXISTS:
CONFIRMED IN XCODE TRACE MODEL (E1)

GPU-PRESENTATION-INFO-AS-INDEPENDENT-REALTIME-DTX:
NOT FOUND

经验总结:

  1. “GPU trace 中存在 presentation timestamp”不等于“GPU counter DVT service 实时返回 presentation timestamp”;Counter 与 Trace/Importer 必须分开理解;
  2. 当前 com.apple.instruments.server.services.gpu 的已知实时合同仍是 counters,不能通过猜 profile 把它宣称成 FrameTime source;
  3. frame presentation info 对 Ground Truth 很有价值,但目前证据指向 KTrace/AGX trace 的 Stop/import 路径,与 Hitches 的结论一致;
  4. 未出现新的 remote selector/实时 payload 证据前,不做 GPU profile/selector 穷举真机测试。

8.10 Stage 6C · QuickTime/Valeria/CoreMediaIO 屏幕流候选审计(2026-09-17) ​

目的:判断零侵入 USB 屏幕视频流的 sample PTS 是否能直接替代 Display FrameTime。

本机探针(E3):通过 CoreMediaIO 设置 kCMIOHardwarePropertyAllowScreenCaptureDevices=1 成功,但当前 AVCapture 枚举只看到 Mac 内建摄像头,没有暴露连接 iPhone 的屏幕 capture device,因此本机暂时无法做 sample PTS 真机校准。

公开协议/实现证据(E1):QuickTime/Valeria 类 USB mirroring 路线公开实现描述为 HEVC 屏幕视频流,典型能力上限约 60 FPS。即使能拿到媒体 sample PTS,它描述的也是编码/传输视频帧时间线,尚无证据等价于 Core Animation presentation-time / Built-In Display frame truth;对 120Hz/ProMotion 动态刷新率尤其不足。

阶段判定:

text
USB-SCREEN-VIDEO-AS-CANONICAL-DISPLAY-FRAMETIME:
NEGATIVE

USB-SCREEN-VIDEO-AS-VISUAL-AUXILIARY:
KEEP AS NON-FRAMETIME TOOL

经验总结:

  1. 不能因为视频帧有 PTS 就把它当作屏幕真实新画面 presentation timestamp;必须区分 compositor/display truth 与 encoder delivery time;
  2. 60 FPS 上限无法完整表达 80/120Hz ProMotion FrameTime;
  3. 该路线可以继续服务截图/视频/人工视觉校验,但不得进入 Perfowl canonical FrameTime。

Stage 6 总结:实时零侵入候选进一步收敛。graphics.coreanimation 只有 1Hz FPS 汇总,GPU presentation 依赖 trace/import,QuickTime/Valeria 不满足 Display FrameTime/120Hz 真值,CoreProfile deferred 仍然重流量。当前唯一保留真实 Display FrameTime 且能够避免长期 raw KDebug 经 DTX 洪流的架构方向,是 Animation Hitches / device-side KTrace 的 deferred recording + Stop 后 Frame Lifetime 解析。下一阶段必须把这条路线从“静态/控制面可行”推进到“端到端取回并解析 presentation-time 的 POC”。

8.11 Stage 7A · Animation Hitches 全模板真机体积基线与最小 Frame Lifetimes Instrument 可行性(2026-09-17) ​

目的:先建立 Apple 官方 Animation Hitches Ground Truth 的真实录制成本,再验证是否能脱离整套模板,仅使用 Frame Lifetimes Instrument 收窄输入集合,为后续真机低体积 POC 建立单变量方案。

A. 20 秒真机 Animation Hitches 全模板基线(E4) ​

当前 xctrace 曾短暂识别真机 00008140-0019786C3C82801C / iOS 18.2.1,对目标进程 netdisk_iPhone 执行:

text
xctrace record
--template "Animation Hitches"
--attach netdisk_iPhone
--time-limit 20s

录制本身达到 20 秒时间上限,但 xctrace 在 120 秒执行预算内没有完成最终 trace 封装;落盘证据为:

text
.trace 总量                 ≈ 3.3 GiB
trace-data.atrc             ≈ 3.3 GiB
临时 instruments*.ktrace    ≈ 3.3 GiB
xctrace finalize            > 120s / 未完成
不完整 trace export --toc   Document Missing Template Error

判定:

text
ANIMATION-HITCHES-FULL-TEMPLATE-AS-PRODUCTION-FRAMETIME:
NEGATIVE — FAR TOO HEAVY

ANIMATION-HITCHES-FULL-TEMPLATE-AS-GROUND-TRUTH:
KEEP / SHORT CONTROLLED RUNS ONLY

该结果说明“把整套 Animation Hitches 直接嵌入 Perfowl”不可接受;但这并不能证明 Frame Lifetimes 单 Instrument 同样重,因为完整模板同时携带 CPU、render、GPU、display、hitch 等多组输入。

B. Frame Lifetimes 单 Instrument 能否独立运行(E3 Host Smoke) ​

xctrace list instruments 明确列出独立:

text
Frame Lifetimes

使用 Blank template,仅添加该 Instrument:

text
xctrace record
--instrument "Frame Lifetimes"
--all-processes
--time-limit 1s

Host smoke 正常完成并可导出,结果:

text
trace 总量             ≈ 9.2 MiB
其中 form.template      ≈ 8.0 MiB(固定模板资产)
record/finalize          PASS
export --toc             PASS

TOC 证明最小 Frame Lifetimes Instrument 的输入被明显收敛为:

text
coreanimation-lifetime-interval

device-display-info
time-info
tick

display-surface-swap

kdebug codes:
0x07,0x00 + 0x1f,0x05
0x31,0xd0
0x85,0x20 + 0x31,0x80
0x31,0xd1
0x31,0xca

这与前面从 Apple modeler 静态审计得到的 Frame Lifetime 依赖关系一致,说明 Frame Lifetimes 可以脱离完整 Animation Hitches 模板独立配置,且不会自动引入所有 Hitches 分析轨道。

阶段经验:

  1. 不能用完整 Animation Hitches 的 3.3 GiB/20s 直接否定 Frame Lifetimes 最小录制;必须区分 Template 与单 Instrument;
  2. xctrace --instrument "Frame Lifetimes" 会自动生成 Blank template + 必需 table/provider 输入,是目前最值得做真机体积验证的 Apple 官方最小配置;
  3. Host 1 秒 9.2 MiB 主要包含约 8 MiB 固定 form.template,不能直接换算真机采集带宽;真正门禁仍是真机 trace-data/ktrace 增量;
  4. 最小 Instrument 仍需要多个 KDebug code set,因此它是否比 CoreProfile 低流量不能凭配置数量判断,必须做 2~10 秒真机 byte census;
  5. 当前 xctrace 对物理设备状态会在 Online/Offline 间漂移,2 秒真机复测曾因 No devices found matching 未启动;这属于环境门禁,不计为候选失败。

Stage 7A 状态:

text
FULL-ANIMATION-HITCHES:
NEGATIVE FOR PRODUCTION

STANDALONE-FRAME-LIFETIMES-INSTRUMENT:
CONFIGURATION PATH CONFIRMED

TRUE-DEVICE BANDWIDTH:
PENDING DEVICE ONLINE

下一步:真机重新 Online 后,只执行 Blank + Frame Lifetimes 2 秒短录制,记录 trace-data/ktrace 实际体积、finalize 时间,并导出 coreanimation-lifetime-interval / display-surface-swap;若体积相对完整 Hitches 数量级下降且 presentation 时间可导出,再进入 10~20 秒验证。与此同时继续审计 Xcode KTrace provider contract,避免依赖设备状态等待。

8.12 Stage 7B · 设备可达性门禁复核(2026-09-17) ​

目的:在执行 standalone Frame Lifetimes 真机 2 秒 byte census 前,区分“设备物理/协议在线”与“Xcode Instruments 可录制”两个独立条件,避免把环境门禁误判成数据源失败。

本轮设备 小红茂 / iOS 17.0 / UDID 00008120-000A038A0232201E 的观测为:

text
pymobiledevice3 usbmux list:
PASS
ConnectionType = USB
HostAttached = true
TrustedHostAttached = true

xcrun devicectl list devices:
PASS
CoreDevice = D3CB1D28-5CD8-55C0-B858-506868E0C0A0
state = available (paired)

xcrun xctrace list devices:
NOT LISTED AS ONLINE

xctrace --device <UDID>:
No devices found matching

xctrace --device <CoreDevice UUID>:
No devices found matching

xctrace --device 小红茂:
No devices found matching

因此这轮没有真正启动 Frame Lifetimes recording,也没有产生可用于吞吐判断的 trace 数据。

阶段判定:

text
PHYSICAL/USBMUX REACHABILITY:
PASS

COREDEVICE PAIRING/AVAILABILITY:
PASS

XCTRACE-INSTRUMENTS RECORDABILITY:
FAIL / ENVIRONMENT GATE

FRAME-LIFETIMES-CANDIDATE VERDICT:
UNCHANGED / NOT TESTED

经验总结:

  1. usbmux/lockdown 在线、devicectl available (paired) 与 xctrace 可录制是三个不同层级,不能互相替代;
  2. 对 FrameTime 替代源实验,只有 xctrace list devices 或实际 xctrace record 成功进入 recording 才算通过 Apple Instruments 设备门禁;
  3. No devices found matching 发生在 recording 创建之前,不得计入候选数据源失败、带宽失败或兼容性失败;
  4. 后续真机 byte census 必须保持实验变量不变:仍然只使用 Blank + Frame Lifetimes,不能因为设备发现问题切回完整 Animation Hitches;
  5. 下一次应优先使用已经被 xctrace 明确列为 Online 的物理设备;设备进入 Online 后直接重跑同一 2 秒命令,避免引入新的 Instrument/配置变量。

下一门禁:等待/选择一台 xctrace Online 的真机,执行 standalone Frame Lifetimes 2 秒录制;只有录制真正完成后才评估 trace-data/ktrace 增量、finalize 时间和 presentation-time / display-surface-swap 可导出性。

8.13 Stage 7C · 替代源 POC 重启与静态协议收敛(2026-09-17) ​

目的:在不重复已证伪 CoreProfile 路线的前提下,重新确认 FrameTime 替代源候选、验证 Apple Frame Lifetimes/Display 真值语义,并把设备不可录制与候选失败严格分离。

本阶段先复核开源实现与当前 Xcode:

  1. py-ios-device 2.4.25 的高级 display/Jank 实现最终仍调用 com.apple.instruments.server.services.coreprofilesessiontap,并从 0x31800318 Presented Frame 时间戳计算逐帧间隔。因此它不是新的数据源,继续沿用会重新进入当前已证伪的 CoreProfile heavy-stream 路线;
  2. 当前 Xcode DVTInstrumentsFoundation 明确包含独立服务名 com.apple.instruments.server.services.hitches.recording,同时存在 com.apple.instruments.server.services.graphics.coreanimation(.immediate/.deferred);
  3. Apple Animation Hitches 的 Built-In Display 轨道语义明确是“实际出现在显示器上的帧 + 期间 VSYNC”,Frame Lifetimes 描述帧从事件到显示的完整生命周期,因此它们满足 canonical Display FrameTime 的语义要求;
  4. hitches.recording 在二进制字符串布局中靠近通用 DTTapMessageHandler / DTTapRecordingInfo_Timestamp,与 coreprofilesessiontap 的 KPerf config surface 不同,值得继续逆向其 recording contract,但“服务名不同”本身不能证明底层完全不使用 KTrace/KPerf;
  5. 本机 xctrace list devices 当前没有任何物理设备处于 Online,之前用于 Perfowl 的设备均出现在 Devices Offline,因此本轮不能诚实执行新的真机 Frame Lifetimes byte census。

阶段判定:

text
PY-IOS-DEVICE-DISPLAY-AS-ALTERNATIVE-SOURCE:
NEGATIVE / SAME COREPROFILE PATH

APPLE-FRAME-LIFETIMES-DISPLAY-SEMANTICS:
CONFIRMED AS GROUND-TRUTH CANDIDATE

HITCHES.RECORDING-INDEPENDENT-SERVICE-SURFACE:
CONFIRMED (STATIC)

TRUE-DEVICE STANDALONE FRAME-LIFETIMES POC:
BLOCKED BY XCTRACE DEVICE-OFFLINE GATE

CANDIDATE VERDICT:
UNCHANGED / KEEP P0

阶段经验总结:

  1. 开源项目宣称“advanced FPS / display / jank”不能直接视为新源,必须追到底层 channel/service;py-ios-device display 已证明只是 CoreProfile 的另一层算法包装;
  2. Apple Built-In Display + VSYNC 是当前零侵入 Display FrameTime 的最佳真值语义,后续候选必须能对齐这一时间线;
  3. hitches.recording 与 Frame Lifetimes 候选目前最值得继续,但必须通过真机 byte census 证明其传输/落盘成本,而不能根据 service 名称推断低开销;
  4. 设备处于 xctrace Devices Offline 时不得把 xctrace record 未启动算作候选失败;
  5. 后续继续静态逆向 selector/config/schema;一旦出现 xctrace Online 真机,优先重跑既定的 Blank + Frame Lifetimes + 2s 单变量实验,不切回完整 Animation Hitches。

下一门禁:优先定位 hitches.recording / Frame Lifetimes 的 selector、config 与 payload schema;若能直接通过 Perfowl 现有 DVT provider 建立该独立 channel,则先做 2 秒 direct-DVT POC。否则等待任一物理设备在 xctrace 中 Online 后执行 standalone Frame Lifetimes 2 秒录制并导出 coreanimation-lifetime-interval / display-surface-swap。

8.14 Stage 7D · Frame Lifetime 原始事件合同与 hitches.recording 真机握手探针(2026-09-17) ​

目的:确认 Apple Frame Lifetimes 在 iOS 上真正依赖的原始事件语义,并绕过 xctrace 设备发现,通过现有 RSD/DVT 连接直接验证 hitches.recording 是否为真实设备服务、通用 Tap 协议是否适用。

A. Xcode Frame Lifetime Modeler 原始合同(E1) ​

当前 Xcode 的 coreanimation-frame-lifetime-modeler.dtac 明确给出 iOS 原始事件:

text
CoreAnimation class/subclass:
0x31 / 0xCA

frame-info event:
subclass = 0xCA
code     = 0x0B
function = 0

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

Modeler 随后使用设备 timebase 把 earliest-time / presentation-time / requested-time 转换为 trace-relative 时间,并产出 coreanimation-lifetime-interval。这证明 Apple Frame Lifetimes 在 iOS 上存在单事件直接携带 presentation timestamp 的合同,不需要像当前 Perfowl 0x31800318 Presented Frame 路线那样仅靠相邻事件时间差推导。

同一 modeler 的输入绑定为:

text
kdebug 0x31/0xCA
kdebug 0x31/0xD0
kdebug 0x31/0xD1
display-surface-swap
time-info
tick

Hitches schema 同时确认存在:

text
expected-presentation-time
presentation-time
swap-id
frame-seed
refresh-interval

因此 presentation-time 是当前替代源 POC 的 canonical 目标字段。

B. 绕过 xctrace Offline 门禁的真机 DVT 探针(E4) ​

虽然 xctrace list devices 当前仍把物理设备列为 Offline,但:

text
pymobiledevice3 usbmux:
小红茂 / iOS 17.0 / 00008120-000A038A0232201E
PASS

existing tunneld RSD:
fdef:2c2a:3660::1:52953
PASS

使用 Perfowl 打包的 pymobiledevice3 11.3.1 x86_64 runtime,通过:

text
developer dvt shell --rsd fdef:2c2a:3660::1 52953

成功连接 DvtProvider / DTXConnection,随后直接创建:

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

结果:

text
DTX channel open:
PASS

returned service:
DTXDynamicService

这把 hitches.recording 从“Xcode 静态字符串”提升为“iOS 17 真机 Instruments Server 实际可打开的 DVT service”。

C. 通用 Tap 协议与 Wire Config(E1/E3) ​

pymobiledevice3 11.3.1 的 TapService 合同确认:

text
setConfig:
start
<data / plist callbacks>
stop

同时通过 macOS 动态加载 DVTInstrumentsFoundation 并实例化 Apple 自己的 DTTapConfig,确认:

text
Default DTTapConfig plist:
{"bm": 0}

spoolToDiskWhenPossible = true:
"s2d" = 1

bufferMode = 1:
"bm" = 1

D. 2 秒最小真机 Start 探针结果(E4) ​

首个受控 config:

text
{"bm": 1, "s2d": 1}

执行:

text
open hitches.recording as TapService
setConfig:
start
wait for first ack/data
2s
stop

实际结果:

text
channel open              PASS
setConfig send            PASS
start send                PASS
first ack/data             NONE
queue after 2s             0
DTX control notification   _channelCanceled: -1
pymobiledevice3 warning    cancellation for unknown channel -1
stop send                  PASS
post-stop payload          NONE

阶段判定:

text
HITCHES.RECORDING TRUE-DEVICE SERVICE AVAILABILITY:
PASS

GENERIC TAP SELECTOR CONTRACT:
CONFIRMED

APPLE FRAME-LIFETIME PRESENTATION-TIME SEMANTICS:
CONFIRMED

MINIMAL {bm:1,s2d:1} CONFIG HANDSHAKE:
FAIL / INCOMPLETE CONFIG

HITCHES.RECORDING SOURCE VERDICT:
NOT NEGATIVE — CONFIG CONTRACT NOT YET RECONSTRUCTED

阶段经验总结:

  1. channel open、setConfig/start 可发送、以及真正进入 recording/收到 ack 是三个独立门禁;不能把 channel 可创建直接等同于配置正确;
  2. _channelCanceled:-1 出现在 start 后且没有任何业务 payload,更符合“服务端拒绝/缺少必要 recording config”的形态;目前没有证据证明数据源本身不可用;
  3. {bm:1,s2d:1} 只是由 Apple DTTapConfig wire key 推导出的最小实验,已经失败,后续禁止原样重复;
  4. 下一步不能继续盲猜 bm/s2d 组合,必须从 Xcode trace template / Instruments 对象图提取实际 DTTapConfig 或对应 recording 参数;
  5. iOS 0x31/0xCA/0x0B 的 arg3=presentation-time 是本阶段最重要的新协议事实;后续任何低带宽候选只要能稳定获得该事件或其模型化结果,就有机会替代当前 CoreProfile Presented Frame 路线。

下一门禁:从 Animation Hitches.tracetemplate / Instruments 私有对象中恢复真实 hitches.recording config;优先验证 Apple 默认/模板实际配置,而不是枚举随机字段。只有出现真实 ack/recording payload 后才进入 byte census 与 presentation-time 解码。

8.15 Stage 7E · Xcode Deferred Mode 复原与 bm=2 单变量真机复测(2026-09-17) ​

目的:根据 Xcode 模板实际 recording option,而不是猜测,验证 Stage 7D 的失败是否仅由错误 bufferMode 引起。

从 Animation Hitches.tracetemplate 的 NSKeyedArchive 对象图恢复出 XRRecordingOptions:

text
recordingMode = 2
supportsDeferredMode = true
supportsImmediateMode = false
supportsWindowedMode = false
windowLimit = 0

结合 Apple DTTapConfig.plist 已确认 bufferMode -> bm,因此本轮严格单变量:

text
Stage 7D: {bm:1, s2d:1}
Stage 7E: {bm:2, s2d:1}
                 ^ only change

真机执行仍使用同一 iOS 17 设备、同一 RSD、同一 hitches.recording channel 与标准 Tap setConfig → start → stop 路径。

实际结果:

text
channel open              PASS
setConfig bm=2,s2d=1      PASS (send)
start                     PASS (send)
first ack/data             NONE
queue after 2s             0
DTX control notification   _channelCanceled: -1
stop                       PASS (send)
post-stop payload          NONE

阶段判定:

text
XCODE HITCHES RECORDING MODE:
DEFERRED / MODE 2 CONFIRMED

BM=2 AS SOLE FIX FOR HANDSHAKE:
NEGATIVE

HITCHES.RECORDING SOURCE VERDICT:
STILL OPEN — CONTROL CONTRACT / REQUIRED CONFIG INCOMPLETE

阶段经验总结:

  1. Xcode 模板明确要求 Hitches Deferred mode,recordingMode=2 是证据支持的值,不再把 mode 枚举作为未知变量;
  2. bm=2 仍在 start 后产生同样 _channelCanceled:-1,因此 Stage 7D 失败不是单纯因为 bm=1;
  3. 后续禁止继续枚举 bm=0/1/2/...,也禁止随机叠加 DTTapConfig 字段;
  4. 下一步必须确认 hitches.recording 的服务端注册类/selector 以及 Xcode 是否通过额外 provider/file-recording control surface 启动它;
  5. hitches.recording channel 在真机可打开仍是有效证据,但“标准 Tap selector 与正确控制合同相同”尚未证明。

下一门禁:逆向 hitches.recording 服务注册与消息处理类,确认实际 selector;只有 selector/control contract 确认后再执行下一次真机 start 探针。

8.16 Stage 7F · hitches.recording Delegate 归属、ActivityTrace 默认合同与握手收口(2026-09-17) ​

目的:确认 hitches.recording 的真实服务端 delegate、是否属于 KTrace 主源,并验证 Stage 7E 是否只是缺少 DTActivityTraceTapConfig 默认字段。

A. 服务端注册归属(E1) ​

通过 DVTInstrumentsFoundation 的 literal xref 与 DTTapService registerCapabilities:forDelegateClass:forConnection: 调用链确认:

text
com.apple.instruments.server.services.hitches.recording
→ DTActivityTraceTapServiceDelegate
→ DTTapService

不是 DTKTraceTapServiceDelegate。

DTActivityTraceTapServiceDelegate 的实现进一步确认:

text
createConfigWithPlist:
→ [[DTActivityTraceTapConfig alloc] initWithPlist:]

validateConfig:
→ always true

因此 hitches.recording 更接近 ActivityTrace / os_log / signpost 辅助 recording,而不是 Frame Lifetimes 的 KDebug/KTrace presentation 主源。

B. ActivityTrace 子类真实默认 wire plist(E3) ​

通过 Apple DTActivityTraceTapConfig 动态实例化:

text
DEFAULT:
{
  bm = 0;
  predicate = "";
}

并确认常见字段 wire key:

text
onlySignposts
signpostsAndLogs
trackExpiredPIDs
includeWallTime
includeSenderInfo
includeEmitLocation
s2d
bm
predicate

Stage 7D/7E 的配置确实遗漏了子类默认的 predicate=""。

C. 单变量补全 predicate 的真机复测(E4) ​

保持:

text
bm=2
s2d=1

只新增:

text
predicate=""

结果:

text
channel open              PASS
setConfig                 PASS (send)
start                     PASS (send)
first ack/data             NONE
queue                      0
DTX control notification   _channelCanceled:-1

与前两轮完全一致。

D. KTrace capability 边界(E1) ​

动态调用 Apple DTDeviceKTraceSupport.capabilities 得到当前 Xcode 真实 KTrace Tap 能力:

text
coreprofilesessiontap
coreprofilesessiontap.config
coreprofilesessiontap.deferred
coreprofilesessiontap.immediate
coreprofilesessiontap.multipleTimeTriggers
coreprofilesessiontap.pmc
coreprofilesessiontap.pmi
coreprofilesessiontap.windowed

没有第二个独立的通用 KTrace DVT channel。

阶段判定:

text
HITCHES.RECORDING AS FRAME-LIFETIME KTRACE PRIMARY SOURCE:
NEGATIVE

HITCHES.RECORDING DIRECT STANDARD-TAP HANDSHAKE:
NEGATIVE ON CURRENT CONTRACT

ACTIVITYTRACE MISSING-PREDICATE HYPOTHESIS:
NEGATIVE

INDEPENDENT GENERIC KTRACE DVT SOURCE OUTSIDE COREPROFILE:
NOT EXPOSED BY DTDeviceKTraceSupport

阶段经验总结:

  1. hitches.recording 名称容易误导;真实 delegate 是 ActivityTrace,不是 Frame Lifetimes 的 KTrace 主输入;
  2. Hitches 产品由多源拼装,不能把某个叫 hitches.* 的服务直接当成逐帧 Display source;
  3. DTDeviceKTraceSupport 的公开 capability 已收敛到 CoreProfile 家族,继续猜不存在的 ktracetap channel 没有价值;
  4. hitches.recording 连续三次基于证据补全配置仍在 start 后被取消,本轮停止继续枚举其配置;
  5. 下一条值得验证的低带宽实时方向只剩:通用 ActivityTrace 中是否存在可启用的 CoreAnimation frame-lifetime signpost。若没有,则 Apple Frame Lifetimes 的真实 presentation-time 在当前 DVT 暴露面上仍只能通过 KDebug/KTrace/CoreProfile 或 Stop 后 trace modeler 获得。

下一门禁:使用已知可工作的 activitytracetap 通用服务,仅采 signpost,检查是否存在 SignpostFrameLifetimeInterval/CoreAnimation instrumentation 所需事件;必须先做 2~5 秒低带宽 census,再决定是否进入解码器 POC。

8.17 Stage 7G · ActivityTrace / CoreAnimation FrameLifetime 低带宽实时替代源 POC(2026-09-17) ​

目的:验证通用 activitytracetap 的 signpost 流中是否直接存在可用于 canonical FrameTime 的 CoreAnimation FrameLifetime 事件,并同时验收逐帧真实性、时间戳链一致性和 DTX 带宽。

A. 真机低带宽配置(E4) ​

设备:

text
小红茂 / iOS 17.0
RSD: fdef:2c2a:3660::1:52953

数据源:

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

使用 pymobiledevice3 11.3.1 ActivityTraceTap 已验证的配置结构,并收窄到 only-signposts:

text
bm = 0
onlySignposts = 1
signpostsAndLogs = 0
predicate = ""
targetPID = -3        # all processes; compositor event 属于系统进程
trackExpiredPIDs = 1
trackPidToExecNameMapping = true
ur = 500

本轮没有启动 CoreProfileSessionTap。

B. 真实逐帧事件发现(E4) ​

真机 ActivityTrace stream 稳定出现:

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

单条事件的 format contract:

text
ID is swap ID.
%{signpost.description:begin_time}llu
frame_seed=%#x
refresh_interval=%llu
buffer_count=%hhu
%{signpost.description:end_time}llu
prev_frame=%llu
skip_request=%hhu
display_id=%u
surface_id=%#x

实际 typed payload 中可稳定解析:

text
swapID / begin_time
frame_seed
refresh_interval
buffer_count
end_time
prev_frame
skip_request
display_id
surface_id

这与 Xcode HitchesTap 的 SignpostFrameLifetimeInterval / Frame Lifetimes 模型语义吻合,并且 refresh_interval、display/surface 信息直接来自 CoreAnimation compositor 路径。

C. 5 秒 Byte Census(E4) ​

同一配置采 5 秒:

text
elapsed                  5.001 s
DTX messages             20
  data                    9
  plist                   11
raw data bytes            193,136 B
approx total payload      193,830 B
throughput                0.03696 MiB/s
all decoded signpost rows 481
FrameLifetime rows        90
observed frame rows/sec   ~18.0
bytes / FrameLifetime     ~2,154 B

当前设备画面不是满 60 FPS 动画,因此 FrameLifetime 只在真实新帧出现时产生;不能用 rows/sec 直接等价刷新率。

与 C1 CoreProfile baseline 对比:

text
CoreProfile exact Presented Frame:
18.76 MiB/s

ActivityTrace FrameLifetime:
0.03696 MiB/s

带宽降低:
≈ 507x
≈ 99.80%

即使考虑未计入的 DTX framing overhead,仍与 CoreProfile 相差两个以上数量级。

D. FrameTime 时间链验证(E4) ​

5 秒样本:

text
refresh_interval:
min/p50/p95/max ≈ 16.666 ms

FrameLifetime begin→end:
p50 ≈ 16.550 ms

successive end_time interval:
p50 ≈ 49.926 ms

随后独立 3 秒一致性复测:

text
FrameLifetime rows                60
adjacent pairs                    59
prev_frame == previous end_time   59
mismatch                          0
internal display IDs              [1]
surface IDs observed              2

两种计算方式完全一致:

text
self-contained:
(end_time - prev_frame) * mach_timebase

chain-derived:
end_time[n] - end_time[n-1]

median = 49.926 ms
min    = 49.909 ms
max    = 49.945 ms

设备 timebase 本轮为:

text
numer = 125
denom = 3

因此候选 canonical 公式为:

text
FrameTimeMs =
(end_time - prev_frame)
* numer / denom
/ 1_000_000

但产品实现不得硬编码 125/3,必须继续从设备 machTimeInfo / 当前 DVT timebase 获取。

E. Attribution / source scope ​

FrameLifetime 事件由系统 compositor 进程发出,不是目标 App PID 事件。因此该 source 应明确标记:

text
scope = foreground-device / display

产品归因规则应与现有 Graphics FPS 一致:

text
targetForeground == true
→ 允许发布 FrameTime

targetForeground == false
→ FrameTime = nil / not-foreground

不能声称事件本身是 process-scoped。

阶段判定:

text
ACTIVITYTRACE COREANIMATION FRAMELIFETIME SOURCE:
PASS (E4)

REAL PER-FRAME TIMESTAMP:
PASS

REALTIME DELIVERY:
PASS

NO COREPROFILE REQUIRED:
PASS

LOW-DTX-THROUGHPUT:
PASS (~0.037 MiB/s)

PREV_FRAME CHAIN CONSISTENCY:
PASS (59/59, 0 mismatch)

FRAMETIME ALTERNATIVE SOURCE POC:
SUCCESS

阶段经验总结:

  1. FrameTime 替代源已经找到:不是 hitches.recording,也不是另一个 KTrace channel,而是通用 activitytracetap 中 CoreAnimation 自己发出的 IOMFBServer / FrameLifetime signpost;
  2. 该事件是 Apple Hitches/Frame Lifetime 体系的一部分,直接携带 begin/end/prev_frame/refresh_interval/display/surface,比 1000/FPS 或视频 PTS 更接近 Display truth;
  3. 真机 POC 证明不需要持续 CoreProfile heavy stream,DTX payload 从 18.76 MiB/s 降至约 0.037 MiB/s;
  4. prev_frame 在连续 59 个 pair 中 100% 精确等于上一条 end_time,因此可用事件内 end_time-prev_frame 直接计算 FrameTime,同时保留跨 sourceEpoch/rebind 首帧 warmup 门禁;
  5. 首条 FrameLifetime 不得无条件使用其 prev_frame,因为它可能指向采集开始前的帧;正式实现应在每个 sourceEpoch/reconnectGeneration/foreground transition 后先建立一帧锚点,再发布后续 FrameTime;
  6. skip_request != 0、display 切换、锁屏/后台、多显示器、ProMotion 80/120Hz、iOS 版本差异仍需验证;当前 POC 只证明 iOS 17 当前设备可用;
  7. all-signposts 5 秒约 194 KB 已足够低,初版不需要为了节省几十 KB/s 冒险研究私有 subsystem/category filter;后续可作为二级优化;
  8. Stage 6 “只剩 Stop 后 Hitches/KTrace” 的阶段结论被本阶段正式 supersede:现在已经有零侵入、实时、低带宽的真机 FrameTime 候选。

下一门禁(产品化前):实现独立实验 Adapter ActivityTraceFrameLifetime,不修改正式 FrameTime 默认源;至少完成 iOS 17 当前设备 5~10 分钟长测、前后台切换、60/120Hz/动态刷新率、与现有 CoreProfile 短窗同场误差对照。通过后再切换生产 FrameTime source,并删除长期 CoreProfile continuation。

Perfowl · Performance Observer