Skip to content

启动观测与数据完整性:CoreProfile、阶段覆盖与端点修复 ​

本文专门回答 StartupTiming 最难的一组问题:九阶段究竟从哪里来、为什么同一个 App 有时会缺阶段、哪些问题 Perfowl 已经修复、哪些属于设备或 App 的观测边界、Total 如何在不造假的前提下恢复。


1. 九阶段的真实来源 ​

深度阶段值来自设备 CoreProfile (coreprofilesessiontap) 导出的 kdebug 事件:

class主要含义覆盖
0x1F07dyld / libSystemSystem Interface、Static Runtime
0x2BDClifecycle transition相邻阶段边界
0x2B87UIKit lifecycle signpostUIKit、Scene、AppDelegate / SceneDelegate 回调
0x31CAinitial frameInitial Frame / Total 终点

阶段耗时来自真实 BEGIN / END timestamp 差,经设备 mach timebase 转换为纳秒/毫秒。没有高层 API 插值,也没有用 FPS、主机时间或邻居阶段估算 kdebug 阶段。


2. 数据链与“lossFree”的边界 ​

一条事件要经过:

text
App / UIKit / dyld
    ↓
XNU kdebug
    ↓
设备缓冲 / CoreProfile tap
    ↓
DTX
    ↓
主机 live queue
    ↓
decoder / attribution

lossFree=true 主要证明主机侧受控队列没有检测到丢弃,不能证明设备缓冲、tap 内部导出或进入主机前的所有环节完全无损。

Perfowl 另外扫描 TRACE_LOST_EVENTS (0x07020008) 并输出 deviceLostEventMarkerCount。它是独立证据:非零表示确实看到设备丢失 marker;零只表示未观测到 marker,不等于全链无损。


3. Perfowl 已经修掉的工具问题:Two-Pass Attribution ​

早期版本曾存在一个真实工具问题:启动最初几毫秒,生命周期事件可能早于完整的 thread→PID 身份映射到达。如果 decoder 在第一次看到事件时立刻做目标过滤,会把实际上属于目标 App 的早期事件错判为非目标。

当前解法是两遍归因:

text
Raw kdebug records
       ↓
Pass 1:建立完整 thread / pid / generation 身份关系
       ↓
Pass 2:重新回放并归因 lifecycle records

这个改动修复的是主机解码/归因时序问题。因此今天看到的阶段缺失不能再简单归因成“PID 映射太晚”;这一类工具 bug 已经有专门的回归测试和多 App 真机证据。


4. 当前剩余的两类阶段不完整 ​

4.1 App 本轮路径没有发出某个条件 signpost ​

对部分重型 App 的真机 System Trace 取证发现,某些 UIScene 回调 code 在整轮 kdebug 中根本不存在。此时不能把“事件不存在”强行解释成采集失败。

只有存在足够的独立证据时,Perfowl 才把对应条件阶段标为 not-applicable;单纯“本轮没看到 BEGIN”不足以证明回调没有执行,因为设备侧竞态也可能造成相同表象。

4.2 设备侧早期事件偶发不可见 ​

同一个 App 不同轮次可能出现早期阶段一轮完整、一轮缺失。离线 raw 回放中,缺失轮的特定 BEGIN 在所有线程里都不存在,同时主机 lossFree=true。

现有证据只能把问题收紧到主机消费之前的设备/tap/传输前链路;没有公开配置可以进一步确定是哪一级覆盖或未送达。工程上应将它视为真实观测边界,而不是通过估算补值。


5. 已经验证无效或不适合的补齐路线 ​

为了避免未来重复走弯路,以下路线已经有实验结论:

  • Ready Barrier / startup-exclusive / receive-parse decouple:能改善主机准备和消费,但不能恢复设备侧根本未出现的事件;
  • CoreProfile action / buffer 参数枚举:已尝试的公开可控组合没有解决随机阶段缺失,部分改法还会破坏基础 stream;
  • 单轮语义重建:许多过渡事件同时承担相邻阶段 END/BEGIN,关键事件一旦不存在,邻居无法恢复到可信精度;
  • System Trace 旁路:底层同属 kdebug/kperf,不构成独立冗余,并且 kperf owner 有冲突边界;
  • os_trace_relay:全量 spike 能看到大量目标日志与 activity transition,但启动九阶段 signpost 为 0,证明这些 UIKit 启动 signpost 是 kdebug-only;
  • StartSuspended 确定性等待:开发调试包可行,但生产 App 缺少 get-task-allow,不符合 Perfowl 的无侵入生产包目标;
  • 跨轮拼接:明确禁止。不同冷启动轮次不能拼成一条现实中从未发生过的启动链。

6. 为什么 Total 比“九阶段完整”更稳 ​

Perfowl 的主结果采用独立端点:

text
Total = System Interface BEGIN → Initial Frame END

因此:

  • 中间某一阶段缺失 → Total 仍可能 clean;
  • Phase Coverage 下降 → 报告可降为 partial,但不应把已有 Total 清零;
  • 只有 Total 关键端点本身不可用,Total 才 missing 或进入明确的 endpoint repair。

这就是为什么产品 UI 只展示真实捕获阶段,并在 footer 披露 X/9,而不为了视觉完整度补出九条空值或 0 值。


7. Endpoint Repair:只修“仍有真实证据”的情况 ​

离线普查后,Total 端点缺失被分成三个主要模式。

Mode A:首帧终点还在,普通 opener 丢失 ​

text
System BEGIN          ✓
First-frame terminal ✓
普通配对 opener       ✗

如果生命周期锚点之后只有唯一合理 terminal,可以使用真实 terminal timestamp重新配对:

text
totalQuality = repaired
totalSource = coreprofile-endpoint-repaired
totalRepairReason = frame-end-repaired

若锚点后存在多个候选 terminal,则保留最早候选但必须标:

text
frame-end-repaired-ambiguous

它是候选结果,不能冒充 clean endpoint。

Mode B:首帧 END 本身不存在 ​

text
First-frame END ✗

没有真实终点就没有诚实恢复路径:

text
Total = missing

禁止用后续普通帧、屏幕截图时间或主机观察时间代替。

Mode C:System Interface BEGIN 不存在,但更晚的早期 dyld 起点仍在 ​

此时允许把最早可用的早期 dyld timestamp 作为替代起点,并明确:

text
totalQuality = repaired
totalRepairReason = start-substituted

它仍然是真实内核 timestamp 差,但不等同清晰 System Interface BEGIN 配对,因此知识库和 UI 都必须保留 repaired 标签,不给“精确 clean Total”的暗示。


8. 启动质量的三个正交维度 ​

以后排查 Startup 必须同时看三条轴:

text
A. Total Provenance
   clean / repaired / missing

B. Phase Coverage
   captured X/9
   missing N
   not-applicable M

C. Transport Integrity
   host lossFree
   deviceLostEventMarkerCount

例如 clean Total + 6/9 + marker=2 并不自相矛盾:说明主端点清晰,但阶段诊断覆盖不完整,同时设备还报告过其他 kdebug 丢失标记。


9. 多轮测试的诚信规则 ​

多轮测试是推荐做法,但用途要分清:

  • 统计 Total 分布:使用同一测试场景的完整轮次计算 P50 / P90;
  • 阶段定位:可以选择整轮 coverage 最完整的一轮深入阅读;
  • 不允许 r1 的 Pre-main + r2 的 UIKit + r3 的 Initial Frame 拼成“9/9 启动”;
  • repaired Total 需要与 clean Total 分开统计,不能静默混成一种质量。

10. 延伸阅读 ​

Perfowl · Performance Observer