外观
R8-Fix-5.2:CLI Post-Core Final Delivery Absolute Deadline
状态:implemented · gate-open / E2 completed / E4 pending
本批次是 R8-Fix-5.1 的续修批次, 只修复 R8-Fix-5.1 独立审计发现的最后一个 P0 缺口:
stream_candidate_session()返回之后、Python 进程退出之前的 CLI 收尾工作 仍然没有任何 deadline。不重新设计 R8-Fix-5 / R8-Fix-5.1 已通过的部分。
1. 独立审计发现的 P0
R8-Fix-5.1 把 core 内部的关闭阶段收进了单一 CloseoutDeadline, 但 MacApp/Sidecar/perfowl_collector_core_cli.py 在 core 关闭协程返回之后 仍然同步执行一段 Post-Core Final Delivery:
text
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 之前,而且当时完全不受 stop_control_received_monotonic_ns 对应的 CloseoutDeadline 约束。
1.1 独立验收给出的复现
使用真实 perfowl_collector_core_cli.run_stream():
- stdin 真实发送
stop; - fake
stream_candidate_session()等待 stop 后立即返回; report-seal.json已存在;- 仅把
finalize_live_candidate_session()换成sleep(30); - 父进程等待 3 秒。
实测结果:
text
3 秒后 Python 子进程仍然活着
wire 只输出到 stop_requested + capture_stopped
stopped 从未发出也就是说,下面这条链路在 R8-Fix-5.1 之后依旧可达:
text
Stop
-> Core closeout 正常返回(有界)
-> finalize_live_candidate_session 阻塞(无界)
-> Python process 一直 alive
-> Swift 10 秒 watchdog
-> graceful_stop_timeout -> SIGTERM / SIGKILL结论:R8-Fix-5.1 当时不能通过最终验收(status 归为 partial / blocked)。
2. 本批唯一目标
把 stream_candidate_session() 返回之后直到 Python 进程退出的全部 CLI 工作 纳入同一个 Stop → Process Exit 绝对期限:
text
Stop command
-> Core closeout
-> live delivery
-> report_sealed / launch_trace
-> stopped
-> wire terminal flush
-> wire close
-> Python process exit共享:
text
T0 = stop_control_received_monotonic_ns
deadline = T0 + COLLECTOR_CLOSEOUT_SOFT_BUDGET_SECONDS (8.0s)不得在 CLI 侧重新创建第二个 8 秒窗口:CLI 必须复用 _terminal_exit_deadline(stop_event)(锚定同一 stop timestamp), 而不是 CloseoutDeadline(time.monotonic_ns())。
3. 禁止修改
text
FPS / FrameTime / P95 FrameTime / Jitter / Refresh Rate 公式
FrameLifetime 语义
ActivityTrace Source Owner / CoreProfile Source Owner
CapturePlan / MetricRegistry
Swift gracefulTimeout = 10 秒
R8-Fix-5 的 pump = 32 events / 1ms、filter = 64 messages / 1ms禁止把宿主超时改长、禁止重新引入 historical replay 到 live Stop、 禁止提前假装 stopped、禁止删除 partial 诊断、 禁止 timeout 后继续无界同步 I/O、 禁止用普通 ThreadPoolExecutor 执行可能阻塞进程退出的收尾工作。
4. 设计
4.1 Live Delivery 成为一个受管阶段
python
def _build_live_delivery_bundle(final_output, wire_session_id, outbox=None) -> dict
async def _run_live_delivery_stage(
*, final_output, wire_session_id, deadline, ledger, executor,
) -> tuple[BoundedStageResult, dict]_build_live_delivery_bundle()把所有 Post-Core 文件操作收敛成一个同步函数: seal 存在 →finalize_live_candidate_session()+ seal payload + seal SHA256; seal 缺失 → partial foundation delivery(live-report-seal-missing); 可选读launch-trace-report.json;_run_live_delivery_stage()用既有的run_bounded_sync_stage()把整个函数作为一个live-delivery阶段跑在一个 daemon executor 上,reserve_seconds = deadline.reserve_seconds(重阶段不得花掉 terminal reserve);- 阶段状态:
completed / timed-out / failed / skipped。
4.2 可遗弃的 daemon executor(不是 to_thread + wait_for)
python
cli_closeout_executor = DaemonSingleWriterExecutor(
thread_name_prefix="perfowl-cli-closeout"
)复用 R8-Fix-5.1 的 DaemonSingleWriterExecutor:单 worker、daemon=True、 shutdown(wait=False) 遗弃而非 join。正常路径 shutdown(wait=True) (worker 空闲,立即返回);超时/失败路径 abandon_executor()。
不接受:
python
await asyncio.wait_for(asyncio.to_thread(finalize_live_candidate_session, out), timeout=t)因为 asyncio.to_thread / 默认 ThreadPoolExecutor 的 worker 是非守护线程, wait_for 只停止 await,不能停止正在执行的同步线程, 解释器退出时仍会 join 它。
4.3 outbox 增量交接
outbox 由调用方传入、被 _build_live_delivery_bundle() 逐步填充:
text
delivery 发布即写入
reportSealed 完成 seal 读取后写入
launchTrace 完成 trace 读取后写入这样即使阶段在 launcher-trace 读取处超时,调用方仍能拿到已经落盘的 delivery(stopped.deliverySHA256 不撒谎),但不会发出任何它没能完成的 terminal 记录。
4.4 timeout 后的行为
text
live delivery timeout
-> abandon_executor(cli_closeout_executor) # 禁止 shutdown(wait=True)
-> delivery = outbox.delivery 或 synthetic partial
-> 不 emit report_sealed / launch_trace
-> emit stopped(closeoutStatus="partial",
closeoutFailureStage="live-delivery",
closeoutFailureReason="partial-live-delivery-timeout")
-> wire flush(有界)
-> wire close(有界)
-> process exitstopped 优先于完整 delivery:进程及时退出 > 完整 final delivery。 异常分支同样不得 abort terminal flow。
4.5 最终裁定与职责分离
manifest["stopDiagnostics"]是 core closeout 的历史事实,不回改;- 新增
terminalCloseoutDiagnostics/terminalCloseout:coreCloseoutStatus/liveDeliveryStatus/liveDeliveryDurationMs/liveDeliveryTimeoutMs/overallCloseoutStatus/failureStage/failureReason/cliCloseoutExecutorAbandoned/cliCloseoutWorkerThreadAlive; stopped顶层新增closeoutStatus(overall)、coreCloseoutStatus、overallCloseoutStatus、closeoutFailureStage、closeoutFailureReason、terminalCloseout;closeout_exit保留 core 字段并补充同一套 overall 语义 +terminalStages账本;- 语义:
core clean + CLI live delivery timeout -> overall closeoutStatus = partial。
4.6 终端兜底与失败原因词表
text
live-delivery 阶段名
partial-live-delivery-timeout 阶段超时
partial-live-delivery-failed 阶段异常(含 seal 不可读 / 边界不一致)
live-delivery-budget-exhausted 进入阶段时重预算已耗尽(reserve 已占满)
live-report-seal-missing 无 writer seal 的 live Stop(R8-Fix-5 语义)4.7 短预算下的 reserve 比例化
_closeout_reserve_seconds(budget) = min(0.75, budget / 4): 生产 8.0s 仍解析为 min(0.75, 2.0) = 0.75(不变), 0.30s 的测试预算则得到 0.075s 的退出预留——否则 reserve 会比总预算还大, 每个重阶段都被直接 skip,短预算 fixture 会退化成空测试。
同理 _wire_close_reserve_seconds(remaining) = min(0.25, remaining * 0.4), 保证「预算快耗尽时 terminal stopped 的 flush 仍拿到真实的切片」, 而不是被固定 0.25s 地板饿成零时长检查。
4.8 附带修复:emit() 从未返回 sequence
审计期间发现 R8-Fix-5.1 的终端 fence 是空转的: emit() 的文档写着「returns its monotonic wire sequence number」, 函数体却没有 return sequence。于是 terminal_stopped_sequence = emit("stopped", …) 恒为 None, flush_until(None, …) 抛 TypeError 又被 suppress(Exception) 吞掉, wireFlushStatus 恒为 skipped、终端 flush 从未真正执行。
本批补上 return sequence(该缺陷不改变退出时间上界,但它意味着 stopped 在进程退出前没有任何有界保证到达宿主)。Test I / Test M 现在 断言 wireFlushStatus == completed 且 terminalStageStatuses["wire-flush"] 存在, 该回归不会再静默通过。
5. 资源约束
- 只允许 1 个 CLI closeout daemon worker;禁止每文件一个线程、多 executor、 busy loop、polling loop、无限 queue、额外进程;
- 时间复杂度 O(最终产物文件数)(固定 5 个以内),不扫描 Session raw;
- Live Stop 仍然禁止 historical replay(不重扫
raw.ndjson、不重放 metric pipeline、 不重建完整 report)。
6. 测试
| 编号 | 场景 | 必须断言 |
|---|---|---|
| H | 真实 subprocess + 真实 run_stream + finalize_live_candidate_session 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 仍被上报 |
| M | slow live delivery timeout | stopped 必须可见,带 closeoutFailureStage=live-delivery 与 partial-live-delivery-timeout,不能只有 capture_stopped |
| N | slow delivery + stdout wire 阻塞 | delivery / wire flush / wire close 全部有界,进程退出 < 9s |
7. E2 门禁(G1–G18)
text
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 仍为 10s8. 不接受的实现
text
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 subprocessImplementation Update · 2026-09-18 · R8-Fix-5.2 代码实施
- 关联方案/问题:R8-Fix-5.2(CLI Post-Core Final Delivery Absolute Deadline), 直接回应 R8-Fix-5.1 独立审计给出的「真实 run_stream +
finalize_live_candidate_sessionsleep(30) → 3 秒后进程仍 alive、stopped未发出」P0 复现。 - 实际修改范围与关键文件(本批只改 1 个产品文件 + 2 个测试文件):
MacApp/Sidecar/perfowl_collector_core_cli.py:- 新增常量
LIVE_DELIVERY_STAGE_NAME = "live-delivery"、CLI_CLOSEOUT_THREAD_NAME = "perfowl-cli-closeout"、CLOSEOUT_CLEAN/PARTIAL、失败原因词表 (partial-live-delivery-timeout/partial-live-delivery-failed/live-delivery-budget-exhausted/live-report-seal-missing); - 新增
_closeout_reserve_seconds()、_wire_close_reserve_seconds()(比例化 reserve,生产 8.0s 行为不变); _terminal_exit_deadline()改为传入比例化 reserve,仍然是终端路径上 唯一的 deadline 构造点;- 新增
_read_launch_trace_report()、_synthetic_partial_delivery()、_partial_live_delivery()、_build_live_delivery_bundle()、_run_live_delivery_stage(); run_stream()终端阶段重写:core 返回后创建一次CloseoutDeadline+CloseoutLedger,把全部 Post-Core 工作跑成live-delivery阶段; 正常shutdown(wait=True)/ 异常abandon_executor(); 最后计算overallCloseoutStatus,stopped增加closeoutStatus/coreCloseoutStatus/overallCloseoutStatus/closeoutFailureStage/closeoutFailureReason/terminalCloseout;finally复用同一 deadline,新增wire-flush/wire-close账本记录,closeout_exit增加 live delivery 遥测、terminalStages、cliCloseoutExecutor*与wireWriterAliveBeforeClose;- 删除 R8-Fix-5 之后已失效的死代码
emit_finalization_progress()(历史 finalizer 的 progress 回调,finalize_candidate_session已不在 live 路径上); - 附带修复:
emit()现在真的return sequence(R8-Fix-5.1 遗漏,导致终端 flush fence 空转,详见方案 §4.8)。
- 新增常量
MacApp/Sidecar/tests/test_closeout_process_exit.py: Test B/F/G 保留,新增 Test H(真实 subprocess + 真实 run_stream,短预算 0.30s,阻塞 delivery 30s)、Test H2/Test N(slow delivery + blocked wire)、 Test I/J/K/L/M(真实 run_stream 进程内,seal 正常 / 缺 seal / 慢 manifest / 慢 launch trace / timeout 仍 emit stopped)、wire fence / 短预算 reserve / 阶段名与原因词表的静态门禁。MacApp/Sidecar/tests/test_perfowl_realtime.py:test_stdin_stop_finishes_without_waiting_for_blocking_reader的终端断言 升级为「flush 必须有界执行(不再允许skipped)」并覆盖terminalCloseout的 partial 语义。
- 最终实现与方案偏差:与方案一致;两处新增(方案未预先声明、但审计必需):
emit()返回值修复(§4.8);- reserve / wire-close reserve 的比例化上限(§4.7),使短预算 fixture 不退化为空测试; 生产 8.0s 预算下
_closeout_reserve_seconds(8.0) == 0.75不变(测试固定断言)。 未修改perfowl_collector_core/下任何文件(realtime.py/realtime_ingress.py/closeout.py/single_writer_executor.py均为 R8-Fix-5.1 原样复用), 未修改任何指标公式、Source Owner、CapturePlan、MetricRegistry与 SwiftgracefulTimeout。
- 验证命令与结果:见下节 Validation Update。
- 当前状态与后续门禁:
- 状态:
implemented · gate-open;证据等级:E2 completed / E4 pending。 - 后续工作:随 Build 67 一起做真机 E4,确认用户 Stop 后
final stopped在 8s 目标内产生、closeoutStatus=clean、terminalCloseout.liveDeliveryStatus=completed、liveDeliveryDurationMs远低于 500ms、cliCloseoutExecutorAbandoned=false、writerDrainStatus=completed、ingressUndrainedAtSeal=0, 且不再出现graceful_stop_timeout与宿主 SIGTERM; 同时复验 R8-Fix-5 的 host gap 门禁。
- 状态:
Validation Update · 2026-09-18 · R8-Fix-5.2 修复后复验(本轮实测)
1. 独立 P0 复现的正向结果(Test H,真实 subprocess + 真实 run_stream)
场景:子进程启动真实 perfowl_collector_core_cli.run_stream(); stream_candidate_session 换成等待真实 stdin stop 后立即返回的 fake; report-seal.json 等产物齐备;仅把 finalize_live_candidate_session 换成 sleep(30);父进程在收到子进程 readiness 后写入真实 stop control frame 并计时到子进程真正退出。关闭预算压缩为 0.30s(仅测试 fixture, 生产常量由断言固定为 8.0s)。
text
Stop -> Process Exit = 0.3012 s (< 1s 门禁,< 9s 宿主门禁)
wire 记录顺序 = hello / frame_filter_experiment_configured /
stop_requested / capture_stopped /
stopped / closeout_exit
liveDeliveryStatus = timed-out
liveDeliveryDurationMs = 225.403 (= 0.30s - 0.075s reserve 的裁剪结果)
cliCloseoutWorkerThreadAlive = true (30s sleep 仍在跑)
cliCloseoutExecutorAbandoned = true
closeoutStatus = partial
closeoutFailureStage = live-delivery
closeoutFailureReason = partial-live-delivery-timeout
report_sealed = 未发出(未伪造)关键:慢 delivery 线程仍 alive,而 Python 进程已经退出—— 这正是 R8-Fix-5.1 缺失的那一半证明。
2. 最坏链路(Test N:slow delivery + stdout 阻塞)
text
Stop -> Process Exit = 0.3633 s
wireBlocked = true(写侧确实被从未读取的 pipe 卡住)
退出时仍存活的 perfowl 线程 = perfowl-cli-closeout(30s delivery sleep)
perfowl-core-wire-writer(阻塞的 stdout 写)
二者均为 daemon:被遗弃而非 join生产预算 8.0s 下的最坏值仍是 8.0s + 终端 teardown,Swift 10s watchdog 保持 ≥1s 余量(test_closeout_process_exit.HostWatchdogContractTests 静态门禁)。
3. 正常路径(Test I)
text
liveDeliveryStatus = completed
liveDeliveryDurationMs = 2.231 (固定 5 文件以内,O(文件数))
report_sealed = 已发出(含 sealSHA256)
launch_trace = 已发出(final=true)
foundation-delivery.json 存在;deliverySHA256 非空
overallCloseoutStatus = clean;closeoutFailureStage/reason = null
wireFlushStatus = completed(terminal fence 真正执行)
cliCloseoutExecutorAbandoned = false4. 退化路径
| 场景 | 结果 |
|---|---|
缺 report-seal.json(Test J) | 不调用 historical finalizer;quality=partial + partialReason=live-report-seal-missing;foundation-delivery.json 落盘;stopped 发出;overall partial |
缺失 seal 且 atomic_manifest sleep 30s(Test K) | 仍在 0.30s 预算内结束;liveDeliveryStatus=timed-out;partial-live-delivery-timeout;deliverySHA256=null;executor 被遗弃 |
launch-trace-report.json 读取 sleep 30s(Test L) | 阶段超时;不发出 launch_trace/report_sealed;但已落盘的 delivery 仍被上报(deliverySHA256 非空,outbox 交接生效) |
| slow delivery timeout(Test M) | stopped 仍发出并携带 partial reason;stopped["stopDiagnostics"] 保持 core 原值 {"closeoutStatus": "clean"} 未被改写 |
5. 汇总验证
text
Python 全量(共享工作区,含并行批次测试) 654 / 654 OK
R8-Fix-5.2 专项 105 / 105 OK
Swift 183 tests / 3 skipped / 0 failures
站点 知识门禁 PASS + VitePress PASS(0 错误 0 死链)
git diff --check 干净静态不变性(逐项确认):
text
Display / FrameTime / P95 / Jitter / Refresh Rate 公式 未修改
ActivityTrace / CoreProfile Source Owner 未修改
CapturePlan / MetricRegistry 未修改
Swift gracefulTimeout = runtimeKind == .core ? 10 : 2 未修改
PUMP_FAIRNESS_MAX_BURST_EVENTS = 32 / NS = 1_000_000 未修改
ACTIVITYTRACE_FILTER_MAX_BURST_MESSAGES = 64 / NS = 1e6 未修改
本批次未触碰 perfowl_collector_core/ 下任何文件6. 状态
R8-Fix-5.2 implemented / E2 completed / E4 pending —— 等待独立验收。 本批次不打 Build 67、不做真机 E4、不写 validated / E4 passed。
Validation Update · 2026-09-19 · 独立 E2 基线门禁复验与测试确定性修复
独立审计在纯净 9ed6bb4 上确认 R8-Fix-5.2 的核心 P0 已闭环,但项目级 Swift 全量最初被两个测试时序假设阻挡,均与 R8-Fix-5.2 产品实现无关:
DeviceFoundationTests.testReachableNetworkHydrationPublishesRealNameAndPersistsCache用固定80ms等待后台 Network hydration;调度稍慢时仍处于checking,造成设备名、可达状态、IdentityCache 三个断言一起失败。LiveTimelineDisplayTests.testBurstCoalescesWithoutDroppingBufferedEvents用固定100ms等待 MainActor 上的 coalesced publish;高负载时 publish 尚未调度,导致 revision / point-count 两个断言一起失败。
修复只修改测试:两处都改为“等待明确目标状态 + 1.0s 绝对上限 + 短间隔 yield”。超时仍然 fail-closed,因此没有放宽任何产品性能阈值,也没有修改 DeviceFoundation / LiveTimelineDisplay 产品代码。两个目标测试分别连续 5/5 PASS。
独立 clean worktree(9ed6bb4 + 两处 test-only patch)最终验证:
text
Swift full 182 tests / 3 skipped / 0 failures
Python 3.12 full 624 / 624 PASS
R8-Fix-5.2 real run_stream slow-delivery repro
Stop -> Process Exit = 0.3721s
stopped + closeout_exit emitted
liveDeliveryStatus = timed-out
cliCloseoutExecutorAbandoned = true
cliCloseoutWorkerThreadAlive = true
wireFlushStatus = completedPython 首次全量复跑中,R8-Fix-4 的合成 120Hz 性能门禁曾出现一次宿主调度离群:Source->Ingress max = 37.020ms(门禁 <10ms)。没有修改阈值;同一纯净 worktree 单独连续复跑该 benchmark 5/5 PASS,随后再次执行 Python 全量 624/624 PASS。该现象记录为测试宿主调度噪声,不作为产品性能结论,也不通过放宽门禁掩盖。
静态不变性继续成立:Display / FrameTime / P95 / Jitter / Refresh Rate 公式、ActivityTrace/CoreProfile Source Owner、CapturePlan、MetricRegistry、Swift Core 10s watchdog、PMD3 pump 32 events / 1ms、ActivityTrace filter 64 messages / 1ms 均未修改。
最终状态:R8-Fix-5.2 E2 独立验收通过;Build 67 可以进入构建;E4 仍 pending。
Validation Update · 2026-09-19 · Build 67 真机 E4 后的定向修复转交
Build 67 真机 E4 确认本批次的 CLI Post-Core Final Delivery 绝对期限继续有效: stopped / closeout_exit 正常发出,Stop → Process Exit 未再撞 Swift 10s watchdog, 数据完整(dataCompleteness=complete、ingressUndrained=0)。
同一轮真机留下两个后续项,均不属于本批次的 Delivery 期限逻辑,转交 R8-Fix-6:
- 残余 Host 交付间隙位置未知:一次 2878ms 的
hostReceive间隙(内含约 599ms 真实 源端呈现不连续)无法归因到七层交付路径中的任何一层,因为 Session 中只有一个末端时间戳。 R8-Fix-6 增加 diagnostics-only 的七层HostTimingChain与有界displayPipelineGapDiagnostics。 - consumer-drain 假超时:辅助 sleeper 被误计为 consumer drain,使
coreCloseoutStatus=timed-out/overallCloseoutStatus=partial,而closeoutFailureStage/closeoutFailureReason均为null,CLI 只能推导出无信息量的core-closeout-timed-out。R8-Fix-6 修正任务分类并补齐归因字段。
本节不改写上面的历史结论与数字。R8-Fix-5.2 的 E2 结论保持有效。