外观
启动阶段细分与预热进程治理 · 下一阶段修复选项 v1
背景:Build54 已修复 prepared 采集复用预热进程时的所有权移交缺陷(
AsyncStream.onTermination=forceStop叠加"取消旧消费者再复用同一流"导致预热进程被杀),当前设备 A / 目标 App A 冷启动→采样→停止→在线封存链路 E4 通过(completed、24 样本)。本文件把用户在真机复测中提出的 5 项后续问题(其中 2 项为同一分类问题,合并后 5 个选项)研究后列为下一阶段修复选项,不改动产品代码,等待逐项批准后按 7 步流程实施。
真机诊断基线(E4,设备 A iPhone15,4 / iOS 17.0 / USB)
用两个 App 对照冷启动,隔离"App 特征"与"解码器局限":
| 阶段 | 系统「设置」App | 目标 App A(北京银行) |
|---|---|---|
| System / Static / UIKit Init | candidate | candidate |
| UIKit Scene Creation | candidate | candidate |
| willFinishLaunchingWithOptions() | not-applicable | missing / phase-not-observed |
| didFinishLaunchingWithOptions() | candidate 2.4 ms | candidate 1458.7 ms |
| sceneWillConnectTo() | candidate 24.3 ms | missing / phase-boundary-incomplete |
| sceneWillEnterForeground() | missing / phase-not-observed | missing / phase-not-observed |
| Initial Frame Rendering | candidate | candidate |
关键计数(目标 App A 会话):unmatchedEndCount=1、supersededBeginCount=2、unknownLifecycleFingerprints=0、requiredMissingPhases=[]、totalDurationMs≈1860 candidate。
根因收敛为一条:目标 App A 的 didFinishLaunchingWithOptions 在主线程同步耗时约 1458 ms,期间嵌套/重叠的生命周期事件使 begin/end 栈产生 1 个未配对 end(unmatchedEndCount=1、supersededBeginCount=2)→ 解码器整体 status 从 candidate 降级为 partial → 连带:① willFinishLaunching 未走 not-applicable 重分类而显示 missing;② sceneWillConnectTo 边界未配成对。sceneWillEnterForeground 则两个 App 均缺,属另一类(覆盖盲点)。
诚实边界:以上 missing 均为真实观测结果,总启动时长有效(requiredMissingPhases=[])。任何方案不得以填 0 或编造补齐;只允许"合法不适用则标 not-applicable / 不显示"与"确有信号但配对失败则修复配对"。
选项 A(用户第 1 项)· 合理缺失不在启动指标上显示
目标:not-applicable(App 未实现的可选回调、完整启动下未观测的场景回调)不在启动阶段时间线中占位,减少视觉干扰;missing(本应有却缺失)仍保留并显式标注原因。
代码落点
MacApp/Sources/PerfowlCore/StartupTracePresentation.swift:75—phases = CollectorLaunchTrace.phaseDisplayOrder.map { ... }生成全部 9 个阶段。MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift:1516—ForEach(presentation.phases)全量渲染。
方案:在 StartupTracePresentation 增加派生属性 visiblePhases(phases 保持完整用于导出/审计),过滤规则:status == .notApplicable 的可选阶段不进入可视集合;missing 保留。UI 的 ForEach 改用 visiblePhases;并在时间线尾部补一行"N 个不适用阶段已隐藏"的可展开说明,保证信息不丢失、可追溯。
关键路径 / 复杂度 / 耗时:纯展示层派生,O(9) 过滤,无采集路径开销;实现约 0.5 人日。仅 Swift,新增 SwiftUI 单元测试断言"not-applicable 不出现在 visiblePhases、missing 仍在"。
口径 / 公式:不改指标口径,仅显示;无需公式入档。
风险:需与选项 B 协同——若 B 未落地,partial 会话下可选回调仍是 missing(不会被 A 隐藏),显示不一致。建议 A、B 同批。
优先级:中(体验),依赖选项 B。
选项 B(用户第 2、4 项,合并)· not-applicable 分类在 partial 下对可选回调也生效
目标:可选回调(willFinishLaunchingWithOptions、sceneWillConnectTo、sceneWillEnterForeground)在启动整体有效但非 candidate(即 partial)时,也能被判为 not-applicable,而不是一律落到 missing,实现分类一致性。
代码落点
MacApp/Sidecar/perfowl_collector_core/launch_trace.py:1308-1319— not-applicable 重分类当前被if status == "candidate":门控,partial时不生效。launch_trace.py:1274—required_phases已明确这三项为非必需(可选)。
方案:将 not-applicable 重分类的触发条件从"整体 status==candidate"放宽为"整体 status ∈ {candidate, partial} 且 requiredMissingPhases 为空且该可选阶段 spanCount==0"。即:只要主链路可信、必需阶段齐全,未观测到的可选回调判 not-applicable(原因区分 callback-not-observed-in-*-launch)。missing 仅保留给"应有却因配对失败而缺"的情况(与选项 D 边界修复配合)。
关键路径 / 复杂度 / 耗时:单函数分支条件调整 + 单元测试;O(阶段数),无额外扫描;约 0.5 人日。
口径 / 公式:涉及 ios.process.startup.* 的质量语义(missing vs not-applicable 判定),属指标口径变更,须先公式入档(更新启动指标知识页的"质量语义与异常处理"要素),再改代码与测试。
风险:需避免把"确实应有但漏抓"误判为 not-applicable。护栏:仅当 requiredMissingPhases==[] 且该阶段确为可选清单成员时才允许;sceneWillConnectTo 的 boundary-incomplete(有半个边界)应优先由选项 D 修复为 candidate,而非在此判 not-applicable。
优先级:中高(正确性 + 一致性),选项 A 的前置。
选项 C(用户第 3 项)· sceneWillEnterForeground 覆盖盲点排查
目标:确认 sceneWillEnterForeground 在冷启动 suspended-launch 路径下"是解码器 profile 未映射该边界"还是"iOS 冷启动首次连接场景本就不发该信号",据此决定是补映射还是正式判为 not-applicable。
证据:系统「设置」App(标准 UIScene)该阶段同样 missing/phase-not-observed,unknownLifecycleFingerprints=0(无未识别指纹)。说明不是"信号在但没映射成未知指纹",而是"该 phase 边界在有序状态机里没有被推导出"或"信号根本不发出"。
代码落点
MacApp/Sidecar/perfowl_collector_core/launch_trace.py:113-116—0x2B87的 codes(21,23,24,25,26,300,301,312,313,12)作为"ordered lifecycle transitions"进入状态机。MacApp/Sidecar/perfowl_collector_core/startup_trace_codes.json—transitionTable为文档性说明,真实 code→phase 推导在解码器状态机内。- 解码器
consume(launch_trace.py:825起)内的有序 phase 推导。
方案(调研为主,E4 驱动):
- 采一份含目标 App A 与系统 App 的原始 CoreProfile chunk,离线枚举
0x2B87各 code 的到达序列与 arg0,比对状态机推导出的 phase 边界,定位sceneWillEnterForeground对应 code 是否出现、是否被前序 phase 吸收。 - 若 code 存在但未推导 → 在状态机补该边界(需真机 E4 佐证 code 语义,遵守"未 E4 不改九阶段 mapping"门禁)。
- 若 code 不出现(iOS 冷启动不发)→ 正式将其纳入选项 B 的 not-applicable 可选清单,并在知识页注明"冷启动路径不产生该回调"。
关键路径 / 复杂度 / 耗时:离线分析脚本 + 至少两轮 E4 对照;约 1.5–2 人日(含证据留存)。运行期无新增开销。
口径 / 公式:可能新增/调整 ios.process.startup.scene_will_enter_foreground_ms 的采集口径,须公式入档 + E4 证据方可从 missing 改判。
风险:最高不确定性项;严禁在无 E4 因果证据下扩大 mapping(既有门禁)。
优先级:中(依赖真机取证),可与 D 合并取证。
选项 D(用户第 3 项延伸)· 边界配对健壮性:消除长 didFinishLaunching 嵌套下的 unmatched end
目标:目标 App A 因超长 didFinishLaunching(约 1458 ms)内嵌套/重叠生命周期产生 unmatchedEndCount=1、supersededBeginCount=2,使整体降级 partial 且 sceneWillConnectTo 边界不完整。改进 begin/end 栈配对,在合法嵌套下正确配对,让 sceneWillConnectTo 恢复为 candidate、整体回到 candidate。
代码落点
MacApp/Sidecar/perfowl_collector_core/launch_trace.py状态机:self._open((thread_id, phase)→begin 栈)、self._unmatched_end_by_phase/_unmatched_end_count、superseded begin 处理、reorder_window_ns=5_000_000(5 ms 重排窗口)。- 配对/渲染:
launch_trace.py:1213-1240(open/unmatched 统计与 phase 质量判定)、:1291-1306(整体 status 判定含not self._unmatched_end_count)。
方案(调研 + 实现):
- 用目标 App A 原始 chunk 重放,导出被 superseded 的 2 个 begin 与 1 个 unmatched end 的 (thread_id, code, arg0, ts),判定它们是"真实嵌套(begin 未闭合前又来同 phase begin)"还是"跨 chunk/线程的乱序"。
- 依据证据改进配对:区分"同线程合法嵌套"(用栈深度正确配对,不算 unmatched)与"跨源乱序"(扩大或分类重排窗口);确保
didFinishLaunching内的子 span 不把 scene 边界吞掉。 - 目标:
unmatchedEndCount→0、sceneWillConnectTo→candidate、整体 status→candidate;did/willFinish数值不回归(用现有 E4 基线做前后对比)。
关键路径 / 复杂度 / 耗时:状态机是启动解码热路径,须保持单遍、O(事件数),重排窗口有界,禁止全量回看;改动后用真实 chunk 基准对比解码耗时(首响应/最差场景),不得退化。约 2–3 人日。
口径 / 公式:影响 sceneWillConnectTo 等 accumulatedMs 的取值边界,须公式入档 + E4 前后对比。
风险:高——配对逻辑改动可能影响所有阶段;必须以现有两轮 E4(Total 1864/1831 ms、6/6 核心阶段)为回归基线,任何核心阶段数值漂移即回退。
优先级:高(直接决定三项 missing 里的两项能否恢复),与选项 C 共享取证。
选项 E(用户第 5 项)· App 退出 / 空闲时备用预热进程的回收
目标:消除"会话结束后 defer 重建的备用预热进程在 App 退出/崩溃后残留、持续占用 CoreProfile tap"的问题(本轮真机复测实测到残留 perfowl_collector_core --prewarm 进程,导致下一次采集 CoreProfile busy→降级无启动分析)。
代码落点
MacApp/Sources/PerfowlCore/PerformanceSessionController.swift:159-165—deinit { prewarmSupervisor?.forceStop(); supervisor?.forceStop() }(SwiftUI App 终止/崩溃时 deinit 不保证被调用)。MacApp/Sources/PerfowlCore/CollectorSupervisor.swift:182—deinit { forceStop() };子进程由Process启动,macOS 下默认不随父进程退出。
方案:
- 父死子亡:Sidecar 启动时建立 watchdog——子进程侧监测父进程存活(如读取 stdin EOF / 传入父 PID 轮询),父不在则自退;或宿主侧为子进程设独立 process group 并在
NSApplication终止通知中批量终止。 - 主动生命周期钩子:在 App 层监听终止/进入后台(
applicationWillTerminate/ScenePhase),主动invalidateCollectorReadiness()。 - 空闲回收:备用预热在"设备/应用未变且长时间未开始采集"时可选择性释放 tap(计时器),下次交互再重建,避免长期独占 CoreProfile。
- 启动自愈:App 启动时扫描并清理上一进程残留的孤儿
perfowl_collector_core/ tunneld。
关键路径 / 复杂度 / 耗时:进程生命周期治理,需真机验证残留是否清零;约 1.5–2 人日。watchdog 采用低频轮询/EOF 触发,避免高频占用。
口径 / 公式:不涉及指标口径;无需公式入档。
风险:误杀正在使用的进程;护栏为严格按 UDID/PID/命令行特征匹配自有进程,绝不动其他应用的 tunnel/tap(沿用现有"只管理自己路由"的边界)。
优先级:中高(直接影响连续采集稳定性与启动分析可得性)。
E4 取证结论(2026-09-14 · 设备 A 10 App×2 轮矩阵 + 离线码级重放)
对设备 A 上 10 款 App(微信/淘宝/支付宝/抖音/百度网盘/QQ/美团/北京银行/京东/设置)各冷启动 2 轮,逐阶段检查,并对 QQ(缺)与微信(全)做原始 kdebug 码级离线重放对比,结论如下:
- 映射是正确的,不是 code→phase 映射 bug:微信两轮 9/9 阶段全 candidate(含
sceneWillConnectTo/sceneWillEnterForeground),证明解码器与 iOS 17.0 21A327 profile 的 code(connectTo=300/301、enterFG=312/313)能正确成相。 - connectTo/enterFG 缺失的真因是「捕获/归因时序间隙」:QQ 目标线程只出现
301(connectTo END),300(BEGIN)与312/313(enterFG)未归因到目标线程 → END 无 BEGIN →phase-boundary-incomplete;enterFG 无任何标记 →phase-not-observed。 - 佐证为时序而非 App 特征/映射:同一 App 轮次间波动明显(京东 r1 多缺、r2 全齐;支付宝 r1 全齐、r2 缺 connectTo;抖音 r1 全缺、r2 部分)。若是映射或 App 行为则应跨轮稳定。
- 早期 boot 阶段偶发缺失(淘宝 Sys/Static 两轮、抖音 r1 全缺)同源:早于目标 PID/线程归因完成的生命周期事件被作用域判为非目标而丢弃。
- 重型 App 的
Initial Frame Rendering缺失(百度网盘)是首帧未在采集窗口内渲染,属另一类(窗口/终点),非本问题。
矩阵覆盖现状(至少一轮拿到完整 9 阶段的 App:微信、支付宝、京东;QQ/美团/北京银行/设置 稳定缺 enterFG,多数还缺 connectTo)。
修复方向(C/D 合并为一个真因):不是改 mapping,而是保留并在目标 PID 归因完成后回溯归因早期生命周期事件(scene BEGIN 常在 attribution 完成前触发而被丢弃)。这属于捕获管线的归因保留(对应历史 v7「目标线程首次归因保留」同一区域),是热路径改动,必须以本矩阵为回归基线做多轮 E4 迭代,不做盲改。
- 第一批(正确性):选项 C + D(共享一次 E4 取证)→ 恢复
sceneWillConnectTo、收敛 unmatched end,判定sceneWillEnterForeground归属。公式先入档。 - 第二批(一致性 + 体验):选项 B → A(分类统一后再做显示过滤)。
- 第三批(稳定性):选项 E(可与任一批并行,无口径依赖)。
每批遵循 7 步流程:研究(固定版本)→ 公式入档(B/C/D 必需)→ 用户明确批准 → 最小实现 + 匹配 E4 验证 → 本地窄提交 + 双远程推送 + 交付 Webhook。
验收门禁(统一)
- 真机 E4:设备 A / 目标 App A 至少两轮;核心 6 阶段与 Total 不回归(以 1864/1831 ms 为基线)。
- 诚实红线:不得以 0 或编造补齐;missing/not-applicable 判定须有 code 级或 App 特征证据。
- 轻量化:启动解码保持单遍有界;改动前后有解码耗时基准对比,无退化。
- 站点联动:涉及口径的批次同步更新启动指标知识页十要素与版本历史(C4)。