Skip to content

Startup 技术演进与实验:从私有映射到端点解耦 ​

2026-10-26 · 从在线准入到有限范围运行观测 ​

下文早期架构和试验数字保留;“最终重放完整”不能追认当时在线已经发布,Process Observed Running也不能直接当冷启动耗时。

9月末—10月初在线实验:SW-1 OFF/ON若ON未满足资格、没有实际应用延长,就只能记inconclusive;最终文件存在还可能是旧修订。实际双sink修复要求事件发出时读回同一版在线报告,目标/epoch基线预先冻结,后到映射不得追认已发布前缀。合成ON正负例/E2包不自动成为正常UI真机稳定性改善。SW-1带日期实施增量。

10月正常入口实验:运行观察五新轮只接受主机首次观察、状态/迟滞及Raw→live→CSV→报告一致性,不是设备出生、物理首帧或业务可用。Total、前台观测及九阶段各立范围;CLI、协议与正常UI结果不能混作同一分母。运行观察 · Total专项与范围。

可复用准入:静帧自有App实测canvas ready且热允许时仍曾active=false,约214.6ms后才active=true;用前台/热事件门一次启动刺激,不用“UI已建好”代替前台证明。它不能反推其它历史启动故障都由同一原因造成。静帧资格轨迹。

避免方式:分开记录入口、来源武装、生命、阶段、实际应用的实验开关和最终持久化;失败不补轮、不把未执行刺激当静态通过;未知端点/阶段仍missing ,不补造语义映射。本节E1复盘,引用E2/E4各限定边界;当前55/66、11项暂停,完整方法见实验索引。

本文提炼 Startup 领域从最初的私有 KDebug 映射尝试,到最终"Total 端点法 + 只展示真实捕获阶段"的完整决策链,聚焦决策依据而不是逐条开发日志。原始细节见文末 devlog 索引。


1. 当前结论先放前面 ​

  • App Launch Total 与九阶段是两个独立的采集/质量维度:Total 使用首尾两个真实端点,九阶段是诊断分解;九阶段缺失不会自动使 Total 失效(Build48 起正式确立,见第 9 节)。
  • 九阶段随机缺失的根因被系统性定位到"设备侧 kdebug / 传输前链路",且用一整套 A/B 实验证伪了当前所有已知的主机侧修复路线和旁路冗余源(第 8 节);这是一条真实的观测边界,不是"还没修好的 bug 列表"。
  • 产品曾短暂切换到 Apple 官方 xctrace App Launch 作为深度 Total/九阶段来源(2026-09-07),在开发签名 Fixture 上验证通过,但因依赖 Xcode 环境、与"任意已安装 App 无侵入黑盒测试"目标冲突,最终没有成为长期默认路径;当前 Registry 记录的标准路径仍是 CoreProfile kdebug lifecycle 解码。
  • 部分"missing"阶段的真实根因是目标 App 自身的主线程长耗时同步工作,而不是采集工具缺陷(第 10 节 Build54 后续诊断)。
  • 2026-10-03 主线状态:独立端点账本、三模式调度审计和 G2 独立生命周期实验代码已合入;G2 选择器未设置时仍走 legacy,不能把合入或 E2 测试说成真机启动时间获取稳定性提升。见第 11 节更新。

2. 最初工程问题 ​

Perfowl 需要在不改包、不注入、不要求开发签名的前提下,量化一次 iOS App 冷启动从进程建立到首帧呈现的真实耗时,并尽量说明时间花在哪个生命周期阶段。iOS 没有公开 API 直接提供这个分解;唯一具备足够精度的私有信号来源是 Instruments 依赖的设备端 CoreProfile / kdebug 生命周期 signpost。

3. 第一代方案与为什么当时合理 ​

第一代方案(p2-launch-analysis-*、p2-startup-kdebug-attribution-v1-* 等)直接订阅 CoreProfile kdebug 流,实时按 debug ID 匹配生命周期事件、按目标 PID 过滤。这个方案在当时合理,因为:CoreProfile 是 Instruments 官方也在用的通道,理论精度最高(真实内核时间戳),且 pymobiledevice3 已经提供了到该 DTX 服务的传输能力,Perfowl 只需要在其上叠加语义映射层。

4. 暴露出的真实问题 ​

  • PID 映射时序问题:启动最初几毫秒,生命周期事件可能早于完整的 thread→PID 身份映射到达;如果 decoder 在第一次看到事件时立即做目标过滤,会把实际属于目标 App 的早期事件误判为非目标(后续用两遍归因修复,见第 5 节)。
  • 阶段随机缺失:即使归因逻辑正确,同一 App 不同轮次仍会出现早期阶段"一轮完整、一轮缺失"的情况,且与主机侧队列丢包证据无关(lossFree=true 时依然发生)。
  • 未知 KDebug 事件的语义空白:目标进程中存在大量当前 decoder 未识别的 KDebug 事件,不能贸然把重复出现的事件猜测为某个具体阶段。

5. 候选方案 A:两遍归因(已采纳) ​

  • 假设:把"识别事件是否属于目标"和"建立目标身份映射"拆成两个独立的遍历阶段,可以消除因映射未就绪导致的早期事件误判。
  • 实验:Pass 1 完整回放建立 thread/pid/generation 身份关系;Pass 2 重新回放并归因 lifecycle records。
  • 判定:修复了主机解码/归因时序问题这一类真实工具 bug,已有专门回归测试和多 App 真机证据覆盖。
  • 结果:采纳,是当前解码器的标准归因方式。今天仍然出现的阶段缺失,不能再简单归因为"PID 映射太晚"。

6. 候选方案 B:未知事件语义普查与映射提升(Startup Mapping v3 / Semantic Evidence v1,已停止推进) ​

  • 假设:通过跨 Session、跨 App、跨系统版本的事件重复性统计,可以把当前 decoder 未识别的私有 UIKit KDebug 事件安全地提升为已知阶段映射的补充证据。
  • 实验(2026-09-06,p2-startup-mapping-v3-result.md):iOS 17.0 与 18.2.1 各 3 次冷启动、每系统覆盖 2 个不同目标 App,6 个 Session 全部 identity verified、drop=0,解码 4,734,239 条 KDebug,40 个事件 fingerprint 达到"同系统 3 次 + 2 App"的重复性门槛。
  • 语义证据补充实验(p2-startup-semantic-evidence-v1-result.md):针对 willFinishLaunching/didFinishLaunching 的 300ms marker 因果审计,"CoreProfile 因果审计通过"为 0/2。
  • 判定:重复性可以量化,但独立语义证据始终为 0——没有证据证明某个稳定重复的私有 code 就等于某个具体生命周期阶段;Apple trace-code 表也没有这些私有 UIKit code 的符号。
  • 结果:停止推进映射提升。这不是采集失败,而是团队明确设定的证据门槛(重复性 + 独立语义证据双重通过)没有被满足;继续沿用现有阶段的 fail-closed 语义(partial/missing),不猜测新映射。

7. 候选方案 C:切换到 Apple 官方 xctrace App Launch(xctrace-app-launch.v1,已停用) ​

  • 假设:改用 Apple 官方工具链导出的启动 trace,可以获得比私有 KDebug 映射更权威、更少猜测的九阶段与 Total。
  • 实验(2026-09-07,p2-startup-xctrace-app-launch-v1-result.md):冻结 life-cycle-period 目标进程筛选、九阶段聚合与 Total 公式;产品链接通——Mac 客户端先用官方工具记录冷启动 trace,成功启动后 Collector Core 只做 passive attach,避免二次启动。设备 A 的 control/willFinish/didFinish 各 3 次全部通过;设备 B 的 control/willFinish 各 3 次通过,didFinish 在 1/3 后设备离线。
  • 判定:在开发签名 Fixture 上 implemented · validated,证明这条链路技术上可行;但 xctrace App Launch 依赖 Xcode 环境完成录制,产品说明也随之标注为"Xcode 条件能力"。
  • 结果:技术验证通过,但未成为长期默认路径。Perfowl 的核心目标是面向任意已安装 App(含商店 App)的无侵入黑盒测试,Xcode 依赖与这一目标冲突。此后(Build48 起,2026-09-12)产品标准路径回到当前 Registry 记录的 CoreProfile 端点方案;xctrace-app-launch.v1 保留为历史 Legacy 实现,不是当前默认口径。

8. 被证伪路线与停止条件(2026-09-15,系统性消因实验) ​

面对"重型 App 冷启动随机漏采个别启动阶段"这一长期未关闭的问题,团队执行了一轮**累积 A/B(Wilson 95% 置信区间,一次只动一个变量)**实验,逐一证伪所有已知可行修法:

路线假设结果
H2 Ready Barrier采集源"就绪"后再放行启动,减少最早事件竞态无显著改善,缺失仍随机
C startup-exclusive启动窗口内延后次要 adapter,独占带宽给 CoreProfile无改善,且引入 target-pid-missing 的 seal 回归——说明问题不在主机带宽
D receive/parse decouple独立解码 worker + 队列,消除主机接收阻塞receiverBlockedMs/rawQueueDepthMax 均无瓶颈信号,缺失不变
CoreProfile action/buffer 参数枚举(ta/bso)解耦生命周期 kdebug 与高体量 baseline/perf 流移除 baseline action 直接杀死整条 CoreProfile 流(0 chunks);可用组合均无法解耦
单轮语义重建(leave-one-boundary-out)用本轮真实事件重建被移除的阶段边界不可行:过渡码是单事件双边界,无法精确恢复;perf/mach 洪流不携带 signpost 身份;唯一可辩护的"最近生命周期邻居"区间宽度实测 0.02–276ms,不足以作为阶段值
System Trace(Instruments)旁路用独立通道补齐 CoreProfile 缺失的阶段底层同为 coreprofilesessiontap + kdebug/kperf,非独立冗余;kperf 单一所有者不能与 Perfowl 的 tap 并发;另起一轮即跨轮拼接,触碰红线;App Launch 模板需要 get-task-allow,生产 App 拉不起
os_trace_relay 统一日志旁路独立传输、可并发、自带 LOSS 标记、带时间戳,理论上可做冗余源决定性 spike 证伪:25s 冷启动 spike 抓到 132,624 条记录,目标进程日志 53,563 条,ACTIVITY_TRANSITION 1,319 条,但 SIGNPOST 全流 0 条,9 个启动阶段边界一个也未出现;关键词命中全是假阳性(如 FirstFrame 命中的是 CoreMedia 视频首帧);根因是 UIKit 启动阶段 signpost 是 kdebug-only(对应 CoreProfile 的 0x2B87 class),不进统一日志系统
StartSuspended 确定性等待挂起启动直到采集源就绪再放行,换取确定性开发调试包可行,但生产 App 缺少 get-task-allow,不符合无侵入生产包目标
跨轮拼接用多轮里各自完整的片段拼出"人工 9/9"明确禁止:这条启动链路在物理世界中从未发生过

停止条件:当主机侧全部四类修法(Ready Barrier / startup-exclusive / receive-parse decouple / CoreProfile action matrix)均无法改变缺失模式,且两条独立旁路源都被证明"非独立"或"没有目标信号"时,团队判定随机缺失发生在设备侧 kdebug 缓冲或传输前链路,现有公开可控配置无法进一步区分具体丢失点,停止继续投入消因实验。

9. 最终方案为什么胜出:Total 端点法与阶段覆盖解耦(Build48,方向 1) ​

面对"设备侧丢失无法进一步修复"的现实,团队采用方向 1:

text
App Launch Total = System Interface BEGIN → Initial Frame END(两个独立端点)
九阶段 = 诊断覆盖度,只展示真实捕获的阶段

Build48(2026-09-12)正式把"任一必需内部阶段缺失 → Total 必须清空"的旧 v3 逻辑,拆分为 Total endpoint gate 与 phase coverage gate 两个独立门禁:Total 只要求 System Interface BEGIN 与(配对到的)Initial Frame END 成立;九阶段完整度成为独立的诊断覆盖度指标,不再决定 Total 是否可用。这个方案胜出的原因:

  1. 用户最关心的主指标(启动多快)不应该被一个已知存在设备侧随机丢失的诊断维度拖累;
  2. 端点法只依赖两个真实事件,出现概率远高于九个事件全部出现;
  3. 不掩盖阶段缺失的事实——UI 仍在 footer 披露 X/9,不补 0、不插值。

10. 从 POC 到产品化又踩了什么坑(Build49~57) ​

  • Build49/50:实时呈现启动 Total 与阶段修订版本,把离线解码逐步搬到实时链路。
  • Build51:归因保留策略调整,避免归因中间态被过早丢弃。
  • Build52:引导(bootstrap)链路修复。
  • Build53(2026-09-14):验证阶段排空(drain)在 completion grace 内有界完成、阶段 revision 在停止前产生;双轮真机测试首次在同一目标 App 上稳定拿到 6/9(willFinishLaunchingWithOptions()、sceneWillConnectTo()、sceneWillEnterForeground() 两轮均未形成完整 span),报告原文明确"6/9 阶段是目标 App 当前可观测结果,不能描述为九阶段完整校准"。
  • Build54:修复"prepared 采集复用预热进程时的所有权移交缺陷"(AsyncStream.onTermination=forceStop 叠加"取消旧消费者再复用同一流"导致预热进程被杀);随后的真机复测发现一个重要事实(p4-startup-and-prewarm-fix-options.md)——用"系统设置 App"与"目标 App A"对照冷启动,目标 App A 的 didFinishLaunchingWithOptions() 在主线程同步耗时约 1458.7ms,期间嵌套/重叠的生命周期事件产生 1 个未配对 end(unmatchedEndCount=1,supersededBeginCount=2),导致解码器整体状态从 candidate 降级为 partial,连带 willFinishLaunching 未被正确重分类为 not-applicable 而显示 missing。这说明部分"missing"阶段的真实根因是目标 App 自身的主线程长耗时同步工作,不是采集工具缺陷;总启动时长本身仍然有效(requiredMissingPhases=[])。
  • Build55:修复 CoreProfile 运行时降级问题。
  • Build56:修复一个真实的"预热"缓存 bug——CoreProfileRawAdapter.prepare_transport() 曾在应用选择前的预热阶段就实例化 Tap 并缓存旧 filter 配置,导致仅选 FrameTime 时永远拿不到呈现帧掩码;架构上改为 start()/restart() 时才根据本次 Session 最终 filters 动态构造全新 Tap 实例;同时把启动+FrameTime 首值延迟从 19s 的固定 2.0s sleep 优化为事件驱动双向竞速,早停等待耗时降到 0.108s。这个"预热"指的是 Sidecar 自身采集适配器的传输准备阶段,与 iOS 系统级 App Prewarming 是两个不同概念,容易混淆,特此说明。
  • Build57:把 Startup 与 FrameTime 统一到单一 CoreProfile 连续采集架构,9 项专项单元测试全量通过。

2026-09-22 补充(Phase A / Phase B,勿与 Build56 混淆):Build56 的教训是「预热阶段不得实例化 Tap、不得缓存旧 filter 配置」。2026-09-22 落地的 Phase B 又把 Dedicated Startup 的传输准备搬回了「App 选定」这一预热时点,因此必须说清边界:prepare_transport() 只做两件事——CoreProfileSessionTap.get_time_config() 与 CoreProfileSessionTapChannel.connect()(即只建立 coreprofilesessiontap 服务通道);它不 claim adapter、不实例化 Tap、不设置 filters、不进入订阅。Tap 依旧只在 start() 里用本轮真实 options 构造,且 start() 会显式把任何预热过的流置空(self._prepared_stream = None),逐字保留「Always build a fresh stream with the actual session options; never reuse a prewarmed tap instance」。

因此 Phase B 不是 Build56 问题域的回退:Build56 修的是「Tap 实例与 filter 被提前固化」,Phase B 只提前了「通道已连、可写」。两者的分界在架构上被固化为 transport_prepared != recording_active——通道热了不代表录音开始,adapter 占用与 kperf 仍然只在 start() 发生。同一批还顺带拆出了 Startup 的独立 provider / DTX owner(Phase A),使 Startup 的失败域不再牵连同一条 CoreProfile 通道上的 FrameTime。

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

2026-10-03 更新:SSO-0A/1A 的端点与调度改进、G2 的独立生命周期采集已进入主线。G2 仅在显式 PERFOWL_STARTUP_CAPTURE_MODE=dedicated-lifecycle 时选择实验路径,默认保持 legacy;本次合入有 E2 测试证据,但未完成 G2 真机 C0/C1 与 SW-1 正式 UI E4 对照。因此启动时间获取稳定性改善仍为 NOT PROVEN,A1 正式 UI E4 仍 OPEN,Hybrid 仍 FROZEN。主线集成记录 · 知识库口径同步。

  • 商店 App 九阶段完整捕获率历史上为 0/30(S5/S6-B,2 设备 × 3 商店 App × 5 轮,iOS 17 21A327 / iOS 18 22C161);已知最好的真机结果是 Build53 单个目标测试 App 的 6/9,尚未出现过商店 App 的 9/9 真机证据。
  • iOS 26/27 目前只有 E2 静态 schema 候选(no-ios26-device-evidence / no-ios27-device-evidence),没有真机证据,不得外推。
  • 设备侧随机丢失的具体机制(内核 kdebug ring 溢出 / CoreProfile 内部导出批处理 / 设备→DTX 前链路中的哪一环)仍未被独立证据区分,只是被系统性消因实验收紧到"host consumer 之前"。
  • Total 端点修复(repaired/start-substituted)的长期真机分布尚未形成独立的专项统计批次。

12. 可复用工程经验 ​

  1. 诚实的证据门槛比"看起来完整"更重要:Mapping v3 在重复性统计上很漂亮(40 个 fingerprint 通过重复性门槛),但坚持"重复性 + 独立语义证据"双重门槛,最终 0 个映射被提升——避免了把猜测写成事实。
  2. 旁路冗余源必须验证"真的独立":os_trace_relay 看起来基础设施齐全(独立传输、自带 LOSS、可并发),但一次决定性 spike 就证明它对启动阶段这个具体信号没有覆盖,"看起来独立"不等于"实际独立"。
  3. 主指标与诊断维度应该有独立的质量门禁:把 Total endpoint gate 与 phase coverage gate 拆开,是用架构手段而不是"多试几次凑齐 9/9"来解决证据不稳定的问题。
  4. 一次只动一个变量的累积 A/B 比同时改多处更快收敛到真实结论:四条主机侧路线逐一证伪的过程,明确排除了"主机带宽/解码阻塞"这一类容易被误判的方向。
  5. "missing" 不总是工具问题:Build54 后续诊断发现目标 App 自身的主线程长耗时会级联影响阶段解码状态,说明在对外宣称"采集边界"之前,值得先确认缺失是否来自被测 App 自身行为。

13. 原始 devlog 索引 ​

  • 最初方案与两遍归因:p2-launch-analysis-plan.md / -result.md,p2-startup-kdebug-attribution-v1-plan.md,p2-startup-lifecycle-decoder-v3-result.md
  • 映射普查与语义证据:p2-startup-mapping-v3-plan.md / -result.md,p2-startup-semantic-evidence-v1-plan.md / -result.md,p2-startup-source-schema-census-s6a-plan.md / -result.md,p2-startup-source-shape-attribution-s6b-plan.md / -result.md
  • xctrace 切换与停用:p2-startup-xctrace-app-launch-v1-plan.md / -result.md
  • 商店 App 跨系统矩阵:p2-startup-store-app-e4-s5-plan.md / -result.md
  • 系统性消因与旁路证伪:p4-startup-timing-capture-status-and-limits.md,p4-startup-capture-stability-and-sidechannel-investigation.md,p4-startup-kdebug-ring-buffer-experiment.md
  • Total/阶段解耦与产品化:p2-build51-startup-attribution-retention.md,p2-build52-startup-bootstrap-repair.md,p2-build53-startup-drain-e4-validation.md,p4-build48-startup-runtime-repair.md,p4-build49-live-startup-presentation.md,p4-build50-startup-revision-presentation.md,p4-build54-live-metric-display-selection.md,p4-startup-and-prewarm-fix-options.md,p4-build55-coreprofile-runtime-degradation.md,p4-build56-coreprofile-prewarm-decouple-and-fast-rotation.md,p4-build57-unified-coreprofile-continuous-capture.md

完整正文见 site/devlog/index.md 时间线或对应 /archive/ 归档页。

14. 延伸阅读 ​

版本历史补记 ​

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

Perfowl · Performance Observer