外观
Build74 Phase 2 后续最小修复方案(Stop-drain 生命周期补齐 + FrameTime 双时间域)
角色 Lead AI(方案阶段) · 2026-09-24 · 判据
judgement = pending-lead-ai·codeChanged = NO依据:R3 取证提交1ef33f7(父f5da818)之交付物artifacts/build74-owner-e4-followup-20260924-1232/冻结产品1ec9462(Build 74);审计基线4c1e424本批只交方案,不修改冻结产品、不重打包、不删除任何原始 Session /.trace。
0. 授权与边界(先读)
| 项 | 本批处置 |
|---|---|
| 角色与阶段 | Lead AI 方案阶段。AGENTS.md §工作流程:「批准边界:'继续分析 / 把方案写细' = 调研」⇒ 只读、不改产品代码 |
| 改动产品代码 | 不做。任何改动都需 Project Owner 明确批准后由 Implementation AI 执行 |
| 「不得无条件改标全部 chunk」 | 遵守。方案不修改采集侧 coreProfileStreamRole 的标定逻辑(realtime.py:4049-4054 逐字节不动) |
「不得放宽 _recover_total_endpoint 锚点」 | 遵守。方案不触碰 _lifecycle_anchor_ticks() 与 Mode A candidates 过滤器 |
| 「禁止引用反事实 2300.676291 ms 为真值」 | 遵守。该值在本方案中只以 被否决反事实 身份出现,并在 §6 给出结构性防误用标记 |
Device Network(__aenter__ / 逐源失败隔离 / missing 语义) | 另开产品批次。本方案正文不含其修复项(§8.5 只给出与本次报告映射的边界划分) |
| Phase C Hybrid Total | 保持 FROZEN。本方案不产出、不推断、不改动任何 Hybrid 值 |
| 原始数据保护 | 原始 Session、原始 .trace、他人未提交文档(MacApp/Tests/1.trace/、site/devlog/p4-build74-instruments-…audit-plan.md、site/devlog/p4-build74-owner-e4-…plan-20260924.md)零读写副作用;全部离线分析在私有归档的副本上进行 |
1. 一页结论
任务 A:根因已被完整钉死(因果链单因、可机械复算)
四轮(S2 / r1 / r2 / r3)的采集侧遥测结构完全相同:coreProfileCaptureMode = "startup-only"、coreProfileRestartCount = 0、startupCoreProfileCompletionWaitStatus = "timed-out"、reason = "startup-completion-grace-expired"、grace 4.0 s。
实测因果链(r3,主机单调域,相对 launch.requested.raw):
| 里程碑 | 主机偏移 | 说明 |
|---|---|---|
startup_core_profile_completion_wait 超时 | ≈ +4.00 s | 活线解码器仍有 openPhases = ['UIKit Scene Creation'] |
t5 startupSemanticCompleteNs(startup_semantic_complete.set()) | +8 227.300 ms | realtime.py:4813 |
handoffRequestedNs | +8 236.281 ms(t5 + 8.981 ms) | 立即停 |
handoffStopAckNs | +8 236.847 ms(t5 + 9.547 ms) | |
首个 frame-continuation chunk 到达 | +8 544.005 ms(t5 + 316.705 ms) | 角色翻转的观测证据 |
| IFR opener 所在 chunk 到达 | +10 458.920 ms | 设备域 +2 246.633 ms(相对 launch start) |
| IFR closer 所在 chunk 到达 | +11 127.077 ms | 设备域 +2 300.676 / +2 313.027 ms |
| drain 结束 | +12 247.724 ms(t5 + 4 020.42 ms) | handoffDrainStatus = "timeout"(drain 上界 4.0 s) |
关键判读:coreProfileStreamRole 由主机侧完成信号决定(realtime.py:4051-4055:role = "frame-continuation" if (prestarted_core is None or startup_semantic_complete.is_set() or startup_rotation_complete.is_set()) else "startup-lifecycle")。四轮中 startup_rotation_complete 从未置位(restartCount = 0),故翻转由 startup_semantic_complete 单独驱动,实测翻转点落在 t5 之后 316.705 ms,与源码顺序一致。
startup-only 模式下不存在帧续采(restartCount = 0、handoffRestartCompleteNs = null),因此 drain 段携带的仍是启动窗口内的原始记录(实测设备域 +2.25 s / +2.31 s,早于 t5 的主机时刻 8.2 s)。该 frame-continuation 标签是主机侧策略产物,不是关于数据内容的证据——这正是缺陷本体。
任务 A 的定量边界(本方案最强结论)
在冻结解码器上,逐轮清点「位于 frame-continuation+post-stop-drain chunk 内 且 在生命周期命名空间 且 被产品自身 _thread_pids 归到目标线程 且 _lifecycle_actions() 非空」的记录:
| 轮次 | 控制角色 | 已准入(可产 marker) | 新增可准入 | 同条件但任意线程 |
|---|---|---|---|---|
| S2(历史参考) | NEGATIVE | 14 | 0 | 9 |
| T1-r1 | NEGATIVE | 9 | 0 | 0 |
| T1-r2 | NEGATIVE | 13 | 0 | 0 |
| T1-r3 | POSITIVE | 14 | 3 | 31 |
r3 的 3 条恰为:0x2B870030(code=12,END UIKit Scene Creation + BEGIN Initial Frame Rendering)与 2 × 0x31CA0006(END Initial Frame Rendering),全部 tid 归属目标线程,opener 早于首个 closer 54.0437 ms。
两条 S2 旁路事实(两者都独立保证负向回归不被破坏):
- S2 在 drain chunk 内确实有 9 条生命周期命名空间可产 marker 的记录,但无一条归属目标线程 ⇒ 目标 PID/generation 门天然把它清零;
- r1 / r2 的
frame-continuationchunk 不含任何 binary chunk(全部为bplist,零 kd 记录,与 R3 取证一致)⇒ 连清点对象都不存在。
⇒ 「有界 Stop-drain 补齐」的准入增量是 3 / 0 / 0 / 0,可论证为恰好命中 r3 且对三轮负向零副作用。
任务 B:报告映射问题已定位到具体的缺失映射行,且是「映射缺口」不是「数据缺口」
| 已请求显示指标 | valueQuality | sampleCount | 现存的唯一可能生产者 | 实际状态 |
|---|---|---|---|---|
ios.frame.time | candidate | 1686 | online_report._apply_frame_coverage()(metric_map 有该行) | 已接通 |
ios.display.refresh_rate | missing | 0 | DisplayPerformanceProjector.refresh_rate_hz | 无映射行 |
ios.foreground.fps | missing | 0 | DisplayPerformanceProjector.frames_per_second(trailing 1 s) | 无映射行 |
ios.frame.pacing_jitter | missing | 0 | DisplayPerformanceProjector.frame_pacing_jitter_ms(10 s 窗 P95−P50) | 无映射行 |
ios.frame.time.p95 | missing | 0 | DisplayPerformanceProjector.frame_time_p95_ms(10 s 滚动 P95) | 无映射行 |
机械证据(同一次 Session d3206bbe…):
metrics-derived.jsonl= 0 字节;metricSHA256 = e3b0c442…(空串的 SHA-256);metricIDs = [];frameCount = 0;formulaVersions = [];而consumedRawEventCount = 1717、lastConsumedRawSequence = 1717⇒ 派生引擎吃满整条流、产出为零。- 五项
coverage[]全部coverage = []、reason = null,且ios.frame.time唯一有样本(1686,来自frameSummary.intervalCount)。⇒coverage窗口数组对全部五项都没接线。 online_report.py:1341-1370的_apply_frame_coverage.metric_map含ios.frame.time与 14 个ios.frame.*兄弟项,唯独不含上表四条。- 四个 metricID 在
metric_engine.py中完全不出现(该引擎只有ios.foreground.fps一条,且源字段是CoreAnimationFramesPerSecond,属graphics.raw路径而非本次的display.frame-lifetime.raw)。 display_performance.py:2011-2019的DisplayPerformanceProjector已实现全部五项并写明口径(trailing 1 s / 10 s 滚动 P95 / 硬件 VSync / P95−P50),甚至已内置FRESHNESS_STALE_HOST_GAP_NS = 1.0 s的「陈旧但不伪造 0」规则;它被realtime.py:819正常喂入,但它产出的值只落在活线样本字典(realtime.py:1074-1097的metrics["framesPerSecond"]等 camelCase 字段),从不进入coverage[]/sample_count(后者的唯一写入点是online_report._consume_metric_frame(),只吃 metric-ID 形状的帧)。- 顶层
dataCompleteness = "complete"与「5 项里 4 项零样本」并存 ⇒ 报告自相矛盾。
结论:四项 sampleCount = 0 是映射/接线缺口,不是采集缺口——同源同窗的 ios.frame.time 拿到 1686 样本即为反证。修复方向是把 DisplayPerformanceProjector 声明为这四项口径的唯一所有者并接线,而不是去扩 _apply_frame_coverage 的 metric_map(那会造出第二个口径所有者,违反 realtime.py:1013-1015 已写明的「Metric Engine is the sole formula owner」)。
2. 任务 A 的完整因果链(已验证,含反例排除)
2.1 证据链
| # | 事实 | 来源 |
|---|---|---|
| A-1 | opener 0x2B870030(class/subclass 0x2B/0x87,code=12)在 r3 交付字节中恰 1 条,目标线程归属正确、顺序正确(早于 closer 54.0437 ms) | 全量字节台账 |
| A-2 | 该 opener 与 2 条 closer 全部落在 coreProfileStreamRole = "frame-continuation"、streamPhase = "post-stop-drain" 的 chunk 内 | chunk 几何 |
| A-3 | 解码器保留谓词 retain_record = … (lifecycle_namespace and not self._emitted and not is_frame_continuation)(launch_trace.py:1148-1151)⇒ 整段丢弃 | 源码 + 单变量对照 |
| A-4 | 丢弃导致 Mode A 零候选(ignoredTerminalCount = 0)⇒ totalDurationMs = null | 解码器内部态复算 |
| A-5 | 单变量对照(浅拷贝 rawPayload 覆盖 role,不 mock/patch)⇒ r3 立即配出 IFR span;同一改标对 S2 完全无影响 | 正 + 负对照 |
| A-6 | 四轮采集遥测结构相同(startup-only / restart 0 / wait timed-out / grace 4.0 s) | app 层 manifest |
| A-7 | 角色翻转由 startup_semantic_complete 单独驱动,实测落在 t5 + 316.705 ms | 源码 + 到达时刻 |
| A-8 | startup-only 无帧续采 ⇒ drain 携带的是启动窗口记录(设备域 +2.25 / +2.31 s ≪ t5 的 +8.23 s 主机时刻) | manifest + 设备 tick |
2.2 S2 的 null 与 r3 的 null 是两个不同的缺口(不可混为一谈)
| S2 | r3 | |
|---|---|---|
| 设备域是否存在 opener | 否(任意 func、任意线程,全轮 code==12 计数为 0) | 是(恰 1 条,目标线程) |
closer 是否被保留进 _ignored_terminal_events | 是(1 条,tid = 目标线程) | 否(ignoredTerminalCount = 0) |
Mode A 为何返回 None | 候选存在但位于 anchor 之前被过滤(anchor = 未闭合的 sceneWillEnterForeground() BEGIN tick,16 389 393 206 930;候选 tick 16 389 392 355 267 < anchor) | 连候选都没有 |
| 修复后应然 | 仍 null(缺的是字节,不是准入) | 阶段闭合、端点带 stop-drain 来源标记 |
r1 / r2 同理:ignoredTerminalCount = 0 ⇒ Mode A 零候选;且其 drain chunk 全为 bplist。
2.3 明确未定、本方案不升级的三项
- S2 / r1 / r2 的 opener 为何不在交付字节里(环形覆盖 / 不同 UIKit 路径 / 采集窗口截断,三候选均未排除);
- drain 内非目标线程的 31(r3)与 9(S2)条生命周期记录属于谁(另一进程 / 预热侧车 / 其它 target generation 均未区分);
- FrameTime 两轴 ≈4.4 倍差的定性成因(见 §8)。
3. 任务 A 方案:有界 Stop-drain 生命周期补齐
3.1 设计原则
- 不改采集侧标定:
realtime.py的 role 判定与 chunk 打标逐字节不动。「有界」体现在解码侧准入,不体现在改标。 - 准入必须是「补齐」而非「扩权」:drain 记录只能闭合已由保留记录开启的启动阶段;不得凭空开出新的、且结束时仍 open 的阶段。
- 结构性不动 Mode A:drain 记录永不进入
_markers与_ignored_terminal_events,因此_lifecycle_anchor_ticks()的取值集合与 Mode A 的candidates过滤器在构造上与冻结版逐字节一致。 - 来源必须可分辨:drain 挣得的端点不得复用
totalSource = "coreprofile-endpoint"+totalRepairReason = "endpoint"这一「真实配对」标签对(见 §6)。 - 有界资源:仅在存在候选 drain 记录时才多跑一遍;否则(S2/r1/r2 全轮)零增量。
3.2 四道准入门(AND,全部可机械检验)
| 门 | 内容 | 依据 | 本批实测 |
|---|---|---|---|
| G1 来源门 | chunk must be chunkKind == "binary" 且 coreProfileStreamRole == "frame-continuation" 且 streamPhase == "post-stop-drain" | 该 phase 由 adapters.py:1108-1135 在有界 drain 内产出,其时长上界即 STARTUP_CORE_PROFILE_DRAIN_MAX_SECONDS = 4.0 s | r3 chunk idx 49 / 50 命中 |
| G2 身份门 | chunk 的 targetGeneration 等于本次 launch 的 generation(consume() 既有门,launch_trace.py:959-966)且 记录的 tid 经产品自身 _thread_pids 解析后 == self._target_pid | 「同一 target PID / generation」 | S2 的 9 条全部被此门清零 |
| G3 设备时域门 | launch_start_ticks < tick ≤ launch_start_ticks + 15.0 s(且 tick > max(启动侧 chunk 内已见设备 tick)),全部以设备 Mach tick 表达 | 「设备侧时间依据」;上界取 STARTUP_CORE_PROFILE_MAX_WAIT_SECONDS = 15.0(采集方自报的启动预算) | r3 区间 = (+1 529.529 ms, +15 000 ms],三条记录落在 +2 246.6 / +2 300.7 / +2 313.0 ms ⇒ 全部纳入 |
| G4 链闭合门 | 按 _lifecycle_actions() 展开后,被准入的 drain 记录必须构成一条起点为「保留阶段的仍 open span」、终点为「闭合」的链,且准入后净新增 open span 为 0。0x31CA0006 仅当同一批准入集合内存在同 PID/TID 的 code=12 opener 时才可作为启动端点(保留 launch_trace.py:1822-1828 既有契约) | 「补齐」语义;防「静默多出 span」;防把后续应用帧的 0x31CA0006 误当启动端点 | r3:链 = [opener: END UIKit Scene Creation + BEGIN IFR] → [closer: END IFR],起点是仍 open 的 UIKit Scene Creation ✓ |
上界为何取 15.0 s 而不是 max(启动侧 tick):后者被实测否定——r3 启动侧 chunk 内最大设备 tick 仅 +1 529.529 ms,而端点记录在 +2 246.6 ms(采集 tap 停止后才从 drain 交付)。因此单边上界不足以表达该窗口,G3 必须写成区间(max(启动侧已见 tick) 之下、launch_start + 15 s 之上)。该结论直接来自实测,非设计偏好。
3.3 结果形态(r3 应然)
ios.process.startup.initial_frame_rendering_ms出现(PHASE_METRIC_IDS,launch_trace.py:135);UIKit Scene Creationspan 闭合 ⇒ 准入后unmatchedBeginCount由 1 归 0,spanCount11 → 12;requiredMissingPhases = []、phaseCoverageComplete = true;totalSource = "coreprofile-endpoint-stop-drain"、totalRepairReason = "frame-end-stop-drain"、totalQuality = "repaired";status仍为partial/reason = "lifecycle-spans-incomplete":本方案不置self._completed = True(见 §3.4);_ignored_terminal_events与冻结版完全一致(r3 仍为 0 条)。
3.4 为什么不置 _completed
_completed 参与两个语义:total_endpoint_complete(launch_trace.py:1435)⇒ status 可升为 candidate;以及 completionObserved。若 drain 端点被允许升为 candidate,则活线完成信号(launch_trace.py:1224-1228:要求 totalDurationMs is not None 且 totalQuality == "candidate")将据此关闭启动面板——把一个 drain 挣得的端点当成干净端点。因此:
- 不置
_completed⇒status留在partial,totalQuality = "repaired"⇒ 活线完成信号继续等待(与现状一致); - 这是刻意保留的诚实性:修复带来的是最终离线报告与阶段覆盖的正确,不是实时完成信号的改变。
3.5 活线仍不会完成的已知后果(必须显式记录,不得隐藏)
launch_trace.py:1211-1212 的活线完成发射带 not is_frame_continuation 前置条件。drain 是本次采集的最后阶段且其 chunk 全为 frame-continuation,故 drain 之后不会再有非 drain chunk。⇒ r3 这类会话在活线 UI 上仍不会自动完成启动面板;本方案只修最终报告。该限制在 §11 风险清单 R6 原样登记,作为后续独立批次的候选议题(不并入本批)。
4. 代码变更边界(文件 / 函数 / 常量级)
以下为待批准的实施边界,本批一行代码都未改。
4.1 MacApp/Sidecar/perfowl_collector_core/launch_trace.py(唯一必改文件)
| 编号 | 位置 | 变更性质 | 内容 |
|---|---|---|---|
| E1 | 模块常量区(launch_trace.py:126 附近) | 新增 | STOP_DRAIN_LIFECYCLE_COMPLETION_ENABLED = True;STOP_DRAIN_COMPLETION_MAX_SECONDS = 15.0(对齐采集侧 STARTUP_CORE_PROFILE_MAX_WAIT_SECONDS);STOP_DRAIN_CHUNK_ROLE = "frame-continuation";STOP_DRAIN_CHUNK_PHASE = "post-stop-drain";STOP_DRAIN_RECORD_CAPACITY = 512;STOP_DRAIN_TOTAL_SOURCE = "coreprofile-endpoint-stop-drain";STOP_DRAIN_REPAIR_REASON = "frame-end-stop-drain" |
| E2 | __init__ 与 _reset_semantics()(:1618-1645) | 新增状态 | _stop_drain_records: list(有界,容量 STOP_DRAIN_RECORD_CAPACITY)、_stop_drain_truncated: bool、_stop_drain_admitted_count、_stop_drain_rejected_count、_stop_drain_reject_reasons: dict[str,int]、_initial_frame_end_from_stop_drain: bool = False、_stop_drain_completion: dict | None = None |
| E3 | consume() 保留判定(:1146-1151) | 改判定,不改语义 | 在原 retain_record 之后追加第二级候选分支:lifecycle_namespace and is_stop_drain and STOP_DRAIN_LIFECYCLE_COMPLETION_ENABLED and not self._emitted ⇒ 用 _record() 取出并追加进 _stop_drain_records(不是 _records),并打 record["fromFrameContinuation"] = True、record["stopDrainPhase"] = payload.get("streamPhase")。不改 saw_lifecycle_record 的赋值(:1146-1147 保持只认非 drain)。超容量时置 _stop_drain_truncated = True 并停止接受(不静默丢弃) |
| E4 | _rebuild_semantics()(:1647-1673) | 新增调用 | 在 self._pair_markers() 之后调用 self._complete_stop_drain_lifecycle()(仅当 _stop_drain_records 非空才进入,否则直接返回 ⇒ 零增量) |
| E5 | 新方法 _complete_stop_drain_lifecycle()(放在 _pair_markers 之后) | 新增 | 实现 G2 / G3 / G4:逐条过滤 → 展开 _lifecycle_actions() → 判链 → 仅对通过者:闭合 self._open 中对应 span、追加 self._spans、必要时置 _initial_frame_end_ns/_ticks 与 self._initial_frame_end_from_stop_drain = True。不写入 _markers、不写入 _ignored_terminal_events、不置 _completed。同时填充 self._stop_drain_completion 台账(含逐门通过/拒绝计数与拒绝原因) |
| E6 | report() 干净端点块(:1336-1350) | 来源可分辨 | 把 total_repair_reason 的初始化上提;在该块内:total_source = STOP_DRAIN_TOTAL_SOURCE if self._initial_frame_end_from_stop_drain else "coreprofile-endpoint";total_repair_reason = STOP_DRAIN_REPAIR_REASON if self._initial_frame_end_from_stop_drain else None |
| E7 | _recover_total_endpoint()(:1882-1941) | 仅加来源标签 | 在 if self._initial_frame_end_ticks is not None: 分支内追加:if self._initial_frame_end_from_stop_drain: end_repair = "stop-drain-completion"。start_ticks 逻辑、anchor = self._lifecycle_anchor_ticks()、candidates = sorted(...)、end_repair = "repaired"/"repaired-ambiguous" 全部逐字节不变 |
| E8 | report() 返回字典(:1577 附近) | 新增报告块 | 追加 "stopDrainCompletion": self._stop_drain_completion(或 None)。不改 totalQuality 的既有推导式(total_repair_reason not in (None, "endpoint") 会自动把它算成 "repaired") |
明确不动(逐字节):_lifecycle_anchor_ticks()、_recover_total_endpoint() 的锚点与候选逻辑、_pair_markers() 的 _ignored_terminal_events 分支(:1822-1833)、_process_lifecycle() 的 marker 准入门(:1704-1739)、_lifecycle_actions()(:2345-2378)、consume() 的 chunk 连续性/_minimal_live_scan/target scoping 门(:1072-1182)。
4.2 MacApp/Sidecar/perfowl_collector_core/realtime.py
本批不改。 role 判定(:4049-4054)、startup_semantic_complete.set()(:4813)、startup_rotation_complete.set()(:4386)、drain 上界(:4857-4874)、完成等待(:4776-4801)全部保持冻结。§1 的时序结论是只读核对得到的。
4.3 任务 B 的候选变更边界(本批只登记,需 Lead 另行批准)
| 编号 | 位置 | 内容 | 为何需要单独批准 |
|---|---|---|---|
| B1 | realtime.py:1069-1097 + online_report._consume_metric_frame() | 把 DisplayPerformanceProjector 的五项产出同时以 metric-ID 形状({"metricID","value","quality"})交到报告链路,使 coverage[].sampleCount 可计数 | 触及「唯一公式所有者」契约;须先裁定 DisplayPerformanceProjector 为这四项的唯一所有者 |
| B2 | realtime.py:1073 的 else 分支 / capture_plan.MetricCoverage | 当 is_active == False 时,为这五项写入有界且具名的 reason(如 display-projector-inactive),取代 reason = null | 与 Device Network 批次的 missing 语义 重叠 ⇒ 本批只做到「显示指标五项」范围并显式划界,全局词表留给 Network 批次 |
| B3 | online_report 顶层 | 在 dataCompleteness = "complete" 之外并置一个可机械检验的计数(如 derivedMetricCoverage = {"requested":5,"withSamples":1}),不静默重定义 dataCompleteness 的语义 | 顶层完整性语义属全局契约,改动面超出本批 |
| B4 | online_report._apply_frame_coverage() 的 metric_map(:1342-1358) | 明确不做:不为上述四条新增 metric_map 行 | 会造出第二个口径所有者(ios.frame.time.p95 的 10 s 窗、refresh_rate 的硬件 VSync 都无法从 frameSummary 取到),违反 realtime.py:1013-1015 既有原则;登记为被否决选项 |
5. 单元 / Fixture 测试设计
全部为新增测试,不改既有断言。命名建议 tests/test_build75_stop_drain_lifecycle_completion.py(若后续批次号变更,文件名随批次)。
5.1 正向(r3 形)
| 编号 | 用例 | 断言 |
|---|---|---|
| A-P-01 | 合成 chunk 序列:启动侧 chunk(含 open UIKit Scene Creation BEGIN)+ drain chunk(含 0x2B870030 与 0x31CA0006,同 tid = target) | initial_frame_rendering_ms 出现;spanCount +1;unmatchedBeginCount 归 0;stopDrainCompletion.admittedRecordCount == 3 |
| A-P-02 | 同上 | totalSource == "coreprofile-endpoint-stop-drain" 且 totalRepairReason == "frame-end-stop-drain" 且 totalQuality == "repaired" |
| A-P-03 | 同上 | status == "partial"、reason == "lifecycle-spans-incomplete"、_completed is False、completionObserved is False |
| A-P-04 | 同上 | ignoredTerminalBeforeBeginCount == 0(与冻结版一致) |
| A-P-05 | fixture 目录 fixtures/ 增加一份匿名化的 r3 形态 fixture(只保留必要 kd 记录,不含任何设备标识) | 端到端 report() 全字段比对基线快照 |
5.2 负向(必须继续 missing)
| 编号 | 用例 | 断言 |
|---|---|---|
| A-N-01 | drain chunk 内生命周期记录但 tid 不属于 target(复刻 S2 的 9 条形) | admittedRecordCount == 0;totalDurationMs is None;requiredMissingPhases 含 Initial Frame Rendering;报告中不出现 stop-drain 任何 token |
| A-N-02 | frame-continuation chunk 全为 bplist(复刻 r1/r2 形) | stopDrainCompletion is None 或 admittedRecordCount == 0;不新增 span |
| A-N-03 | S2 真实会话离线重放 | totalDurationMs is None、missing == ['Initial Frame Rendering']、ignoredTerminalBeforeBeginCount == 1;与冻结解码器逐字段一致(除本批新增块) |
| A-N-04 | r1 真实会话离线重放 | missing == ['System Interface Initialization','Initial Frame Rendering'];准入 0 |
| A-N-05 | r2 真实会话离线重放 | missing == ['Initial Frame Rendering'];准入 0 |
5.3 对抗性(提级与扩权探测)
| 编号 | 注入 | 必须仍为 |
|---|---|---|
| A-X-01 | 把 drain 记录的 target_generation 改成另一代 | 拒绝;rejectReasons["generation-mismatch"] >= 1 |
| A-X-02 | 只注入 0x31CA0006(无 code=12 opener) | 拒绝(G4 链门);totalDurationMs is None;不得进 _ignored_terminal_events |
| A-X-03 | 注入 tick <= max(启动侧已见 tick) 的 drain 记录 | 拒绝(G3 前向门);rejectReasons["device-window"] >= 1 |
| A-X-04 | 注入只有 BEGIN、无 END 的 drain 记录 | 拒绝(链不闭合)⇒ 净新增 open span 必须为 0 |
| A-X-05 | 关闭 STOP_DRAIN_LIFECYCLE_COMPLETION_ENABLED 后重放四轮 | 与冻结解码器逐字段一致(含 _ignored_terminal_events、anchor、Mode A 结果) |
| A-X-06 | 四轮重放,比较 _ignored_terminal_events 元组集合与 _lifecycle_anchor_ticks() | 逐轮逐值相等(证明「未放宽锚点」) |
| A-X-07 | 注入超过 STOP_DRAIN_RECORD_CAPACITY 的 drain 记录 | stopDrainCompletion.truncated is True,且不静默部分准入 |
5.4 效率门禁(按 AGENTS.md §工作流程)
- 关键路径:
consume()内每条 drain-chunk 记录多一次「namespace +_lifecycle_actions()」判定;_complete_stop_drain_lifecycle()只在_stop_drain_records非空时运行。 - 实测规模:r3 的
fc+actionable(任意线程)= 31、目标线程命中 3 ⇒ 缓冲上界 31 条(容量 512 有 16× 余量);S2/r1/r2 均为 0 ⇒ 零增量。 - 复杂度:
O(R_drain),R_drain有界;无新增全量扫描、无新增数据复制、无新增进程。 - 需补 benchmark:同一不可变 Session 上开关本特性各跑 N 轮,报
total (ms)、ru_maxrss增量、decodedRecordCount不变性(三项均须「本批自有增量」口径,未归因数字原样出表)。
6. 标签契约与「反事实」防误用
6.1 必须保持的不变式
totalSource == "coreprofile-endpoint"且totalRepairReason == "endpoint"⟺ 真实配对(非恢复路径)
本方案通过引入两个新 token 保持该不变式:totalSource = "coreprofile-endpoint-stop-drain"、totalRepairReason = "frame-end-stop-drain"。三处既有消费点因此自动正确:
totalQuality(launch_trace.py:1568-1572)⇒"repaired"(不是"candidate");- 活线完成信号(
:1224-1228)⇒ 要求"candidate"⇒ 不触发; _apply_frame_coverage()的items/total_item分支(online_report.py:1063-1068)⇒ 该端点不进入阶段级覆盖的candidate通路。
6.2 被否决的反事实
R3 记录的单变量对照曾把 drain chunk 的 role 直接改标,得到 totalDurationMs = 2300.676291、totalSource = "coreprofile-endpoint"、missing = []。
该组合被明确否决,理由有三,且均为结构性而非口味问题:
- 它把 drain 挣得的端点伪装成干净端点(
totalSource/totalRepairReason落在真实配对标签对)⇒ 违反 §6.1 不变式; - 它靠无条件改标 chunk 达成,正是本次被明令禁止的手法;本方案的 3/0/0/0 增量与链闭合门才是「有界」的证据;
- 该数值在本方案中不得作为真值引用。它只在 §1 / 本节以「被否决反事实」出现;实施后报告中它会以
totalSource="coreprofile-endpoint-stop-drain"+totalQuality="repaired"出现,与干净端点结构上不可混淆。
7. 任务 B 的时间域 / 覆盖度证据方案
7.1 现象基线(R3 已审计,逐字保留)
| 量 | 值 | 时间域 |
|---|---|---|
Session 墙钟跨度(durationMonotonicNs) | 137.304 s(R3 报 137.809 s,含 startedAt→endedAt 口径差) | 主机墙钟 |
主机接收轴跨度(1716 条 hostReceiveMonotonicNs) | 134.487 s | 主机单调 |
设备 presentation 轴跨度(1716 条 presentationTimestampNs) | 30.721 s | 设备呈现 |
| 比值 | 0.2284(≈1 : 4.38),200 事件分桶 [0.2608 … 0.2352] 无阶跃无漂移 | — |
| 设备序列完整性 | 2 → 1717,distinct 1716,缺口 0、乱序 0 | 完整 |
| 设备轴帧间隔 | p50 16.642 ms、mean 17.913、87.9% 恰 16.6 ms | 量化到刷新倍数 |
| 主机轴帧间隔 | p50 70.311 ms、mean 78.418、max 512.697 | — |
产品自报 frameSummary.durationMs | 30 721.029 ms | 设备轴 |
产品自报 displayAcquisitionCensus.observedSpanMs | 134 912.872 ms | 主机轴 |
产品自报 sourceGapCount | 0 | — |
| 产品内部比值 | 4.39×,未标注 | — |
7.2 已被证据排除的两项、与仍需证明的项
| 候选解释 | 关键反证 | 裁定 |
|---|---|---|
HOST_TRANSPORT_THROTTLED(主机内部管线积压) | displaySourceDeliveredCount = displaySourceConsumedCount = displayIngressAcceptedCount = 1716;displaySourceToIngressP50/P95/Max = 0.1616 / 0.2605 / **0.702 ms** ⇒ 主机内部传输总代价 < 1 ms | 观察上不成立 |
TWO_SURFACES(双 surface 交错错配) | 全局 presentationTimestampNs 严格递增且唯一;两 surface 各约 859 / 857 帧,交错不可能产生恒定 4.38 倍差 | 观察上不成立 |
DEVICE_TIMELINE_QUANTIZED | 设备把间隔量化成 1×/2×/4×/5×/6× 刷新(分辨率 16.64 ms) | 待证明(最简) |
DEVICE_PRESENTATION_CLOCK_RESCALED | 比值恒定而非随机,符合系统性缩放特征 | 待证明(离线不可证伪) |
REAL_12FPS | 与设备自报 55.91 / median 60.09 fps 冲突 | 待证明 |
本批只主张:两轴恒定相差 ≈4.4 倍,且产品未标注。不主张任何单一成因。
7.3 证据方案(离线,全部只读、不需要真机)
E-B1 · 用产品自身的投影器把两轴并列出来
DisplayPerformanceProjector.consume(event, host_receive_monotonic_ns=…) + snapshot(now_ns) 是纯函数式接口,可直接离线重放 T2 的 1716 条 display.frame-lifetime.raw(含其 hostReceiveMonotonicNs),并对每次发布导出:
hostReceiveMonotonicNs / presentationTimestampNs /
framesPerSecond / fpsQuality / frameTimeMs / frameTimeP95Ms /
refreshRateHz / framePacingJitterMs / host_gap_ns(投影器内部)判别力:FRESHNESS_STALE_HOST_GAP_NS = 1.0 s(display_performance.py:2036)已经把「无明显证据」与「源真的空闲」分开。若 refreshRateHz ≈ 60 稳定、且 stale 样本占比与 4.38 倍差相关(而设备轴 87.9% 恰 1× 刷新),则 DEVICE_TIMELINE_QUANTIZED 与 HOST_TRANSPORT_THROTTLED 可被分离:量化解释预测「设备轴丢长间隔」,陈旧解释预测「主机轴有 >1 s 静默段且与 stale 标记重合」。这是本方案最高优先级的可执行项,因为它同时验证 §8 的报告映射修复是否真的能产出这四项。
E-B2 · 覆盖度账本(两轴并列,作为报告新增块)
runtimeMs / sampleCount /
firstSampleHostNs / lastSampleHostNs / observedSpanMs(host) /
firstPresentationNs / lastPresentationNs / presentationSpanMs(device) /
spanRatio / gapCountOver1s / maxHostGapMs / staleSampleCount /
refreshRateHz / p95WindowsObserved / p95WindowsRequired要求:observedSpanMs 必须与 displayAcquisitionCensus 自身口径可比,presentationSpanMs 必须与 frameSummary.durationMs 可比,并把 spanRatio 与 staleSampleCount 作为一等字段呈现——即「产品既没把 134.9 s 的观测跨度呈现出来」这一缺陷在报告层被关闭。
E-B3 · 交叉一致性门(可机械检验的自证)
| 门 | 断言 |
|---|---|
| 设备轴自证 | 由原始 1716 条 presentationTimestampNs 重算的跨度 = frameSummary.durationMs(30 721.029 ms),误差 ≤ 1 ms |
| 主机轴自证 | lastCallbackMonotonicNs − firstCallbackMonotonicNs = displayAcquisitionCensus.observedSpanMs(134 912.872 ms),精确相等 |
| 事件守恒 | eventCount = presentedFrameCount = 1716;intervalCount = 1686;frameLifetimeMatchedCount = 1716;frameLifetimeRejectedCount = 19 912;totalMessageCount = 21 628 |
| 量化自证 | refreshMultiple 取值集合 ⊆ {≈1, ≈2, ≈4, ≈5, ≈6}×,与 p50 16.642 ms 一致 |
E-B4 · 明确不可得的部分(守边界)
无设备侧独立墙钟桥时,设备↔主机的绝对偏斜不可证明(与项目自身 C0「离线结构上无法产出同事件双流配对 witness」的结论一致)。因此:
- 本方案不声称已证明成因;
- 若后续需要定性,唯一可行路径是 E4 期间增补 host 侧可观测桥(例如分段操作时由操作者在宿主侧留下带时刻的分段标记,使每段的
hostReceiveMonotonicNs区间可与人工时刻对齐),仍不构成设备轴→主机轴的绝对换算; - 该缺口原样登记,不做「零候选」式断言。
8. 四段滚动 / 静止同机测试与时间标记方案
8.1 配置(必须与 T2 逐项一致,否则标 INVALID)
| 项 | 期望值 |
|---|---|
| 载体 | 冻结 Build74 Perfowl App UI(MacApp/dist/ 冻结包,SHA-256 3a557cc0…) |
requestedMetricIDs | ["ios.display.refresh_rate","ios.foreground.fps","ios.frame.pacing_jitter","ios.frame.time","ios.frame.time.p95"] |
launchMode | controlled-cold-launch |
startupEnabled / screenshotsEnabled | false / false |
requestedFrameFilter / effectiveFrameFilter | 0x31800318 / ["0x31800318"] |
capturePlanHash | 必须等于 T2 的 bff494dc…(记录后立刻核对,不一致 ⇒ INVALID) |
8.2 分段与时间标记(真实打点、不代填)
| 段 | 计划时长 | 动作 |
|---|---|---|
| S1 | 0–30 s | 连续滚动 |
| S2 | 30–60 s | 静止(手指离开屏幕,App 保持前台) |
| S3 | 60–90 s | 连续滚动 |
| S4 | 90–120 s | 静止 |
| 尾 | +20 s | 保持前台静止,保证 Stop 前有静止样点 |
打点机制(三级,互为独立来源,任一缺失即记为缺口、不补造):
- 宿主侧 5 s 心跳账本(主,零产品改动):录制期间在另一终端运行
while :; do date -u +%FT%T.%3NZ; sleep 5; done > phase-marks.txt, 记录 Session 的startedAt/endedAt与durationMonotonicNs,用两端锚点做一次线性拟合把墙钟心跳映射到单调域。140 s 量级下分辨率 ≤ 1 s,足以切段。 - 操作者手写四段起止(辅):操作者自报「滚动/静止」切换时刻(只记整秒),与 (1) 交叉核对;两者不一致时记录分歧,不取平均、不改数据。
- 信号自证(独立于人工):静止段的
refreshMultiple应几乎恒为 1×(设备轴 p50 ≈ 16.64 ms),滚动段应出现 2×–6× 倍数与更宽的refreshMultiple取值集合。用refreshMultiple的分布跳变独立推断段边界,再与 (1)(2) 对照。三条独立来源一致才认定为有效分段。
8.3 每轮交付物
- Session 目录 +
SHA256SUMS(含core-profile-chunks.bin、manifest.json、report-final.json、display-acquisition-census.json); - 段边界账本(心跳原文 + 手写原文 + 由
refreshMultiple推断的边界,三者并列,含分歧记录); - 双轴报告(按 §7.3 E-B1/E-B2 产出);
- 每轮的
capturePlanHash/effectiveFrameFilter核对结论。
9. 真机验收矩阵
9.1 任务 A
| # | 载体 / 输入 | 期望(判据) |
|---|---|---|
| A-1 | 冻结 App UI,Startup-only 冷启动(与 S2/r1/r2/r3 同配置) | ios.process.startup.initial_frame_rendering_ms 出现;requiredMissingPhases = [];phaseCoverageComplete = true;totalSource = "coreprofile-endpoint-stop-drain";totalRepairReason = "frame-end-stop-drain";totalQuality = "repaired";status = "partial";ignoredTerminalBeforeBeginCount = 0;stopDrainCompletion.admittedRecordCount = 3 |
| A-2 | 离线重放 S2 | totalDurationMs = null;missing = ["Initial Frame Rendering"];准入 0;报告内不出现任何 stop-drain token |
| A-3 | 离线重放 r1 | missing = ["System Interface Initialization","Initial Frame Rendering"];准入 0 |
| A-4 | 离线重放 r2 | missing = ["Initial Frame Rendering"];准入 0 |
| A-5 | 关闭 STOP_DRAIN_LIFECYCLE_COMPLETION_ENABLED 重放四轮 | 与冻结解码器逐字段一致(含 anchor 与 _ignored_terminal_events) |
| A-6 | 对抗注入 A-X-01…A-X-07 | 逐条按 §5.3 拒绝,不得提级 |
| A-7 | 效率对比 | §5.4 三项预算全过,decodedRecordCount 不变 |
9.2 任务 B
| # | 输入 | 期望 |
|---|---|---|
| B-1 | 离线重放 T2 的 1716 条 frame-lifetime(E-B1) | 五项指标可产出;stale 计数与 4.38 倍差的相关性可读;host_gap_ns 分布随发布可导出 |
| B-2 | E-B2 覆盖度账本 | observedSpanMs ≈ 134.9 s、presentationSpanMs ≈ 30.7 s、spanRatio ≈ 4.38 三值均出现在报告中 |
| B-3 | E-B3 交叉一致性门 | 四条断言全过(误差 ≤ 1 ms 或精确相等) |
| B-4 | 修复前后对比 | 五项 coverage[] 的 sampleCount 不全为 0;reason 不再为 null;dataCompleteness 与样本事实不再矛盾 |
| B-5 | §8 四段同机测试 | 四段边界三源一致(或分歧被显式记录);滚动/静止双轴账本可对比 |
所有判据的最终裁定归 Lead AI;本方案不输出 PASS/FAIL。
10. 风险清单
| # | 风险 | 影响 | 缓解 / 检出 |
|---|---|---|---|
| R1 | drain 端点被静默提升为「干净端点」 | 下游把 2.3 s 级 drain 端点当无异议真值 | 新 source/reason token(§6.1)+ totalQuality = "repaired" + 不置 _completed;活线完成信号继续等待 |
| R2 | 污染 _ignored_terminal_events ⇒ 锚点/候选集被放宽 | 直接违反明令 | 构造性规避:drain 记录不进 _markers、不进 _ignored_terminal_events;A-X-06 逐轮断言 anchor 与候选集逐值相等 |
| R3 | 后续应用帧的 0x31CA0006 被误认为启动端点 | 假 Total | G4 链门要求同一准入集合内存在同 PID/TID 的 code=12 opener(保留 :1822-1828 既有契约)+ G3 设备区间门 |
| R4 | G3 前向门可能误拒合法记录(drain 设备域回卷) | 漏修 r3 形 | 门写成区间而非单边;拒绝必计数并出表(rejectReasons),永不静默丢弃 |
| R5 | drain 缓冲无界 | 内存 | STOP_DRAIN_RECORD_CAPACITY = 512(实测需 31);超限置 truncated = true 并停止接受,A-X-07 钉死 |
| R6 | 活线 UI 仍不会自动完成(drain chunk 不发射完成快照,:1211-1212) | 用户体验与最终报告不一致 | 显式登记为本批不修项;作为后续独立批次候选,不并入本批 |
| R7 | 任务 B 的 reason 语义与 Device Network 批次重叠 | 双批冲突 | §4.3 B2 把范围限死在「显示指标五项」,全局 missing 词表显式让给 Network 批次 |
| R8 | 第二个口径所有者(metric engine / projector / frame report) | 口径分叉 | B4 明确拒绝扩 _apply_frame_coverage;须先裁定 projector 为唯一所有者,并加「单所有者」单测 |
| R9 | dataCompleteness 语义被静默重定义 | 全局契约漂移 | B3 只并置计数,不重定义;顶层语义变更需 Lead 单独批准 |
| R10 | 反事实 2300.676291 ms 被引用为真值 | 违反明令 | §6.2 结构化防误用;新 token 使二者不可混淆 |
| R11 | 站点侧效应(pre-push hook 在 site/+docs/ 变更时触发部署) | 非预期上线 | 已知并接受:本批确为文档批次;推送前复核 diff 路径集合 |
| R12 | 负向回归表述被误写成「r3 仍为 null」 | 自相矛盾 | 见 §11 诚实性声明:负向回归只能表述为「S2/r1/r2 继续 missing」,r3 的 null 不可能逐字保持 |
11. 诚实性声明(必须在验收时一并读)
- 负向回归不可能逐字保持:一旦修复生效,r3 的
totalDurationMs由null变为真实值。因此负向回归的唯一正确表述是:「S2 / r1 / r2 必须继续 missing,且任何轮次、任何情况下都不得出现*-repaired之外的虚假干净标签、不得放宽锚点」。把「r3 仍为null」写进验收判据,是把已修缺陷当回归基准。 - 本批零产品改动,故其自身证据等级为 E2(离线构建/测试级),不含任何真机 E4 结论。
- 不在本批范围内的三项未定项(§2.3)在本方案中保持未定,未以任何方式升级。
- 未自判:本方案不输出 PASS/FAIL,不宣称任何阶段门禁关闭或开启;
judgement = pending-lead-ai。 - 原始 Session、原始
.trace、他人未提交文档在本批中零读写副作用;全部离线分析在私有归档副本上进行,编号目录只增不删。
12. 明确不做的事 / 另开批次
| 项 | 处置 |
|---|---|
| 修改冻结产品代码 / 重打包 / 重新公证 | 不做(本批为方案阶段) |
采集侧 coreProfileStreamRole 标定 | 不做(不得无条件改标全部 chunk);仅在文档层登记「startup-only 模式下 drain 段被标为 frame-continuation 属语义不当」这一事实,供后续独立议题 |
放宽 _recover_total_endpoint 锚点 / Mode A 候选集 | 不做 |
Device Network __aenter__ / 逐源失败隔离 / 全局 missing 语义 | 另开产品批次(§4.3 已划界) |
| Phase C Hybrid Total 相关值 | 保持 FROZEN,不产出、不推断 |
| 活线完成信号(drain 段不发射完成快照) | 本批登记不修(R6) |
原始 Session / .trace 入库 | 不做 |
13. 复现与证据来源
只读分析脚本(私有归档,编号目录只增不删)
<PRIVATE_ARCHIVE>/build74-owner-e4-followup-20260924-1050/scripts/
analyze_drain_admission_delta.py → app-rounds/drain-admission-delta.json (§1 准入增量 3/0/0/0 + Mode A 复算)
analyze_startup_delivery_lag.py → app-rounds/startup-delivery-lag.json (§1 时序表 + 设备域区间 + §8.2 设备侧依据)
analyze_startup_rounds.py (既有引擎:cp / validate_recorded_session / 解码器构造,被上两个脚本复用)
probe_ifr_pairing.py / lifecycle_code_census.py / ifr_timeline_geometry.py /
control_ifr_retention.py / analyze_t2_frame.py / session_sha_inventory.pyR3 已审计交付物
artifacts/build74-owner-e4-followup-20260924-1232/
startup-endpoint-forensics.md (任务 A 端点取证)
t2-frame-dual-timeline.md (任务 B 双时间轴)
test-matrix.json / session-sha-inventory.json / evidence-hashes.json产品只读核对锚点(本方案全部时序与准入结论的源码依据,均已逐字节未改)
realtime.py : 91-94 / 154 / 158 / 160(常量)· 4049-4055(role 判定)· 4776-4801(完成等待 emit)
4813(startup_semantic_complete.set)· 4386(startup_rotation_complete.set)
4857-4874(rotation 请求 + drain 上界)
launch_trace.py : 126-135(PHASE_METRIC_IDS)· 947-1062(consume)· 1146-1151(retain_record)
1192-1194(saw_completion_marker)· 1336-1350(干净端点 + totalSource)
1435-1464(status / publish_total)· 1568-1578(totalQuality/Source/RepairReason)
1618-1673(_reset_semantics / _rebuild_semantics)· 1704-1739(_process_lifecycle)
1773-1866(_pair_markers)· 1868-1880(_lifecycle_anchor_ticks,不动)
1882-1941(_recover_total_endpoint,不动)· 2345-2378(_lifecycle_actions)
adapters.py : 1108-1135(streamPhase: post-stop-drain)
online_report.py : 1087-1155(_consume_metric_frame)· 1341-1370(_apply_frame_coverage)
capture_plan.py : 233-310(MetricCoverage)
display_performance.py : 2011-2036(DisplayPerformanceProjector 五项口径 + FRESHNESS_STALE_HOST_GAP_NS)
realtime.py : 814-823(投影器喂入)· 902-906(_latest 唯一写入点)· 1069-1097(快照消费)
metric_engine.py : 141 / 476(仅 ios.foreground.fps,源字段 CoreAnimationFramesPerSecond)14. 交给 Lead 的三个待决点
| 编号 | 待决问题 | 本方案倾向 | 影响 |
|---|---|---|---|
| D1 | drain 挣得的端点是否发布 totalDurationMs,还是只闭合阶段、Total 保持 null? | 发布,但只带 coreprofile-endpoint-stop-drain / frame-end-stop-drain / repaired 三标记(活线信号不受影响)。备选:STOP_DRAIN_PUBLISH_TOTAL = False,Total 保持 null、阶段照常闭合 | 决定 A-1 的判据形态 |
| D2 | G3 设备区间门的上界是否采用 launch_start + STARTUP_CORE_PROFILE_MAX_WAIT_SECONDS(15 s)? | 采用,并附「拒绝必计数出表」 | 决定对抗性用例 A-X-03 |
| D3 | 任务 B 的 B1/B2/B3 是否并入本批实施,还是拆为独立批次(与 Device Network missing 语义批次协调)? | 拆批:本批只落诊断与证据方案(§7),实施另立 | 决定本批的实施范围 |
judgement = pending-lead-ai
15. Lead Audit Amendment(2026-09-24)
角色 Execution AI(修订阶段) · 2026-09-24 · 基线
main = eeee607(父1ef33f7)· 判据judgement = pending-lead-ai·codeChanged = NO本段为追加增量:不回改 §0–§14 的任何数字。凡与上文冲突处,以本段为准,逐条对照见 §15.9。 本轮未改MacApp/产品代码、测试代码与历史报告;未运行新的 bm/bso/bsoc 实验;未解冻 Phase C;未提交 / 未推送 / 未部署 / 未发送交付 webhook。
15.0 修订摘要(六项返工 → 落点)
| 返工 | Lead 裁定 | 本段落点 | 对 §0–§14 的处置 |
|---|---|---|---|
| 1 | 3/0/0/0 是「role 排除后的预筛记录数」,不是四道门的最终准入数;r3 必须选定一条闭合链并记录另一 closer 的拒绝理由;补齐身份/代际/纪元/丢失边界 | §15.1 | §1 表格与结论句的「准入增量」称谓作废(数值保留) |
| 2 | 画清候选缓存 / _semantic_dirty / _reset_semantics() / report() 生命周期;核对 512 容量在三模式下是否成立;给出可执行测试 | §15.2 | §5.4「512 有 16× 余量」降级为待验证假设 |
| 3 | D1:本阶段只产 stop-drain 端点诊断,不发布 totalDurationMs,不提升阶段覆盖或 UI 为干净候选值 | §15.3 | §3.3 的发布形态被取代;§4.1 E6/E7 取消 |
| 4 | D2:删除 launch_start+15 s 与「超过启动侧最大 tick」两道硬准入门;设备侧边界缺失时 fail closed | §15.4 | §3.2 的 G3 整行与末段删除 |
| 5 | D3:任务 B 拆为 B0/B1/B2/B3;更正 metric_engine.py 事实;不把 dataCompleteness=complete 读作「五项均有样本」 | §15.5 | §4.3 的 B1–B4 编号重排;§1/§13 的事实表述更正 |
| 6 | 测试矩阵必须覆盖七类边界;新真机轮次按原始字节形态条件判定 | §15.6 | 增补于 §5/§9 之外,不改其既有编号 |
15.1 返工 1:3/0/0/0 的正确定性、r3 闭合链的唯一选择与证据边界
15.1.1 「预筛」与「准入」是两个不同的计数
- 预筛(role 排除后的候选清点):S2
0/ r10/ r20/ r33。它只施加了两个条件——记录位于coreProfileStreamRole == "frame-continuation"且streamPhase == "post-stop-drain"的 binary chunk 内、且落在生命周期命名空间(launch_trace.py:1119-1123的(0x1F,0x07)/(0x2B,{0x87,0xDC})/(0x31,0xCA))。它不含身份门、链闭合门的任何判决。§1「任务 A 的定量边界」把该数写成「有界准入增量」「准入增量是 3/0/0/0」是术语错误,本段更正为「预筛记录数」。 - 准入(通过全部门、真正进入报告的记录数):本轮未测量,因为链闭合规则此前未定义(见 15.1.2)。
- r3 的预期准入不是 3,而是 2:3 条预筛记录中,1 条 opener + 2 条 closer 只能构成一条闭合链(opener 与较早的那条 closer),较晚的那条 closer 必须被显式拒绝并计数。§5.1 A-P-01 的
admittedRecordCount == 3与 §9.1 A-1 的同项据此作废,改为admittedRecordCount == 2∧rejectedRecordCount == 1∧rejectReasons["terminal-already-closed"] == 1。 - 另需钉死:S2 的「任意线程 9 条」与 r3 的「任意线程 31 条」都是预筛量,不是准入量。
15.1.2 闭合链的唯一选择规则(可机械检验,复用冻结契约)
冻结解码器已经定义了胜出规则,不需要新造:_pair_markers 按 (timestampNs, arrivalSequence, actionOrder) 排序(:1777-1784),并在首次为 (tid, PHASES[8]) 配出 span 且 debug_id == INITIAL_FRAME_END_DEBUG_ID 时置 _initial_frame_end_ns/_ticks、_completed = True,随后**break**(:1862-1866)。⇒ 冻结契约 = 最早 mach tick 的 terminal 胜出;其后同 (tid, phase) 的 terminal 不再被消费。
新增的 drain 链闭合必须逐条复刻该语义:
- 记录按
(machTicks, arrivalSequence)排序(与_rebuild_semantics两遍同序); - 展开
_lifecycle_actions()(:2345-2378)得 marker 流,再按(timestampNs, arrivalSequence, actionOrder)排序;该 marker 流不得写入self._markers(§3.1 第 3 条); - 第一条能闭合「保留记录已开启的仍 open span」的 terminal 胜出;此后同一
(tid, phase)的 terminal 一律记terminal-already-closed拒绝——不计 span、不计unmatched_end_count。
r3 应用结果(app-rounds/drain-admission-delta.json,本轮只读复算):
| 记录 | 设备域偏移(相对 launch start) | machTicks 序 | actions | 判决 | 理由 |
|---|---|---|---|---|---|
0x2B870030(code=12) | +2 246.633 ms | 最早 | END UIKit Scene Creation + BEGIN Initial Frame Rendering | 准入 | 起点 = §2.2 已确认仍 open 的 UIKit Scene Creation(anchor 16 767 956 263 341 < 该记录 tick) |
0x31CA0006(#1) | +2 300.676 ms | 中 | END Initial Frame Rendering | 准入(闭合链终点) | 首个到达的 IFR terminal |
0x31CA0006(#2) | +2 313.027 ms | 最晚 | END Initial Frame Rendering | 拒绝 | terminal-already-closed |
- 第二条 closer 的拒绝理由(逐字):
terminal-already-closed—— 该(目标 tid, 8)的 span 已由较早的 terminal 闭合,冻结配对契约在此处break,故本条不作为端点、不产生第二个 span、不改动_initial_frame_end_*。 - Δ 的精确值:
machTicks差 = 296 418 ⇒296 418 × 125 // 3 = 12 350 750 ns⇒ Δ = 12.35075 ms。(§1 表中的+2 300.676 / +2 313.027为 3 位小数展示值,本轮不再回改。) - 可机械检验性:胜出者 =
argmin(machTicks),与到达顺序、chunk 序号无关(两条 closer 同属 chunk #50)⇒ 对「到达顺序与 tick 顺序相反」的注入用例(§15.6 T-D-01)必须给出同一结果。 - 负向可机械检验性:S2 / r1 / r2 的预筛集为空或被身份门清零 ⇒ 无链可闭合,
admittedRecordCount == 0;任何规则变更都不得使其产出0x31CA0006端点(T-N-01…03)。
15.1.3 为什么不能沿用 _recover_total_endpoint 的 Mode A 标签表达本次选择
本轮只读复算(app-rounds/drain-admission-delta.json):
| 轮次 | _ignored_terminal_events | candidates(anchor 之后) | Mode A 结果 |
|---|---|---|---|
| S2 | 1 | 0(候选 tick 16 389 392 355 267 < anchor 16 389 393 206 930) | None |
| r1 | 0 | 0 | None |
| r2 | 0 | 0 | None |
| r3 | 0 | 0 | None |
⇒ r3 修复后 _ignored_terminal_events 仍为 0(drain 记录按 §3.1 永不进入该列表)。因此把 r3 端点写成 totalRepairReason ∈ {"frame-end-repaired", "frame-end-repaired-ambiguous"} 是错的:这两个标签只由 Mode A 的 candidates 路径产生(:1932-1940),而该路径在 r3 上无候选。这条约束同时收窄了 §4.1 E7 的作用域,与 §15.3 合并改写。
15.1.4 身份 / 代际 / 纪元 / 重连 / 设备丢失的证据边界(逐项实测)
读数取自四轮 Sessions/<sid>/collector-core/startup-report.json 与 launch-trace-report.json(两文件在这些字段上逐字段相同),全部只读:
| 边界 | 字段来源 | S2 | r1 | r2 | r3 | 本方案的准入后果 |
|---|---|---|---|---|---|---|
| PID | target_identity_audit().bindingSource;_process_lifecycle 首行门(:1708-1709) | 有目标 | 有目标 | 有目标 | 有目标 | _target_pid is None ⇒ 全部 lifecycle 记录直接 return ⇒ fail closed(整批不可用) |
| 线程映射 | _thread_pids(:1675-1702 两遍吸收 0x07000004 / 0x07000008 / PERF_THREAD_DATA_DEBUG_ID) | — | — | — | r3 三条全命中目标 tid | 最小扫描下映射记录每线程只留第一条(:1173-1182);全量扫描下吸收全设备映射。两者对身份门结论无差(门要求等于目标 PID) |
| target generation | targetGeneration / ignoredGenerationEventCount | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 0 | 四轮该门从未触发。且 consume() 的 generation 门在 normalized_generation is None 时根本不检查(:959-966)⇒ 该门是缺值 fail-open,不能单独充当身份凭证;身份必须由 PID/线程映射承担 |
| source epoch | report-final.json.sourceEpoch、coverage[].sourceEpoch、rawManifest.sourceEpochCount | — | — | — | T2:sourceEpoch = 1(5 项 coverage 均为 1),sourceIdentityCount = 2,sourceIdentityDigest = 5c725d7d… | 诊断块必须带 sourceEpoch;≠1 或跨 epoch ⇒ 拒绝发布(source-epoch-unknown) |
| reconnect generation | rawManifest.coreProfileConfigGeneration / coreProfileRestartCount / handoffRestartCompleteNs | — | — | — | 四轮 coreProfileRestartCount = 0、handoffRestartCompleteNs = null | 无重连。若将来 restartCount > 0 ⇒ drain 窗口跨代 ⇒ 不发布(reconnect-across-drain) |
| 设备丢失证据 | sourceQuality.deviceLostEventMarkerCount、startupObservability.deviceLostMarkerCount / lostBefore/During/AfterStartupCount / lostByCPU | 1(0/1/0,cpu0) | 4(0/4/0,cpu0) | 3(0/3/0,cpu0) | 10(0/10/0,cpu0) | 见下三条约束 |
设备丢失边界的三条诚实性约束(本轮新增,必须随报告一并读)
deviceLostEventMarkerCount是丢失事件(marker)条数,不是丢失记录数——源码注释(:1607-1611)与方法 docstring(:1952-1955)均如此声明,且deviceLostArgSemanticsVerified = False(:1999)。sourceQuality.lossFree = true与deviceLostEventMarkerCount > 0并存不是矛盾:lossFree的判据是live_queue_drop_count == 0(:1605-1606与:2135-2136),只覆盖宿主侧活线队列,不含设备侧环形丢失。四轮全部lossFree = true且prelaunchRingOverwriteCount = liveQueueDropCount = 0。⇒ 不得把lossFree = true读作「无设备丢失」,也不得把lossFree = false当成唯一失效条件。该字段名与语义的错位只登记,本批不改名。lostDuringStartupCount在 r1 上是「窗口未定义」的兜底值,不是「落在启动窗内」的证据:_build_startup_observability的三分支(:1974-1979)在launch_start_ns is None且frame_end_ns is None时落入else。r1 的launchStartTimestampNs = None(本轮复核,与 §15.4 的 fail-closed 依据同源)⇒ r1 的during = 4不携带时间窗信息。S2 / r2 / r3 的launchStartTimestampNs有值,其during才具备「≥ launch start」语义;但四轮initialFrameEndTimestampNs均为None⇒ 上界一律未知,during实为「≥ launch start 之后的全部」。 ⇒ 本方案不把设备丢失计数用作准入的替代证据;它只作为stopDrainEndpointDiagnostics.deviceLost的伴随字段原样出表。
15.2 返工 2:候选缓存 / _semantic_dirty / _reset_semantics() / report() 的状态生命周期
15.2.1 冻结版的真实状态机(逐行核对 launch_trace.py)
__init__ :480 _records = [] :507 _semantic_dirty = False :508 _reset_semantics()
consume() 尾部 · 每条保留记录 :1190 _records.append(record) :1191 _semantic_dirty = True
consume() 尾部 · 活线快照发射 :1223 candidate = self.report() ← report() → _ensure_semantics()
:1248 self._emitted = True
:1254 self._records.clear() ← 释放活线启动期状态
:1255 self._reset_semantics() ← 清 _markers/_spans/_open/_ignored_terminal_events/…
:1256 self._semantic_dirty = False
_ensure_semantics() :1671 if self._semantic_dirty: self._rebuild_semantics()
_rebuild_semantics() :1648 _reset_semantics() :1664 pass1 _absorb_attribution
:1666 pass2 _process_lifecycle :1668 _pair_markers :1669 _semantic_dirty = False
bind_target() :604-608 _emitted/_emitted_report/_live_revision/_last_live_signature 复位 + _semantic_dirty = True
:609 _ensure_semantics()
report() :1298-1299 if self._emitted_report is not None and source_raw_sha256 is None: return 缓存
:1300 self._ensure_semantics()四条必须由测试钉死的不变式
- I-1(脏标记单调性):
_records的追加只在:1190-1191成对发生 ⇒ 「有记录且不脏」只允许出现在:1669/:1256之后。 - I-2(重置完备性):
_reset_semantics()(:1618-1645)逐项重建/清空 19 个字段;其中_ignored_terminal_events、_ignored_terminal_count、_open、_spans、_completed、_initial_frame_end_ns/_ticks、_launch_start_ns均被清除。新增的 drain 状态必须同时进入该重置清单(_stop_drain_records、_stop_drain_truncated、_stop_drain_admitted_count、_stop_drain_rejected_count、_stop_drain_reject_reasons、_initial_frame_end_from_stop_drain、_stop_drain_completion),否则活线发射后 drain 台账会跨代残留。 - I-3(发射后不可复活):
_emitted is True之后(:1248-1256)_records已清空且语义已重置 ⇒ 后续 chunk(含 drain 尾段)不得改变已发布报告;候选收集必须带not self._emitted条件(§4.1 E3 已有)。 - I-4(缓存与最终报告分叉):
report(source_raw_sha256=…)走重建路径(:1298要求source_raw_sha256 is None才落缓存)⇒ 最终报告由持久化 Raw 重建,与活线快照不是同一对象。这正是 §15.3 要说明的「最终报告与活线 UI 表现不同」的机制来源。
15.2.2 512 容量在三种模式下的成立性(本轮结论:原论证不成立,须改为三模式分别论证)
先一个必须写明的事实:冻结解码器的 _records 是无界的(:480 初始化、:1190 追加,全文无任何淘汰分支)。因此 §4.1 E1 里的 STOP_DRAIN_RECORD_CAPACITY = 512 不是「沿用既有容量」,而是本批新引入的局部上界;§5.4「容量 512 有 16× 余量」把「实测 31 条」与「一个新常数」当成继承关系,该论证作废。
| 模式 | 触发条件(源码) | 参与扫描的记录 | drain 候选集上界 | 512 是否成立 |
|---|---|---|---|---|
最小扫描 _minimal_live_scan = True | :466-469:not _retain_all_records and profile.validation_status != "fixture" | 仅 LIVE_LIFECYCLE_DEBUG_IDS ∪ {PRESENTED_FRAME_DEBUG_ID, TRACE_LOST_EVENTS_DEBUG_ID} ∪ LIVE_ATTRIBUTION_DEBUG_IDS 的命中项(:1078-1090,bytes.find) | = drain chunk 内白名单记录数;r3 实测「任意线程 31」 | ✅ 31 ≪ 512,但上界是实测值,不是证明值 |
全量扫描 retain_all_records=True | 同上取反;审计脚本 startup_source_shape_audit.py:80、startup_source_schema_audit.py:78 即此模式 | 全部 kd 记录(:1091-1097 逐条 unpack) | ≤ drain_chunk_bytes / 64,与 drain 字节数成正比,无结构性小上界 | ⚠️ 不成立:drain 上界 4.0 s 在高频启动期可容纳远超 512 条生命周期记录 |
| 离线回放 | LifecycleDecoder(...) 由 validate_recorded_session 构造(复用 analyze_startup_rounds.py) | 取决于构造点传入的 retain_all_records | 与所选模式同上 | 取决于构造点的取值,必须在测试中显式断言,不得默认 |
处置(本轮建议,待 Lead 裁定 D2′):把容量语义改为两段式——① 硬上界由时间窗给出(候选只接受落在本次 launch 的 drain 窗口内的记录),使上界与 4.0 s 而非与 chunk 字节数相关;② STOP_DRAIN_RECORD_CAPACITY 保留为兜底,其值由最坏情况估计而非「r3 实测 31」导出;超限时置 _stop_drain_truncated = True 并把 truncated 提升为报告层一等字段。在 ①② 落地前,本批不得声明「容量已证明充足」。
超限处置的两种可选语义(必须由 Lead 二选一,不得含糊):
- (A)全批作废:
truncated is True且admittedRecordCount == 0(「不做部分准入」的可检验化); - (B)保留已接受者:
truncated is True且admittedRecordCount == 容量,并在诊断块显式标注被丢弃数。
15.2.3 可执行测试(不接受「只断言最终数字」)
| 编号 | 目标 | 断言(可执行) |
|---|---|---|
| T-S-01 | I-1 脏标记单调性 | 逐 chunk 喂入后采样 live_retained_record_count(:1292-1295)与新增只读属性 semantic_dirty;断言不存在「有记录且不脏」的中间态 |
| T-S-02 | I-2 重置完备性 | 人为填充 drain 七个状态字段 ⇒ 调 _reset_semantics() ⇒ 断言七字段全部归零 |
| T-S-03 | I-3 发射后不可复活 | _emitted = True 后继续喂 drain chunk ⇒ report() 返回同一对象(is 同一性)且 stopDrainEndpointDiagnostics 逐字段不变 |
| T-S-04 | I-4 缓存/最终分叉 | report() 与 report(source_raw_sha256="…") 返回不同对象;在「不含 drain 新增块」的字段上逐字段相等 |
| T-S-05 | 最小扫描容量 | 合成 drain chunk 分别含 n ∈ {31, 512, 513} 条白名单生命周期记录 ⇒ n ≤ 512 全准入;n = 513 按 §15.2.2 的 (A) 或 (B) 语义断言 |
| T-S-06 | 全量扫描容量 | 同 T-S-05 但在 retain_all_records=True 下重跑;允许失败——失败即证明 512 不足,作为 §15.2.2 ① 的实证 |
| T-S-07 | 离线回放容量 | 用 LifecycleDecoder(retain_all_records=True/False) 直接构造两棵树(若 validate_recorded_session 不暴露该参数则不经它),断言两模式的 admittedRecordCount 并记录差异,差异不得被静默吞掉 |
| T-S-08 | 零增量 | S2/r1/r2 在三模式下均 stopDrainEndpointDiagnostics is None 或 admittedRecordCount == 0,且 ignoredTerminalBeforeBeginCount 与冻结版相等 |
15.3 返工 3(D1):本阶段只产 stop-drain 端点诊断,不发布 totalDurationMs
D1 裁定:不发布。本段取代 §3.3 的发布形态、§3.4 的部分论证与 §14 D1 的「本方案倾向」。
15.3.1 新结果形态(r3 应然)
核心约束:主报告(phaseResults / spanCount / unmatchedBeginCount / requiredMissingPhases / phaseCoverageComplete / status / reason / totalDurationMs / totalQuality / totalSource / totalRepairReason / _ignored_terminal_events)逐字段与冻结版相同。唯一增量 = 一个新的诊断块。
| 字段 | 冻结版(现状) | D1 下修复后 | 说明 |
|---|---|---|---|
totalDurationMs | null | null | 不发布 |
totalQuality | "missing" | "missing" | 因 total is None(:1568 既有推导),不提升 |
totalSource / totalRepairReason | null | null | 诊断值不进这两个契约位 |
status / reason | partial / lifecycle-spans-incomplete | 不变 | |
phaseCoverageComplete | False | False | 不提升阶段覆盖 |
requiredMissingPhases | ["Initial Frame Rendering"] | 不变 | 诊断视图另列 |
spanCount / unmatchedBeginCount | 11 / 1 | 不变 | 不写 _spans / 不写 _open |
_completed | False | False | |
stopDrainEndpointDiagnostics | 不存在 | 新增 | 见下 |
活线完成信号(:1224-1228) | 不满足 | 不满足 | 与现状一致 |
| UI 面板 | partial | partial | 不升级为干净候选值 |
新增诊断块的字段(全部为只读派生,不参与任何主契约):
stopDrainEndpointDiagnostics:
prefilteredRecordCount : 3 # 预筛(role 排除后)
admittedRecordCount : 2 # 通过身份门 + 链闭合
rejectedRecordCount : 1
rejectReasons : {"terminal-already-closed": 1}
chainStartClosedPhase : "UIKit Scene Creation"
chainEndOpenedPhase : "Initial Frame Rendering"
selectedCloserDeviceMs : 2300.676
rejectedCloserDeviceMs : 2313.027
selectedMinusRejectedTicks : 296418
selectedMinusRejectedMs : 12.35075
diagnosticEndpointMs : 2300.676 # 仅诊断:区间长度,不是 Total
diagnosticEndpointSource : "coreprofile-endpoint-stop-drain"
diagnosticRepairReason : "frame-end-stop-drain"
phaseCoverageDiagnosticOnly : true
truncated : false
sourceEpoch : 1
reconnectGeneration : 0
deviceLost : {markerCount, lostBefore/During/After, lostByCPU, argSemanticsVerified:false}实现层影响(相对 §4.1 的收紧):
- §4.1 E6(写
total_source)取消;E7(改_recover_total_endpoint的来源标签)取消;E8 改为本单一诊断块。 - §4.1 E5 收窄:链闭合逻辑在副本结构上计算并只写入诊断块;禁止写
self._open、self._spans、self._initial_frame_end_ns、self._initial_frame_end_ticks、self._completed。 - E1 中
STOP_DRAIN_TOTAL_SOURCE/STOP_DRAIN_REPAIR_REASON两个常量保留但降为诊断标识(不得出现在totalSource/totalRepairReason)。 - 由此,D1 下本批对解码器语义的写面仅剩:新增有界候选缓存 + 一个只读诊断块。
_pair_markers、_lifecycle_anchor_ticks、_recover_total_endpoint、report()主流程逐字节不变。
15.3.2 最终报告与活线 UI 的不同表现(D1 明确要求说明)
| 面 | 活线(Panel / report-live.json / launch-trace-live.json) | 最终(report-final.json) |
|---|---|---|
totalDurationMs | 无 | 无 |
| 完成信号 | :1224-1228 要求 totalDurationMs is not None ∧ totalQuality == "candidate" ⇒ 永不满足 | 同 |
| drain 段快照 | :1211-1212 的 not is_frame_continuation 前置 ⇒ drain chunk 不发射完成快照 | 由持久化 Raw 重建时可跑完整 drain 诊断 |
| 阶段表 | partial | partial(不变) |
| 诊断块 | 不存在(drain 段不发射) | 存在 |
| 用户可见行为差异 | 无 | 无(仅报告多一个只读块) |
⇒ D1 使活线与最终报告在「无 Total、阶段仍 partial」上完全一致,差异仅剩「最终报告多一个只读诊断块」。这比 §3.3(发布 Total)的行为面更窄,并消除了 §3.5 / R1 描述的「活线静默不完成 vs 最终有 Total」不一致。
15.3.3 未来若要发布数值,必须补齐的三项(本批不做)
- 来源标签:需要一组不与真实配对混淆的 token(维持 §6.1 不变式),并同时定义
totalQuality映射与全部下游消费点(:1224-1228、online_report.py:1063-1068)。 - Swift 展示:MacApp 侧必须能显示「该 Total 来自 stop-drain 补齐」的显式标注(含被拒绝 closer 的 Δ);现状 Swift 只消费 Total 值本身,无来源位。
- E4 门禁:必须有一轮真机 E4 证明该 Total 与独立可观测参考(操作者分段标记 + 宿主轴)在同一会话内可比;本批为 E2,不得以离线重放代替。
未补齐前,totalDurationMs 保持 null。
15.4 返工 4(D2):删除 launch_start + 15 s 与「超过启动侧最大 tick」两道硬准入门
D2 裁定:§3.2 的 G3 整行与紧随其后的「上界为何取 15.0 s」整段作废。
15.4.1 删除理由(三条,均有实测支撑)
- 15 s 是主机等待预算,不是设备 stop fence:
STARTUP_CORE_PROFILE_MAX_WAIT_SECONDS = 15.0表达“采集方愿意等多久”(四轮startupCoreProfileCompletionWaitStatus = "timed-out"、ElapsedSeconds ≈ 4.0),与「设备侧生命周期在哪个 tick 结束」无因果关系。拿它当设备域上界=把主机策略当设备证据。 - 以「≤ 启动侧最大 tick」为单边上界会恰好全拒要救的记录(
app-rounds/startup-delivery-lag.json,只读复算):r3 启动侧 chunk 内最大设备 tick 折算 +1 529.529 ms(maxDeviceTickInStartupLifecycleChunks),而三条预筛记录在 +2 246.633 / +2 300.676 / +2 313.027 ms ⇒admittedByDeviceBoundCount = 0。该门在原理上与目标相反:端点本就晚于 tap 停止,这正是它被排进 drain 的原因。 - 对负向回归零贡献:三轮的负向保护已由身份门与来源门独立给出(S2 的 9 条被身份门清零;r1 / r2 的 drain chunk 全为
bplist、零 kd 记录)⇒ 删除 G3 不削弱任何一轮的负向结论。
15.4.2 替代:不做区间门,只做顺序门 + 缺值 fail closed
- 保留的唯一时间约束(并入链闭合,不单列准入门):被准入的 terminal 必须晚于其闭合的 open span 的 BEGIN
machTicks。该语义已由冻结版承担——_pair_markers:1838-1841的timestamp_ns < begin["timestampNs"]⇒unmatched_end;drain 子状态机必须复用同一比较,不得自造上界。 - 不做任何以 launch start 为基准的设备域区间判断(既无下界也无上界)。
fail closed 形态(必须写进实现与测试)
| 缺值情形 | 本轮实测 | 必需行为 |
|---|---|---|
launchStartTimestampNs is None | r1 实际取到 None(launchStartTimestampNs = None;PHASES[0] BEGIN 缺失 ⇒ _launch_start_ns 未置) | 不得使用任何「相对 launch start」的量做准入;若实现需要 launch_start_ticks 而为 None,则拒绝整批 drain 候选并记 rejectReasons["launch-start-unknown"];严禁退回「无上界即放行」 |
captureFirstKernelTs / captureLastKernelTs 缺失 | 四轮均有值(:1985-1986) | 不参与准入,仅诊断 |
sourceEpoch 缺失或 ≠ 1 | T2 = 1;四轮启动轮未跨 epoch | ⇒ 不发布诊断块(source-epoch-unknown) |
coreProfileRestartCount > 0(重连) | 四轮均为 0 | ⇒ 不发布(reconnect-across-drain) |
_target_pid is None | 四轮均有目标 | ⇒ 整批不可用(PID 门天然 fail closed) |
initialFrameEndTimestampNs is None | 四轮均为 None | ⇒ 上界未知;只能走 §15.4.2 的顺序门,不得改用任何代理上界 |
对抗性用例同步改写:§5.3 A-X-03 作废(其内容即以「启动侧最大 tick」为门),替换为 A-X-03′:注入 endMachTicks <= beginMachTicks 的 drain terminal ⇒ 拒绝并记 non-monotonic-terminal;A-X-04(只有 BEGIN、无 END)保留,断言改为复用同一计数。
15.5 返工 5(D3):任务 B 拆为 B0/B1/B2/B3;更正 metric_engine.py 事实
15.5.1 事实更正(本文档一处事实错误)
§1 与 §13 的「四个 metricID 在 metric_engine.py 中完全不出现(该引擎只有 ios.foreground.fps 一条…)」前半句不准确:
metric_engine.py:141确实存在MetricFormula("ios.foreground.fps", "FPS", "foreground-device", "frames-per-second", "count(presentationTimestamp in trailing 1 second)", "activitytrace-framelifetime-window-fps.v1");metric_engine.py:476确实存在("ios.foreground.fps", "CoreAnimationFramesPerSecond")的源字段映射。
正确表述:ios.foreground.fps 在 Metric Engine 中已有一条既存路径,但它绑定的是 graphics.raw 形状的输入字段 CoreAnimationFramesPerSecond,与 T2 实际交付的 display.frame-lifetime.raw(adapterID = pmd3.activity-trace.frame-lifetime.raw、rawUnit = signpost-framelifetime)不是同一条输入通道,故在 T2 上不会被喂入(T2 无 graphics.raw 事件)。
⇒ §1 表格中 ios.foreground.fps 的「现存唯一可能生产者」一栏修正为两个候选:metric_engine.py 的 graphics.raw 路径(本会话不可用)与 DisplayPerformanceProjector.frames_per_second(本会话可用但未接线)。§6.2 与 §13 中「仅 ios.foreground.fps」的括注同理更正。
结论方向不变:四项 sampleCount = 0 仍是接线缺口,不是采集缺口。且 ios.display.refresh_rate / ios.frame.pacing_jitter / ios.frame.time.p95 三项确实在 metric_engine.py 中无任何路径。
15.5.2 拆批后的四条线
| 编号 | 名称 | 内容 | 证据来源 | 本批处置 |
|---|---|---|---|---|
| B0 | 时钟/投影取证 | 用 DisplayPerformanceProjector 离线重放 T2 的 1716 条 display.frame-lifetime.raw,导出双轴(hostReceiveMonotonicNs vs presentationTimestampNs)与五项口径;产出 stale 占比、host_gap_ns 分布、refreshMultiple 分布 | §7.3 E-B1 + T2 原始事件 | 本批只做取证(离线、只读、零产品改动) |
| B1 | 单所有者接线 | 裁定 DisplayPerformanceProjector 为这四项口径的唯一所有者,并以 metric-ID 形状交到报告链路,使 coverage[].sampleCount 可计数 | realtime.py:1069-1097 + online_report._consume_metric_frame()(:1087-1155,sample_count 唯一写入点) | 另批实施;本批只登记边界与前置裁定 |
| B2 | 缺失 reason | 为 is_active == False 等情形写入有界且具名的 reason(现为 reason = null),范围限死显示指标五项;全局 missing 词表让给 Device Network 批次 | realtime.py:1098-1125 的 else 分支 + capture_plan.MetricCoverage(:233-310) | 另批实施(与 Network 批次协调) |
| B3 | 请求指标覆盖摘要 | 在 dataCompleteness 之外并置可机械检验的计数(如 derivedMetricCoverage = {"requested": 5, "withSamples": 1}),不重定义 dataCompleteness | T2 实测:dataCompleteness = "complete" 与 4/5 项零样本并存 | 另批实施;本批只登记 |
被否决项保留:原 §4.3 B4(扩 _apply_frame_coverage.metric_map)继续明确不做。本轮逐行核对 metric_map(online_report.py:1342-1358)共 15 个键 = ios.frame.time + 14 个 ios.frame.* 兄弟项,确实不含 ios.display.refresh_rate / ios.foreground.fps / ios.frame.pacing_jitter / ios.frame.time.p95(其中 ios.frame.time.p95 的 10 s 窗与 refresh_rate 的硬件 VSync 无法从 frameSummary 取值)⇒ 扩表会造出第二个口径所有者,违反 realtime.py:1013-1015。
15.5.3 dataCompleteness = "complete" 的正确读法(D3 明令)
dataCompleteness描述的是报告产物自身是否完整落盘/封闭(T2:reportStatus = "sealed"、publishedRawSequence = 1717),不是「请求的五项指标均有样本」。- T2 反证:
coverage5 项中 4 项sampleCount = 0、reason = null、coverage = [];仅ios.frame.time有sampleCount = 1686(=frameSummary.intervalCount),而顶层仍为"complete"。且唯一「有样本」项的firstSourceSequence/lastSourceSequence均为null⇒ 该覆盖账本连序列区间都未接线。 - ⇒ 不得把
dataCompleteness = "complete"解释为「五项指标均有样本」;B3 只并置计数,不改其语义。
15.6 返工 6:扩展后的可执行验收矩阵
所有用例必须分别在最小扫描 / 全量扫描 / 离线回放三种模式下运行并分别出表(§15.2.2)。本节新增于 §5 / §9 之外,不改其既有编号。
15.6.1 正向(r3 原始 drain 保留 + 链闭合)
| 编号 | 用例 | 断言 |
|---|---|---|
| T-P-01 | r3 原始字节离线重放(不改标、不合成、不 mock) | prefilteredRecordCount == 3;admittedRecordCount == 2;rejectedRecordCount == 1;rejectReasons["terminal-already-closed"] == 1 |
| T-P-02 | 同上 | selectedMinusRejectedTicks == 296 418;selectedMinusRejectedMs == 12.35075;胜出者 machTicks 严格更小(与到达顺序无关) |
| T-P-03 | 同上 | totalDurationMs is None;totalQuality == "missing";totalSource is None;totalRepairReason is None(D1) |
| T-P-04 | 同上 | status == "partial";reason == "lifecycle-spans-incomplete";phaseCoverageComplete is False;requiredMissingPhases == ["Initial Frame Rendering"];spanCount 与 unmatchedBeginCount 与冻结版相同 |
| T-P-05 | 合成最小 fixture(1 opener + 1 closer) | admittedRecordCount == 2;rejectedRecordCount == 0;rejectedCloserDeviceMs is None |
15.6.2 负向(S2 / r1 / r2 真实会话)
| 编号 | 用例 | 断言 |
|---|---|---|
| T-N-01 | S2 离线重放 | admittedRecordCount == 0;totalDurationMs is None;requiredMissingPhases == ["Initial Frame Rendering"];ignoredTerminalBeforeBeginCount == 1;报告中不出现任何 stop-drain token(stopDrainEndpointDiagnostics is None) |
| T-N-02 | r1 离线重放 | requiredMissingPhases == ["System Interface Initialization", "Initial Frame Rendering"];admittedRecordCount == 0;因 launchStartTimestampNs is None 而不得产生任何端点;若出现候选则必须记 launch-start-unknown(r1 实测候选为 0) |
| T-N-03 | r2 离线重放 | requiredMissingPhases == ["Initial Frame Rendering"];admittedRecordCount == 0 |
| T-N-04 | 三轮在上述三模式下 | 与冻结解码器逐字段一致(除本批新增块);_ignored_terminal_events 元组集合与 _lifecycle_anchor_ticks() 逐轮逐值相等 |
15.6.3 Lead 点名的七类边界(逐项必须有独立用例)
| 编号 | 类别 | 注入 / 构造 | 断言 |
|---|---|---|---|
| T-D-01 | 双 closer | 同一 chunk 内两条 0x31CA0006,且到达顺序与 tick 顺序相反 | 胜出者仍为 argmin(machTicks);另一条记 terminal-already-closed;selectedMinusRejectedMs > 0 |
| T-D-02 | 跨代际 | drain chunk 携带与本次不同的 target_generation | ignoredGenerationEventCount +1;准入 0;诊断块不出现(:959-966 在 chunk 级直接 return) |
| T-D-03 | 跨代际(缺值) | drain chunk 的 target_generation = None | 记录并断言该门不拦(:959-966 为缺值 fail-open)⇒ 此时身份仅由 PID/线程映射门承担;该门不通过则准入 0 |
| T-D-04 | 线程重用 | 同一 tid 先后绑定到两个 PID(含目标) | 两遍归因(_absorb_attribution 覆盖式)后的 _thread_pids 决定归属;断言准入结果与「最终映射」一致且可复算 |
| T-D-05 | 乱序 tick | drain chunk 内记录 tick 逆序(模拟多 CPU 交错) | 排序键 (machTicks, arrivalSequence) 生效;胜出 closer 不随到达顺序改变 |
| T-D-06 | 超容量 | drain 记录数 = 512 / 513(最小扫描);并在全量扫描下探真实上界 | 按 §15.2.2 的 (A) 或 (B) 语义断言;全量扫描用例允许失败以暴露 512 不足 |
| T-D-07 | drain timeout | handoffDrainStatus == "timeout" 且在 4.0 s 上界处截断 | 截断处无终点的记录不得被当成端点;rejectReasons["drain-truncated"] 计数;诊断块 endpointComplete = False |
| T-D-08 | live / final 差异 | 同一 Session 先取活线快照(report-live.json 形),再取最终(report(source_raw_sha256=…)) | 二者在无 drain 新增块的字段上逐字段相等;最终报告多出 stopDrainEndpointDiagnostics;活线 totalDurationMs is None;I-4 对象不同一 |
15.6.4 新真机轮次的条件判定(不得预设三条事件或 Total)
每轮先做原始字节形态分类,再套对应判据。禁止预设「每轮都有 1 opener + 2 closer」或「每轮都有 Total」。
| 形态类 | 字节特征(只读判定) | 该类通过条件 |
|---|---|---|
| α 完整链 | drain chunk 内命中 1× 0x2B870030 且 ≥1× 0x31CA0006(同目标 tid) | admittedRecordCount == 2;多余 closer 记 terminal-already-closed;诊断块齐备;totalDurationMs is None |
| β 仅有 terminal | 命中 0x31CA0006 但无 0x2B870030 | 必须拒绝(链无起点):endpointComplete = False、rejectReasons["no-opener"] >= 1;不得产出端点,也不得进 _ignored_terminal_events |
| γ 无 lifecycle | drain chunk 无生命周期命名空间记录(或全 bplist) | stopDrainEndpointDiagnostics is None;零增量;与冻结版逐字段一致 |
| δ 缺 launch start | launchStartTimestampNs is None | fail closed(§15.4.2);rejectReasons["launch-start-unknown"](有候选时计数) |
| ε 跨 epoch / 重连 | sourceEpoch != 1 或 coreProfileRestartCount > 0 | 不发布诊断块 |
| ζ 多 opener | ≥2× 0x2B870030 | 走 _pair_markers 既有「latest-begin 覆盖」语义(_superseded_begin_count,:1800-1818);断言净新增 open span 为 0 |
每轮产出 shape-class.json(形态类 + 原始命中计数 + 对应判据结论),不写 PASS/FAIL,只写「符合 / 不符合该类判据」及不符合项。
15.7 未证事项(本段新增,不得与已证事项混排)
| # | 未证事项 | 为何未证 | 处置 |
|---|---|---|---|
| U-1 | S2 / r1 / r2 的 opener 为何不在交付字节里 | 环形覆盖 / 不同 UIKit 路径 / 采集窗口截断三候选均未排除(§2.3) | 保持未定;不得以「r3 能修」推断三轮也可修 |
| U-2 | drain 内非目标线程的 31(r3)/ 9(S2)条生命周期记录归属 | 未区分另一进程 / 预热侧车 / 其它 generation | 保持未定;身份门使其与结论无关 |
| U-3 | FrameTime 两轴 ≈4.4 倍差的定性成因 | 离线缺设备↔主机绝对桥(§7.3 E-B4) | B0 只取证,不作因 |
| U-4 | 512 容量在全量扫描模式下的真实上界 | 本轮仅证明「无常量可继承」+「r3 实测 31」;上界与 drain 字节数相关 | 列为待 Lead 裁定 D2′(§15.2.2) |
| U-5 | deviceLostArg1 的真实语义 | 源码自述 deviceLostArgSemanticsVerified = False(:1999) | 不得据此推断丢失记录数 |
| U-6 | r1 lostDuringStartupCount = 4 的时间窗含义 | launchStartTimestampNs 与 initialFrameEndTimestampNs 均为 None ⇒ 落入兜底桶 | 不得读作「落在启动窗内」 |
| U-7 | 第二条 closer(+2 313.027 ms)在被拒后的现实语义 | 无独立证据(仅知其为同 tid 的 0x31CA0006) | 只记「被拒绝」,不解释其来历 |
| U-8 | 「阶段覆盖在主报告里补齐」是否可行 | D1 下链闭合只写诊断块,闭合无法经 _pair_markers 表达 | 若 Lead 要求主报告也补齐,须回到「drain 端点能否进主契约」的原始问题(§15.3.1) |
15.8 逐条证据来源(本段全部读数的可复算锚点)
| 读数 | 来源(全部只读) |
|---|---|
四轮 targetGeneration = 1 / ignoredGenerationEventCount = 0 | Sessions/<sid>/collector-core/startup-report.json(与 launch-trace-report.json 在这些字段上逐字段相同) |
四轮 deviceLostEventMarkerCount = 1 / 4 / 3 / 10;lostBefore/During/After = 0/n/0 | 同上,sourceQuality 与 startupObservability |
四轮 launchStartTimestampNs:S2 有值、r1 = None、r2 有值、r3 有值;四轮 initialFrameEndTimestampNs 均为 None | 同上,startupObservability |
T2 sourceEpoch = 1、sourceIdentityCount = 2、sourceIdentityDigest = 5c725d7d…、reportStatus = "sealed"、publishedRawSequence = 1717 | Sessions/d3206bbe…/collector-core/report-final.json |
T2 coverage 5 项(含 sourceEpoch: 1、firstSourceSequence: null、sampleCount = 0/0/0/1686/0) | 同上 |
T2 dataCompleteness = "complete"、metricManifest(空串 SHA-256 / frameCount = 0 / metricIDs = [] / consumedRawEventCount = 1717) | 同上 |
_apply_frame_coverage.metric_map 的 15 个键 | online_report.py:1342-1358 |
metric_engine.py:141 / :476 的 ios.foreground.fps 路径 | metric_engine.py(本条为事实更正) |
lossFree = (live_queue_drop_count == 0) | launch_trace.py:1605-1606 与 :2135-2136 |
_records 无界;_semantic_dirty 成对写入;_reset_semantics() 的 19 项;活线发射后的 clear() + 重置 | launch_trace.py:480 / :1190-1191 / :1254-1256 / :1618-1645 / :1647-1669 |
首配对 terminal 胜出 + break | launch_trace.py:1862-1866 |
| generation 门在缺值时 fail-open | launch_trace.py:959-966 |
| 阶段丢失窗口三分支 | launch_trace.py:1974-1979;deviceLostArgSemanticsVerified = False(:1999) |
_minimal_live_scan 的三模式判据 | launch_trace.py:466-469;审计脚本 startup_source_shape_audit.py:80、startup_source_schema_audit.py:78 |
r3 预筛 3 条的 tick / tid / chunk / 角色(含 machTicks 差 296 418) | app-rounds/drain-admission-delta.json(analyze_drain_admission_delta.py) |
r3 启动侧最大设备 tick 折算 +1 529.529 ms;admittedByDeviceBoundCount = 0 | app-rounds/startup-delivery-lag.json(analyze_startup_delivery_lag.py) |
四轮 Mode A candidates 全为空;r3 _ignored_terminal_events 为空 | app-rounds/drain-admission-delta.json |
15.9 与 §0–§14 的冲突清单(便于 Lead 逐处定位)
| 位置 | 冲突内容 | 本段处置 |
|---|---|---|
| §1「任务 A 的定量边界」表头与结论句 · §3.2 标题 | 把 3/0/0/0 称为「有界准入增量」 | 作废称谓 ⇒ §15.1.1(预筛数;r3 预期准入 = 2) |
§1 ios.foreground.fps 行「现存的唯一可能生产者」 · §13 括注 | 称 metric_engine 中无该路径 | 事实更正 ⇒ §15.5.1 |
| §3.2 G3 整行 | launch_start + 15 s 且 tick > max(启动侧已见设备 tick) | 删除 ⇒ §15.4 |
§3.2 末段「上界为何取 15.0 s 而不是 max(启动侧 tick)」 | — | 整段删除 ⇒ §15.4.1 |
§3.3 totalSource / totalRepairReason / totalQuality = "repaired" | 发布形态 | 被 D1 取代 ⇒ §15.3.1(改为只读诊断块) |
§3.3「requiredMissingPhases = []、phaseCoverageComplete = true」· §3.3「spanCount 11 → 12」 | 覆盖升级 | 取消:主报告逐字段不变 ⇒ §15.3.1 |
| §3.4「修复带来最终离线报告与阶段覆盖的正确」 | 与 D1 冲突 | 收窄为「仅诊断层正确」 ⇒ §15.3.1 / §15.3.2 |
| §4.1 E6 / E7 | 写 total_source / 改 _recover_total_endpoint 标签 | 取消 ⇒ §15.3.1 |
| §4.1 E8 | 新增报告块 | 改为单一诊断块 ⇒ §15.3.1 |
| §4.1 E5 | 闭合 self._open、追加 self._spans | 收窄:只在副本上计算 ⇒ §15.3.1 |
| §4.3 B1–B4 编号 | — | 重排为 B0–B3 ⇒ §15.5.2 |
§5.1 A-P-01 admittedRecordCount == 3 | 含被拒 closer | 作废 ⇒ 2 + 1 拒绝 ⇒ §15.1.1 |
| §5.1 A-P-02 / A-P-03 / A-P-04 | 依赖发布与覆盖升级 | 按 §15.6.1 改写 |
| §5.3 A-X-03 | 以「启动侧最大 tick」为门 | 作废 ⇒ A-X-03′(non-monotonic-terminal)§15.4.2 |
| §5.4「容量 512 有 16× 余量」 | 论证不成立 | 降级为待验证假设 ⇒ §15.2.2 |
§9.1 A-1(含 3 / Total / phaseCoverageComplete = true) | — | 按 §15.6.1 改写 |
§11.2 / §6.2 / §13 中 metric_engine.py 的括注 | 事实表述 | 更正 ⇒ §15.5.1 |
| §14 D1 / D2 / D3 的「本方案倾向」 | 发布 / 采用 15 s / 拆批 | 全部按 Lead 裁定改写:不发布 / 删除 / 拆 B0–B3 |
15.10 本轮边界(重申,不因修订而放宽)
codeChanged = NO:未改MacApp/产品代码、测试代码、历史报告;未运行新的 bm/bso/bsoc 实验;未解冻 Phase C。- 原始 Session 与
MacApp/Tests/1.trace/只读;共享工作区三份未跟踪文件(MacApp/Tests/1.trace/与两份他人 devlog)原样保留。 - 本轮不提交 / 不推送 / 不部署 / 不发交付 webhook。
docs/architecture/perfowl-p4-build74-phase2-post-r3-minimal-repair-plan-2026-09-24.html(62 944 B)仍是eeee607时点的快照,未随本轮修订更新;按 A1/C2,修订稿的归档 HTML 应在 Lead 批准且 Owner 授权提交后新增文件(docs/不回改),本批不产出。judgement = pending-lead-ai;本段不输出 PASS/FAIL。
15.11 待 Lead 复审的决策点(本段新增)
| 编号 | 待决问题 | 本段倾向 |
|---|---|---|
| D1′ | 诊断块命名与位置:采用独立的 stopDrainEndpointDiagnostics 并保持 totalSource = null?若要求主报告阶段表也补齐,则必须回到「drain 端点能否进主契约」的原始问题 | 诊断块独立、主契约不动 |
| D2′ | 512 容量:接受 §15.2.2 的两段式(时间窗硬上界 + 兜底常量 + truncated 一等字段),还是先测出全量扫描真实上界再定值?超限采用 (A) 全批作废还是 (B) 保留已接受者? | 先测(T-S-06 允许失败)再定值;超限取 (A) |
| D3′ | B0 是否在本批内执行(离线、只读、零产品改动),B1–B3 另批? | B0 本批执行取证,B1–B3 另批 |
judgement = pending-lead-ai