外观
启动观测与数据完整性:CoreProfile、阶段覆盖与端点修复
本文专门回答 StartupTiming 最难的一组问题:九阶段究竟从哪里来、为什么同一个 App 有时会缺阶段、哪些问题 Perfowl 已经修复、哪些属于设备或 App 的观测边界、Total 如何在不造假的前提下恢复。
1. 九阶段的真实来源
深度阶段值来自设备 CoreProfile (coreprofilesessiontap) 导出的 kdebug 事件:
| class | 主要含义 | 覆盖 |
|---|---|---|
0x1F07 | dyld / libSystem | System Interface、Static Runtime |
0x2BDC | lifecycle transition | 相邻阶段边界 |
0x2B87 | UIKit lifecycle signpost | UIKit、Scene、AppDelegate / SceneDelegate 回调 |
0x31CA | initial frame | Initial Frame / Total 终点 |
阶段耗时来自真实 BEGIN / END timestamp 差,经设备 mach timebase 转换为纳秒/毫秒。没有高层 API 插值,也没有用 FPS、主机时间或邻居阶段估算 kdebug 阶段。
2. 数据链与“lossFree”的边界
一条事件要经过:
text
App / UIKit / dyld
↓
XNU kdebug
↓
设备缓冲 / CoreProfile tap
↓
DTX
↓
主机 live queue
↓
decoder / attributionlossFree=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 分开统计,不能静默混成一种质量。