Skip to content

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…):

  • 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-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 应然) ​

  • ios.process.startup.initial_frame_rendering_ms 出现(PHASE_METRIC_IDS,launch_trace.py:135);
  • UIKit Scene Creation span 闭合 ⇒ 准入后 unmatchedBeginCount 由 1 归 0,spanCount 11 → 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
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 §工作流程) ​

  • 关键路径: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"。三处既有消费点因此自动正确:

  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」的结论一致)。因此:

  • 本方案不声称已证明成因;
  • 若后续需要定性,唯一可行路径是 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"]
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,足以切段。
  2. 操作者手写四段起止(辅):操作者自报「滚动/静止」切换时刻(只记整秒),与 (1) 交叉核对;两者不一致时记录分歧,不取平均、不改数据。
  3. 信号自证(独立于人工):静止段的 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离线重放 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


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 的处置
13/0/0/0 是「role 排除后的预筛记录数」,不是四道门的最终准入数;r3 必须选定一条闭合链并记录另一 closer 的拒绝理由;补齐身份/代际/纪元/丢失边界§15.1§1 表格与结论句的「准入增量」称谓作废(数值保留)
2画清候选缓存 / _semantic_dirty / _reset_semantics() / report() 生命周期;核对 512 容量在三模式下是否成立;给出可执行测试§15.2§5.4「512 有 16× 余量」降级为待验证假设
3D1:本阶段只产 stop-drain 端点诊断,不发布 totalDurationMs,不提升阶段覆盖或 UI 为干净候选值§15.3§3.3 的发布形态被取代;§4.1 E6/E7 取消
4D2:删除 launch_start+15 s 与「超过启动侧最大 tick」两道硬准入门;设备侧边界缺失时 fail closed§15.4§3.2 的 G3 整行与末段删除
5D3:任务 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 / r1 0 / r2 0 / r3 3。它只施加了两个条件——记录位于 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 链闭合必须逐条复刻该语义:

  1. 记录按 (machTicks, arrivalSequence) 排序(与 _rebuild_semantics 两遍同序);
  2. 展开 _lifecycle_actions()(:2345-2378)得 marker 流,再按 (timestampNs, arrivalSequence, actionOrder) 排序;该 marker 流不得写入 self._markers(§3.1 第 3 条);
  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_eventscandidates(anchor 之后)Mode A 结果
S210(候选 tick 16 389 392 355 267 < anchor 16 389 393 206 930)None
r100None
r200None
r300None

⇒ 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(两文件在这些字段上逐字段相同),全部只读:

边界字段来源S2r1r2r3本方案的准入后果
PIDtarget_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 generationtargetGeneration / ignoredGenerationEventCount1 / 01 / 01 / 01 / 0四轮该门从未触发。且 consume() 的 generation 门在 normalized_generation is None 时根本不检查(:959-966)⇒ 该门是缺值 fail-open,不能单独充当身份凭证;身份必须由 PID/线程映射承担
source epochreport-final.json.sourceEpoch、coverage[].sourceEpoch、rawManifest.sourceEpochCount———T2:sourceEpoch = 1(5 项 coverage 均为 1),sourceIdentityCount = 2,sourceIdentityDigest = 5c725d7d…诊断块必须带 sourceEpoch;≠1 或跨 epoch ⇒ 拒绝发布(source-epoch-unknown)
reconnect generationrawManifest.coreProfileConfigGeneration / coreProfileRestartCount / handoffRestartCompleteNs———四轮 coreProfileRestartCount = 0、handoffRestartCompleteNs = null无重连。若将来 restartCount > 0 ⇒ drain 窗口跨代 ⇒ 不发布(reconnect-across-drain)
设备丢失证据sourceQuality.deviceLostEventMarkerCount、startupObservability.deviceLostMarkerCount / lostBefore/During/AfterStartupCount / lostByCPU1(0/1/0,cpu0)4(0/4/0,cpu0)3(0/3/0,cpu0)10(0/10/0,cpu0)见下三条约束

设备丢失边界的三条诚实性约束(本轮新增,必须随报告一并读)

  1. deviceLostEventMarkerCount 是丢失事件(marker)条数,不是丢失记录数——源码注释(:1607-1611)与方法 docstring(:1952-1955)均如此声明,且 deviceLostArgSemanticsVerified = False(:1999)。
  2. sourceQuality.lossFree = true 与 deviceLostEventMarkerCount > 0 并存不是矛盾:lossFree 的判据是 live_queue_drop_count == 0(:1605-1606 与 :2135-2136),只覆盖宿主侧活线队列,不含设备侧环形丢失。四轮全部 lossFree = true 且 prelaunchRingOverwriteCount = liveQueueDropCount = 0。⇒ 不得把 lossFree = true 读作「无设备丢失」,也不得把 lossFree = false 当成唯一失效条件。该字段名与语义的错位只登记,本批不改名。
  3. 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-01I-1 脏标记单调性逐 chunk 喂入后采样 live_retained_record_count(:1292-1295)与新增只读属性 semantic_dirty;断言不存在「有记录且不脏」的中间态
T-S-02I-2 重置完备性人为填充 drain 七个状态字段 ⇒ 调 _reset_semantics() ⇒ 断言七字段全部归零
T-S-03I-3 发射后不可复活_emitted = True 后继续喂 drain chunk ⇒ report() 返回同一对象(is 同一性)且 stopDrainEndpointDiagnostics 逐字段不变
T-S-04I-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 下修复后说明
totalDurationMsnullnull不发布
totalQuality"missing""missing"因 total is None(:1568 既有推导),不提升
totalSource / totalRepairReasonnullnull诊断值不进这两个契约位
status / reasonpartial / lifecycle-spans-incomplete不变
phaseCoverageCompleteFalseFalse不提升阶段覆盖
requiredMissingPhases["Initial Frame Rendering"]不变诊断视图另列
spanCount / unmatchedBeginCount11 / 1不变不写 _spans / 不写 _open
_completedFalseFalse
stopDrainEndpointDiagnostics不存在新增见下
活线完成信号(:1224-1228)不满足不满足与现状一致
UI 面板partialpartial不升级为干净候选值

新增诊断块的字段(全部为只读派生,不参与任何主契约):

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 诊断
阶段表partialpartial(不变)
诊断块不存在(drain 段不发射)存在
用户可见行为差异无无(仅报告多一个只读块)

⇒ D1 使活线与最终报告在「无 Total、阶段仍 partial」上完全一致,差异仅剩「最终报告多一个只读诊断块」。这比 §3.3(发布 Total)的行为面更窄,并消除了 §3.5 / R1 描述的「活线静默不完成 vs 最终有 Total」不一致。

15.3.3 未来若要发布数值,必须补齐的三项(本批不做) ​

  1. 来源标签:需要一组不与真实配对混淆的 token(维持 §6.1 不变式),并同时定义 totalQuality 映射与全部下游消费点(:1224-1228、online_report.py:1063-1068)。
  2. Swift 展示:MacApp 侧必须能显示「该 Total 来自 stop-drain 补齐」的显式标注(含被拒绝 closer 的 Δ);现状 Swift 只消费 Total 值本身,无来源位。
  3. 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 删除理由(三条,均有实测支撑) ​

  1. 15 s 是主机等待预算,不是设备 stop fence:STARTUP_CORE_PROFILE_MAX_WAIT_SECONDS = 15.0 表达“采集方愿意等多久”(四轮 startupCoreProfileCompletionWaitStatus = "timed-out"、ElapsedSeconds ≈ 4.0),与「设备侧生命周期在哪个 tick 结束」无因果关系。拿它当设备域上界=把主机策略当设备证据。
  2. 以「≤ 启动侧最大 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 的原因。
  3. 对负向回归零贡献:三轮的负向保护已由身份门与来源门独立给出(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 Noner1 实际取到 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 缺失或 ≠ 1T2 = 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}),不重定义 dataCompletenessT2 实测: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 反证:coverage 5 项中 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-01r3 原始字节离线重放(不改标、不合成、不 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-01S2 离线重放admittedRecordCount == 0;totalDurationMs is None;requiredMissingPhases == ["Initial Frame Rendering"];ignoredTerminalBeforeBeginCount == 1;报告中不出现任何 stop-drain token(stopDrainEndpointDiagnostics is None)
T-N-02r1 离线重放requiredMissingPhases == ["System Interface Initialization", "Initial Frame Rendering"];admittedRecordCount == 0;因 launchStartTimestampNs is None 而不得产生任何端点;若出现候选则必须记 launch-start-unknown(r1 实测候选为 0)
T-N-03r2 离线重放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_generationignoredGenerationEventCount +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乱序 tickdrain chunk 内记录 tick 逆序(模拟多 CPU 交错)排序键 (machTicks, arrivalSequence) 生效;胜出 closer 不随到达顺序改变
T-D-06超容量drain 记录数 = 512 / 513(最小扫描);并在全量扫描下探真实上界按 §15.2.2 的 (A) 或 (B) 语义断言;全量扫描用例允许失败以暴露 512 不足
T-D-07drain timeouthandoffDrainStatus == "timeout" 且在 4.0 s 上界处截断截断处无终点的记录不得被当成端点;rejectReasons["drain-truncated"] 计数;诊断块 endpointComplete = False
T-D-08live / 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
γ 无 lifecycledrain chunk 无生命周期命名空间记录(或全 bplist)stopDrainEndpointDiagnostics is None;零增量;与冻结版逐字段一致
δ 缺 launch startlaunchStartTimestampNs is Nonefail 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-1S2 / r1 / r2 的 opener 为何不在交付字节里环形覆盖 / 不同 UIKit 路径 / 采集窗口截断三候选均未排除(§2.3)保持未定;不得以「r3 能修」推断三轮也可修
U-2drain 内非目标线程的 31(r3)/ 9(S2)条生命周期记录归属未区分另一进程 / 预热侧车 / 其它 generation保持未定;身份门使其与结论无关
U-3FrameTime 两轴 ≈4.4 倍差的定性成因离线缺设备↔主机绝对桥(§7.3 E-B4)B0 只取证,不作因
U-4512 容量在全量扫描模式下的真实上界本轮仅证明「无常量可继承」+「r3 实测 31」;上界与 drain 字节数相关列为待 Lead 裁定 D2′(§15.2.2)
U-5deviceLostArg1 的真实语义源码自述 deviceLostArgSemanticsVerified = False(:1999)不得据此推断丢失记录数
U-6r1 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 = 0Sessions/<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 = 1717Sessions/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 胜出 + breaklaunch_trace.py:1862-1866
generation 门在缺值时 fail-openlaunch_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 = 0app-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

Perfowl · Performance Observer