planP2E2partial2026-09-24

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 msrealtime.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(历史参考)NEGATIVE1409
T1-r1NEGATIVE900
T1-r2NEGATIVE1300
T1-r3POSITIVE14331

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 旁路事实(两者都独立保证负向回归不被破坏):

  1. S2 在 drain chunk 内确实有 9 条生命周期命名空间可产 marker 的记录,但无一条归属目标线程 ⇒ 目标 PID/generation 门天然把它清零;
  2. r1 / r2 的 frame-continuation chunk 不含任何 binary chunk(全部为 bplist,零 kd 记录,与 R3 取证一致)⇒ 连清点对象都不存在。

⇒ 「有界 Stop-drain 补齐」的准入增量是 3 / 0 / 0 / 0,可论证为恰好命中 r3 且对三轮负向零副作用。

任务 B:报告映射问题已定位到具体的缺失映射行,且是「映射缺口」不是「数据缺口」

已请求显示指标valueQualitysampleCount现存的唯一可能生产者实际状态
ios.frame.timecandidate1686online_report._apply_frame_coverage()(metric_map 有该行)已接通
ios.display.refresh_ratemissing0DisplayPerformanceProjector.refresh_rate_hz无映射行
ios.foreground.fpsmissing0DisplayPerformanceProjector.frames_per_second(trailing 1 s)无映射行
ios.frame.pacing_jittermissing0DisplayPerformanceProjector.frame_pacing_jitter_ms(10 s 窗 P95−P50)无映射行
ios.frame.time.p95missing0DisplayPerformanceProjector.frame_time_p95_ms(10 s 滚动 P95)无映射行

机械证据(同一次 Session d3206bbe…):

结论:四项 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-1opener 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-8startup-only 无帧续采 ⇒ drain 携带的是启动窗口记录(设备域 +2.25 / +2.31 s ≪ t5 的 +8.23 s 主机时刻)manifest + 设备 tick

2.2 S2 的 null 与 r3 的 null 是两个不同的缺口(不可混为一谈)

S2r3
设备域是否存在 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 明确未定、本方案不升级的三项

  1. S2 / r1 / r2 的 opener 为何不在交付字节里(环形覆盖 / 不同 UIKit 路径 / 采集窗口截断,三候选均未排除);
  2. drain 内非目标线程的 31(r3)与 9(S2)条生命周期记录属于谁(另一进程 / 预热侧车 / 其它 target generation 均未区分);
  3. FrameTime 两轴 ≈4.4 倍差的定性成因(见 §8)。

---

3. 任务 A 方案:有界 Stop-drain 生命周期补齐

3.1 设计原则

  1. 不改采集侧标定:realtime.py 的 role 判定与 chunk 打标逐字节不动。「有界」体现在解码侧准入,不体现在改标。
  2. 准入必须是「补齐」而非「扩权」:drain 记录只能闭合已由保留记录开启的启动阶段;不得凭空开出新的、且结束时仍 open 的阶段。
  3. 结构性不动 Mode A:drain 记录永不进入 _markers 与 _ignored_terminal_events,因此 _lifecycle_anchor_ticks() 的取值集合与 Mode A 的 candidates 过滤器在构造上与冻结版逐字节一致。
  4. 来源必须可分辨:drain 挣得的端点不得复用 totalSource = "coreprofile-endpoint" + totalRepairReason = "endpoint" 这一「真实配对」标签对(见 §6)。
  5. 有界资源:仅在存在候选 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 sr3 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 应然)

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 挣得的端点当成干净端点。因此:

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`
E3consume() 保留判定(: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 台账(含逐门通过/拒绝计数与拒绝原因)
E6report() 干净端点块(: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" 全部逐字节不变
E8report() 返回字典(: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 另行批准)

编号位置内容为何需要单独批准
B1realtime.py:1069-1097 + online_report._consume_metric_frame()把 DisplayPerformanceProjector 的五项产出同时以 metric-ID 形状({"metricID","value","quality"})交到报告链路,使 coverage[].sampleCount 可计数触及「唯一公式所有者」契约;须先裁定 DisplayPerformanceProjector 为这四项的唯一所有者
B2realtime.py:1073 的 else 分支 / capture_plan.MetricCoverage当 is_active == False 时,为这五项写入有界且具名的 reason(如 display-projector-inactive),取代 reason = null与 Device Network 批次的 missing 语义 重叠 ⇒ 本批只做到「显示指标五项」范围并显式划界,全局词表留给 Network 批次
B3online_report 顶层在 dataCompleteness = "complete" 之外并置一个可机械检验的计数(如 derivedMetricCoverage = {"requested":5,"withSamples":1}),不静默重定义 dataCompleteness 的语义顶层完整性语义属全局契约,改动面超出本批
B4online_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-05fixture 目录 fixtures/ 增加一份匿名化的 r3 形态 fixture(只保留必要 kd 记录,不含任何设备标识)端到端 report() 全字段比对基线快照

5.2 负向(必须继续 missing)

编号用例断言
A-N-01drain chunk 内生命周期记录但 tid 不属于 target(复刻 S2 的 9 条形)admittedRecordCount == 0;totalDurationMs is None;requiredMissingPhases 含 Initial Frame Rendering;报告中不出现 stop-drain 任何 token
A-N-02frame-continuation chunk 全为 bplist(复刻 r1/r2 形)stopDrainCompletion is None 或 admittedRecordCount == 0;不新增 span
A-N-03S2 真实会话离线重放totalDurationMs is None、missing == ['Initial Frame Rendering']、ignoredTerminalBeforeBeginCount == 1;与冻结解码器逐字段一致(除本批新增块)
A-N-04r1 真实会话离线重放missing == ['System Interface Initialization','Initial Frame Rendering'];准入 0
A-N-05r2 真实会话离线重放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 §工作流程)

---

6. 标签契约与「反事实」防误用

6.1 必须保持的不变式

> totalSource == "coreprofile-endpoint" 且 totalRepairReason == "endpoint" ⟺ 真实配对(非恢复路径)

本方案通过引入两个新 token 保持该不变式:totalSource = "coreprofile-endpoint-stop-drain"、totalRepairReason = "frame-end-stop-drain"。三处既有消费点因此自动正确:

  1. totalQuality(launch_trace.py:1568-1572)⇒ "repaired"(不是 "candidate");
  2. 活线完成信号(:1224-1228)⇒ 要求 "candidate" ⇒ 不触发;
  3. _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 = []。

该组合被明确否决,理由有三,且均为结构性而非口味问题:

  1. 它把 drain 挣得的端点伪装成干净端点(totalSource/totalRepairReason 落在真实配对标签对)⇒ 违反 §6.1 不变式;
  2. 它靠无条件改标 chunk 达成,正是本次被明令禁止的手法;本方案的 3/0/0/0 增量与链闭合门才是「有界」的证据;
  3. 该数值在本方案中不得作为真值引用。它只在 §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.durationMs30 721.029 ms设备轴
产品自报 displayAcquisitionCensus.observedSpanMs134 912.872 ms主机轴
产品自报 sourceGapCount0—
产品内部比值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」的结论一致)。因此:

---

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"]
launchModecontrolled-cold-launch
startupEnabled / screenshotsEnabledfalse / false
requestedFrameFilter / effectiveFrameFilter0x31800318 / ["0x31800318"]
capturePlanHash必须等于 T2 的 bff494dc…(记录后立刻核对,不一致 ⇒ INVALID)

8.2 分段与时间标记(真实打点、不代填)

段计划时长动作
S10–30 s连续滚动
S230–60 s静止(手指离开屏幕,App 保持前台)
S360–90 s连续滚动
S490–120 s静止
尾+20 s保持前台静止,保证 Stop 前有静止样点

打点机制(三级,互为独立来源,任一缺失即记为缺口、不补造):

  1. 宿主侧 5 s 心跳账本(主,零产品改动):录制期间在另一终端运行

while :; do date -u +%FT%T.%3NZ; sleep 5; done > phase-marks.txt, 记录 Session 的 startedAt / endedAt 与 durationMonotonicNs,用两端锚点做一次线性拟合把墙钟心跳映射到单调域。140 s 量级下分辨率 ≤ 1 s,足以切段。

  1. 操作者手写四段起止(辅):操作者自报「滚动/静止」切换时刻(只记整秒),与 (1) 交叉核对;两者不一致时记录分歧,不取平均、不改数据。
  2. 信号自证(独立于人工):静止段的 refreshMultiple 应几乎恒为 1×(设备轴 p50 ≈ 16.64 ms),滚动段应出现 2×–6× 倍数与更宽的 refreshMultiple 取值集合。用 refreshMultiple 的分布跳变独立推断段边界,再与 (1)(2) 对照。三条独立来源一致才认定为有效分段。

8.3 每轮交付物

---

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离线重放 S2totalDurationMs = null;missing = ["Initial Frame Rendering"];准入 0;报告内不出现任何 stop-drain token
A-3离线重放 r1missing = ["System Interface Initialization","Initial Frame Rendering"];准入 0
A-4离线重放 r2missing = ["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-2E-B2 覆盖度账本observedSpanMs ≈ 134.9 s、presentationSpanMs ≈ 30.7 s、spanRatio ≈ 4.38 三值均出现在报告中
B-3E-B3 交叉一致性门四条断言全过(误差 ≤ 1 ms 或精确相等)
B-4修复前后对比五项 coverage[] 的 sampleCount 不全为 0;reason 不再为 null;dataCompleteness 与样本事实不再矛盾
B-5§8 四段同机测试四段边界三源一致(或分歧被显式记录);滚动/静止双轴账本可对比

所有判据的最终裁定归 Lead AI;本方案不输出 PASS/FAIL。

---

10. 风险清单

#风险影响缓解 / 检出
R1drain 端点被静默提升为「干净端点」下游把 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 被误认为启动端点假 TotalG4 链门要求同一准入集合内存在同 PID/TID 的 code=12 opener(保留 :1822-1828 既有契约)+ G3 设备区间门
R4G3 前向门可能误拒合法记录(drain 设备域回卷)漏修 r3 形门写成区间而非单边;拒绝必计数并出表(rejectReasons),永不静默丢弃
R5drain 缓冲无界内存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 为唯一所有者,并加「单所有者」单测
R9dataCompleteness 语义被静默重定义全局契约漂移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. 诚实性声明(必须在验收时一并读)

  1. 负向回归不可能逐字保持:一旦修复生效,r3 的 totalDurationMs 由 null 变为真实值。因此负向回归的唯一正确表述是:**「S2 / r1 / r2 必须继续 missing,且任何轮次、任何情况下都不得出现 *-repaired 之外的虚假干净标签、不得放宽锚点」**。把「r3 仍为 null」写进验收判据,是把已修缺陷当回归基准。
  2. 本批零产品改动,故其自身证据等级为 E2(离线构建/测试级),不含任何真机 E4 结论。
  3. 不在本批范围内的三项未定项(§2.3)在本方案中保持未定,未以任何方式升级。
  4. 未自判:本方案不输出 PASS/FAIL,不宣称任何阶段门禁关闭或开启;judgement = pending-lead-ai。
  5. 原始 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.py

R3 已审计交付物

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 的三个待决点

编号待决问题本方案倾向影响
D1drain 挣得的端点是否发布 totalDurationMs,还是只闭合阶段、Total 保持 null?发布,但只带 coreprofile-endpoint-stop-drain / frame-end-stop-drain / repaired 三标记(活线信号不受影响)。备选:STOP_DRAIN_PUBLISH_TOTAL = False,Total 保持 null、阶段照常闭合决定 A-1 的判据形态
D2G3 设备区间门的上界是否采用 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