R8-Fix-5.2:CLI Post-Core Final Delivery Absolute Deadline 修复方案
独立审计发现的 P0
R8-Fix-5.1 只把 core 的关闭阶段收进了单一 CloseoutDeadline;perfowl_collector_core_cli.py 在 stream_candidate_session() 返回之后仍同步执行一段 Post-Core Final Delivery,且完全不受 stop_control_received_monotonic_ns 对应的 deadline 约束:
finalize_live_candidate_session(final_output) read report-seal.json / manifest.json / metrics-manifest.json / report-final.json integrity boundary validation / SHA256 write foundation-delivery.json read launch-trace-report.json emit report_sealed / emit launch_trace / emit stopped
这段工作发生在 stopped 之前。
独立验收复现(真实 run_stream)
1. stdin 真实发送 stop
2. fake stream_candidate_session() 等待 stop 后立即返回
3. report-seal.json 已存在
4. finalize_live_candidate_session() 换成 sleep(30)
5. 父进程等待 3 秒
实测:3 秒后 Python 子进程仍然活着
wire 只输出到 stop_requested + capture_stopped
stopped 从未发出
Stop -> Core closeout 正常返回 -> finalize 阻塞 -> process alive
-> Swift 10s watchdog -> graceful_stop_timeout -> SIGTERM/SIGKILL结论:R8-Fix-5.1 当时不能通过最终验收(status = partial / blocked)。
本批目标
Stop command -> Core closeout -> live delivery -> report_sealed / launch_trace -> stopped -> wire terminal flush -> wire close -> Python process exit T0 = stop_control_received_monotonic_ns deadline = T0 + 8.0s
CLI 不得重新创建第二个 8 秒窗口:必须复用 _terminal_exit_deadline(stop_event),而不是 CloseoutDeadline(time.monotonic_ns())。
修复主线
_build_live_delivery_bundle():把全部 Post-Core 文件操作收敛成一个同步函数(seal 存在 →finalize_live_candidate_session()+ seal payload + seal SHA256;seal 缺失 → partial foundation deliverylive-report-seal-missing;可选读launch-trace-report.json)。_run_live_delivery_stage():用run_bounded_sync_stage()把整个函数作为一个live-delivery阶段跑在DaemonSingleWriterExecutor(thread_name_prefix="perfowl-cli-closeout")上,reserve_seconds = deadline.reserve_seconds。- 正常
shutdown(wait=True)(worker 空闲);超时/失败abandon_executor();禁止 timeout 后shutdown(wait=True)。 - outbox 增量交接:
delivery发布即写入,因此阶段超时仍能上报已落盘的deliverySHA256,但不发出任何未能完成的 terminal 记录。 - timeout 后:abandon → 不 emit
report_sealed/launch_trace→ 仍 emitstopped(closeoutStatus=partial、closeoutFailureStage=live-delivery、closeoutFailureReason=partial-live-delivery-timeout)→ 有界 flush → 有界 close → 退出。 - 职责分离:
manifest["stopDiagnostics"](core 历史事实)不回改;新增terminalCloseout与overallCloseoutStatus;core clean + CLI delivery timeout ⇒ overall = partial。 - 附带修复:
emit()从未return sequence,导致 R8-Fix-5.1 的终端 flush fence 空转(flush_until(None)的TypeError被suppress(Exception)吞掉,wireFlushStatus恒为 skipped)。本批补上返回值。 - 比例化 reserve:
_closeout_reserve_seconds(budget) = min(0.75, budget / 4)(生产 8.0s 仍为 0.75)、_wire_close_reserve_seconds(remaining) = min(0.25, remaining * 0.4),使短预算 fixture 不退化、终端stopped的 flush 不被固定地板饿死。
禁止项
不得修改 FPS / FrameTime / P95 FrameTime / Jitter / Refresh Rate 公式、FrameLifetime 语义、ActivityTrace 与 CoreProfile Source Owner、CapturePlan、MetricRegistry、Swift 10s gracefulTimeout、R8-Fix-5 的 pump 32events/1ms 与 filter 64messages/1ms 公平门禁;不得把宿主超时改长、不得重新引入 historical replay 到 live Stop、不得提前假装 stopped、不得删除 partial 诊断、不得 timeout 后继续无界同步 I/O、不得用普通 ThreadPoolExecutor(非守护 worker)承载收尾工作、不得只测 helper 而不测真实 run_stream subprocess。
测试 H–N
| 编号 | 场景 | 必须断言 |
|---|---|---|
| H | 真实 subprocess + 真实 run_stream + finalize sleep(30),短预算 0.30s | 不等待 30s;Stop → Process Exit < 1s;证明 slow delivery 线程仍 alive 而进程已退出;stopped 仍发出且带 partial reason;未伪造 report_sealed |
| I | 正常 live delivery(seal / manifest / metrics-manifest / report-final 全有效) | liveDeliveryStatus=completed;report_sealed 发出;stopped 发出;wire flush completed;foundation-delivery.json 存在且 deliverySHA256 非空;overall clean |
| J | 缺 report-seal.json | 不进入 historical replay;quality=partial + partialReason=live-report-seal-missing;foundation-delivery.json 写入受 deadline 控制 |
| K | 缺失 seal 路径上 atomic_manifest() sleep 30s | 仍受 live-delivery 阶段 deadline 控制,不得走成另一条无界分支 |
| L | launch-trace-report.json 读取/解码很慢 | 整个 live-delivery 阶段有界;已完成 delivery 仍被上报,仅放弃 launch trace |
| M | slow live delivery timeout | stopped 必须可见并带 closeoutFailureStage=live-delivery 与 partial-live-delivery-timeout |
| N | slow delivery + stdout wire 阻塞 | delivery / wire flush / wire close 全部有界,进程退出 < 9s,两条阻塞线程均为被遗弃 daemon |
E2 门禁 G1–G18
G1 真实 run_stream slow live delivery 不等待 30s G2 slow live delivery Stop -> Process Exit < 9s G3 短预算 fixture Stop -> Process Exit < 1s G4 slow live delivery timeout 后仍 emit stopped G5 stopped 包含 partial terminal closeout reason G6 missing report seal 不进入 historical replay G7 missing seal 的 atomic_manifest 同样有界 G8 slow launch trace read 有界 G9 slow delivery + blocked wire 仍 process exit < 9s G10 正常 live delivery 正常产生 foundation-delivery.json G11 正常 report_sealed 正常产生 G12 normal path overall closeout clean G13 Python 全量 0 failures G14 Swift 0 failures G15 site PASS G16 Display 公式未变 G17 Source Owner 未变 G18 Swift core timeout 仍为 10s
不接受的实现
await asyncio.to_thread(finalize_live_candidate_session) 普通 ThreadPoolExecutor + wait_for 把 Swift 10s 改长 live delivery timeout 后 shutdown(wait=True) 重新创建新的 8 秒 CLI deadline stopped 前继续无限文件读写 恢复 historical finalizer delivery timeout 后不给 stopped delivery timeout 后仍报告 clean 只测 helper,不测真实 run_stream subprocess
E2 实现与验证结论(2026-09-18)
真实 subprocess 实测(预算压缩为 0.30s):Test H Stop → Process Exit = 0.3012s,此时被遗弃的 perfowl-cli-closeout worker 仍在 30s sleep 中(cliCloseoutWorkerThreadAlive=true、cliCloseoutExecutorAbandoned=true),stopped 与 closeout_exit 仍然发出,closeoutStatus=partial / closeoutFailureReason=partial-live-delivery-timeout;Test N(slow delivery + blocked stdout)Stop → Process Exit = 0.3633s,perfowl-core-wire-writer 与 perfowl-cli-closeout 两条线程均被遗弃为 daemon。生产 8.0s 预算下 worst case Stop → Process Exit 仍为 8.0s + 终端 teardown,保持 Swift 10s watchdog ≥ 1s 余量。
Python 全量 654/654 OK(共享工作区,含并行批次测试);R8-Fix-5.2 专项 105/105 OK;Swift 183 tests / 3 skipped / 0 failures;站点知识门禁 + VitePress 构建 PASS;git diff --check 干净。状态:implemented · gate-open / E2 completed / E4 pending。完整实现细节与字段清单见站点 devlog Canonical 文档。