Skip to content

启动时间采集现状、无法完整采集的根因与已做尝试 ​

面向后续维护者。记录 iOS 启动时间(App Launch 九阶段)在当前架构下的真实采集表现、为什么对部分 App 无法 100% 完整采集、以及本轮围绕该问题做过的全部尝试与真机(E4,设备 A / iPhone15,4 / iOS 17.0)证据。核心结论:归因类工具 bug 已修复;剩余不完整来自「App 自身不发信号」与「设备侧最早事件的采集竞态」两类;后者对 App Store 生产 App 无确定性软件解(受 get-task-allow 限制)。

1. 启动九阶段模型与数据来源 ​

启动分析基于设备 CoreProfile(coreprofilesessiontap)的内核 kdebug 生命周期事件(class 0x1F07 dyld/libSystem、0x2B87 UIKit signpost、0x2BDC 过渡、0x31CA 首帧)。九个阶段(PHASES):

  1. System Interface Initialization(必需)
  2. Static Runtime Initialization(必需)
  3. UIKit Initialization(必需)
  4. UIKit Scene Creation(必需)
  5. willFinishLaunchingWithOptions()(可选回调)
  6. didFinishLaunchingWithOptions()(必需)
  7. sceneWillConnectTo()(可选回调)
  8. sceneWillEnterForeground()(可选回调)
  9. Initial Frame Rendering(必需,Total 端点)

每阶段由一对 BEGIN/END kdebug 事件配对成 span,取内核时间戳差(mach ticks 经时钟系数换算)。质量语义:candidate(完整 span)/partial(有值但边界不完整)/missing(未成 span)/not-applicable(App 未运行该回调)。值均为真实内核事件差,不补 0、不估算(诚实红线)。

2. 当前采集表现(E4:设备 A × 10 App × 2 轮) ​

  • 轻/中量 App(微信、支付宝、京东等):稳定 9/9 candidate。微信两轮均 9/9。
  • 早期阶段偶发缺失(淘宝、高德、抖音某些轮次):System/Static/UIKit Init 间歇性 missing——同一 App 轮次间波动(例:高德 r1 缺 UIKit Init/Static、r2 全齐;京东 r1 缺、r2 全)。
  • 重型 App 的 scene 阶段(QQ、美团、北京银行、抖音):sceneWillConnectTo/sceneWillEnterForeground 稳定缺失,现已判 not-applicable 并隐藏(见 §4.1)。
  • 重型 App 的 Initial Frame / 尾段(百度网盘,首帧渲染慢,总时长 1.2–2.3s):完成等待改为 liveness 判据后(见 §4.1),源仍在传输就逐段续期、停滞才收尾。真机四轮回归:百度网盘、高德各 3/4 完整;剩余各 1 次不全经原始遥测确认为设备采集竞态(百度=头部 UIKit Init 丢、高德=中/尾段 sceneWillConnect→首帧区段丢,lossFree 均为 true → 非主机队列丢),属 §3.2 无确定性解,非本次可修的工具 bug。

3. 无法完整采集的三类根因(均 E4 证据支撑) ​

3.1 App 自身不发信号(App 行为,非工具 bug) ​

对 QQ 用 Apple 官方 System Trace 抓冷启动(52639 条 QQ kdebug 行):

  • QQ 进程 0 条 0x2B87 UIScene 生命周期事件;
  • sceneWillConnectTo/EnterForeground 的 code 300/312/313 全系统不存在。

即 QQ/美团/北京银行/抖音这类重型 App(自定义启动/闪屏/预连接场景)冷启动不发标准 UIScene willConnect/EnterForeground signpost。微信/淘宝/京东会发,我们的工具都能采到。连 Apple 自己的 xctrace/System Trace 也拿不到(因为信号不存在)。→ 归类 not-applicable、隐藏,符合诚实红线。

3.2 设备侧最早事件的采集竞态(间歇、随机、非我方丢包) ​

对高德 r1(缺 UIKit Init/Static)做原始 kdebug 离线重放取证:

  • UIKit Init BEGIN(code 90)在整条捕获流的任何线程都不存在(count=0),r2 却有;
  • sourceQuality.lossFree=true——排除了主机侧环形缓冲/队列丢弃,也不是配对/归因 bug(事件不在,无从配对)。

校正(codex 复核):lossFree=true 只覆盖主机侧队列,不证明整条链完整——设备内核 kdebug 缓冲溢出、设备→主机传输链上的丢失都不在其观测范围内。因此上面的取证只能排除"主机队列丢的"与"配对/归因 bug"两种,"设备侧最早事件竞态导致覆盖/未送达"目前是最合理推断,但尚未被硬证据闭合:要把"设备缓冲溢出丢失"与"App 根本不发"彻底分开,需要采集链可靠透出并计数设备的 TRACE_LOST_EVENTS 内核丢事件标记(该审计尚未落地,列入后续方案 A)。

在此前提下的推断:非挂起冷启动时,App 进程开始执行后头几毫秒的最早事件与「设备 kdebug缓冲 → tap → 主机」链路的刷新节奏/每核缓冲状态之间存在竞态。命中好窗口→全采到;落进缝隙→最早事件在被抽走前被覆盖/未送达。哪个 CPU 核启动、缓冲多满、调度时机都在变 → 每次冷启动独立地有一定概率漏采(双向随机:见矩阵中 r1/r2 互有缺失)。

3.3 架构限制:唯一确定性修法被 get-task-allow 挡死 ​

能彻底消除 §3.2 竞态的唯一手段是挂起启动:request_launch(start_suspended=True) 把 App 冻结在 main() 之前 → 确认 tap 在流式 → 再 resume。挂起期间 App 不发任何事件,等 tap 确认后解冻,最早事件必然落在窗口内(非预热/Fixture 路径正是这样做,对可调试 App 有效)。

但真机验证:对高德 start_suspended=True 直接报

"The app must be debuggable and signed with 'get-task-allow'. Permission to debug com.autonavi.amap was denied. Unable to acquire task port."

挂起启动需要 debug 权限(get-task-allow),App Store 生产 App 没有 → 被系统拒绝。 这与 xctrace 无法启动生产 App 同源。这就是预热路径当初就用「非挂起启动」的根本原因——不是设计疏忽,而是生产 App 只能非挂起启动。故 §3.2 竞态对生产 App 无确定性软件解。

4. 已做的尝试与结论(逐项) ​

4.1 已交付且有效 ​

  • 两遍生命周期归因(launch_trace.py _rebuild_semantics):先建完整 thread→pid 表再发标记,早期阶段不再因「目标线程绑定记录晚到」被丢。E4:System/Static missing 3→0、willFinish 4→1,淘宝/京东恢复,微信保持 9/9,无退步。
  • scene/可选回调 not-applicable 分类(startup-timing §7 v45):Total 有效(必需齐全)时,完全未观测的可选回调判 not-applicable;孤立的、被误归因的 END(无 BEGIN,§3.1 的幻影)也判 not-applicable,不再显示为 missing;有完整 span 或仍开启 BEGIN 的真实缺口仍保留 missing。工作台隐藏 not-applicable 阶段。E4:QQ/北京银行两轮 scene+willFinish 全 not-applicable(隐藏),connectTo missing 10→3、enterFG 12→5。
    • 校正(codex 复核):not-applicable 的正当性依赖 §3.1 的独立证据(System Trace 全系统 0 条),而非仅凭"没观测到 BEGIN"——"未见 BEGIN ≠ 回调没执行",早期竞态(§3.2)同样会只留下孤立 END。隐藏 not-applicable 只是呈现选择,不改变真实采集完整度;证据不足时保持 missing。
  • 完成等待改为 liveness 判据(尾部/中段缺失修复):解码器暴露单调递增的 decodedRecordCount;基础宽限期(STARTUP_CORE_PROFILE_COMPLETION_GRACE_SECONDS,4s)到点时,若源仍在吐新记录 → 消费一次短探测延长(STARTUP_CORE_PROFILE_LIVENESS_PROBE_SECONDS,1s)并随记录持续到来逐段续期至硬上限(STARTUP_CORE_PROFILE_MAX_WAIT_SECONDS,15s);若记录停滞 → 立即收尾(设备丢包,再等无用)。用户立即停止始终优先。回归测试:test_startup_flush_publishes_total_then_restarts_frame_source / test_startup_flush_extends_for_open_initial_frame。
    • 演进原因(真机四轮回归 + codex 复核):初版窄修法仅在 didFinishLaunchingOpen / initialFrameOpen 时延长。百度网盘、高德各四轮验证显示:①该延长分支一次都没触发(extended 全程 false,超时那刻开着的常是更早的 sceneWillConnectTo/UIKit Scene Creation);②按"哪个阶段 open"逐个加白名单既不完备、也无法区分"启动慢"与"设备丢尾段"。改用 liveness 后,跟得住慢但在传输的启动、对停滞快速放弃,比阶段白名单更普适。
  • 预热切换 rebind 降级标志传播:空闲切换 App 时不再丢失 startupSourceUnavailable,避免误判"CoreProfile 通道未武装"致开始按钮置灰。

4.2 尝试后否决/回退 ​

  • CoreProfile bm(buffer mode)0→1:真机测 QQ,scene 仍缺,无效,已回退。缓冲大小未被 pymobiledevice3 config 暴露,且扩大设备缓冲越过「pymobiledevice3 为唯一传输所有者、仅对齐冻结配置」的架构边界。
  • 挂起启动(方案2):见 §3.3,真机确认对生产 App 被 get-task-allow 拒绝、会导致所有生产 App 采集失败,已回退。
  • 自动补采一轮:分析后否决——§3.2 是每次独立的概率事件,补采只是再掷一次骰子,不保证完整(甚至可能第一次全、补采反而缺);且表现为 App 连续冷启动两次,可见、耗时、语义乱。

4.3 取证手段 ​

  • Apple xctrace / System Trace:确认 §3.1(生产 App 不发 scene signpost);但 --launch 对生产 App 被 get-task-allow 拒、--all-processes 对第三方 App 生命周期抓得浅——Apple 自己也无法完整取生产 App 的启动生命周期,我们的 CoreProfile tap 反而更全。
  • 离线原始 kdebug 重放:确认 §3.2(最早事件 count=0、lossFree=true)。

5. 结论、当前可用性与诚实边界 ​

  • 归因类工具 bug 已消除;大多数轻/中量 App 启动九阶段可稳定完整采集。
  • 剩余不完整分两类,均非可修的工具 bug:
    1. 重型 App 不发 scene 回调(App 行为,已 not-applicable 隐藏);
    2. 最早事件的设备采集竞态(间歇、随机;对生产 App 因 get-task-allow 无确定性解)。
  • 务实缓解:需要完整启动数据时对目标 App 多冷启动几次取最完整的一轮;工具已如实标注缺失与原因,不误导。
  • 不做:不为"好看"把真实 missing 隐藏;不越传输所有者边界改设备缓冲;不对生产 App 用挂起启动。

6. 证据索引(E4,设备 A / iPhone15,4 / iOS 17.0,匿名别名) ​

  • 10 App × 2 轮启动矩阵(两遍归因前后对比)。
  • QQ / 微信离线码级重放(scene code 300/301/312/313 存在性对比)。
  • QQ Apple System Trace(52639 行 QQ kdebug、0 条 0x2B87、code 300/312/313 全系统缺)。
  • 高德 r1 离线重放(UIKit Init BEGIN count=0、lossFree=true)。
  • 高德 start_suspended=True 的 get-task-allow 拒绝错误。

7. 设备侧丢事件标记的直接观测(Phase A 单次样本回放) ​

用只读脚本 MacApp/scripts/startup-integrity-reconcile.py 对已落盘的样本回放(emit_completed_trace=False、retain_all_records=True),做"接收/落盘 ↔ 解码(任意线程 BEGIN/END) ↔ retain-all 报告"逐阶段照合,并扫描内核 TRACE_LOST_EVENTS(0x07020008)。

样本(各一次冷启动):

样本缺失段落(any BEGIN/END)TRACE_LOST_EVENTS解除解码期裁剪后
高德(尾部)sceneWillEnterForeground 0/0;Initial Frame 0/13(BEGIN 缺);sceneWillConnectTo 1/02未恢复缺失阶段
百度(头部)willFinish 0/0;didFinish 0/0;UIKit Init 1/0;Initial Frame 0/203未恢复缺失阶段
对照(完整)九阶段齐全7—

收紧后的结论(严格限定在本脚本证据范围内):

  • 已直接观测到设备侧丢事件标记;对现有样本解除解码期裁剪后,未恢复缺失阶段。
  • 完整轮同样带非零标记(7 条)→ 标记存在 ≠ 段落缺失;是"丢到哪些码"的抽签。host 侧 lossFree=true 与 deviceLostEventMarkerCount>0 并存,再次印证 lossFree 只覆盖主机队列。

不外推为(超出本脚本证据范围):

  • 并非"所有接收、落盘前裁剪与交接都已排除"(现有样本本就由裁剪链路产出,无法反证落盘前交接);
  • 并非"每一个缺失阶段都已证明由设备丢失导致"。

生产报告透出(本批已实现):启动报告 sourceQuality.deviceLostEventMarkerCount 透出本轮标记数,UI 显示"本轮检测到设备丢事件标记 N 条"。语义护栏:N 为标记数、非丢失事件总数;字段缺省显示"未检查",为 0 显示"未观测到丢失标记(≠ 全链无损)";阶段覆盖与源完整性分开显示(九阶段齐全可与设备丢失告警并存);计数限定于启动采集窗口,不重复计入预热/重复解码/报告刷新。

后续实验触发条件(暂不启动 Phase B):先随正常使用积累样本,分别统计①丢失标记出现率、②必需阶段完整率、③丢失与采集负载的关系。仅当出现"高负载明显伴随更多丢失"等信号,再做精简单源采集对照;届时验证的是降低后续采集的丢失概率,而非找回已丢事件。九阶段继续保持 partial/candidate,不承诺单次稳定采齐。

版本历史 ​

  • 2026-09-15 v2:新增 §7——用只读回放脚本直接观测到设备侧 TRACE_LOST_EVENTS 标记;生产报告透出 deviceLostEventMarkerCount(含语义护栏与显示口径);给出收紧结论与 Phase B 触发条件;样本高德=2/百度=3/完整=7。
  • 2026-09-14 v1:首次记录启动采集现状、三类根因与全部尝试;关联启动阶段与预热修复选项与启动指标知识页 §7 v45。

Perfowl · Performance Observer