外观
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 是否可用。这个方案胜出的原因:
- 用户最关心的主指标(启动多快)不应该被一个已知存在设备侧随机丢失的诊断维度拖累;
- 端点法只依赖两个真实事件,出现概率远高于九个事件全部出现;
- 不掩盖阶段缺失的事实——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 1822C161);已知最好的真机结果是 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. 可复用工程经验
- 诚实的证据门槛比"看起来完整"更重要:Mapping v3 在重复性统计上很漂亮(40 个 fingerprint 通过重复性门槛),但坚持"重复性 + 独立语义证据"双重门槛,最终 0 个映射被提升——避免了把猜测写成事实。
- 旁路冗余源必须验证"真的独立":
os_trace_relay看起来基础设施齐全(独立传输、自带 LOSS、可并发),但一次决定性 spike 就证明它对启动阶段这个具体信号没有覆盖,"看起来独立"不等于"实际独立"。 - 主指标与诊断维度应该有独立的质量门禁:把 Total endpoint gate 与 phase coverage gate 拆开,是用架构手段而不是"多试几次凑齐 9/9"来解决证据不稳定的问题。
- 一次只动一个变量的累积 A/B 比同时改多处更快收敛到真实结论:四条主机侧路线逐一证伪的过程,明确排除了"主机带宽/解码阻塞"这一类容易被误判的方向。
- "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 | 补本月实验结论、失败路线与适用限制;早期阶段数字保留 | 月度梳理 |