Skip to content

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

  1. stdin 真实发送 stop;
  2. fake stream_candidate_session() 等待 stop 后立即返回;
  3. report-seal.json 已存在;
  4. 仅把 finalize_live_candidate_session() 换成 sleep(30);
  5. 父进程等待 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 exit

stopped 优先于完整 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 控制,不得走成另一条无界分支
Llaunch-trace-report.json 读取/解码很慢整个 live-delivery 阶段有界;已完成的 delivery 仍被上报
Mslow live delivery timeoutstopped 必须可见,带 closeoutFailureStage=live-delivery 与 partial-live-delivery-timeout,不能只有 capture_stopped
Nslow 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 仍为 10s

8. 不接受的实现 ​

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 subprocess

Implementation 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_session sleep(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 语义。
  • 最终实现与方案偏差:与方案一致;两处新增(方案未预先声明、但审计必需):
    1. emit() 返回值修复(§4.8);
    2. 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 与 Swift gracefulTimeout。
  • 验证命令与结果:见下节 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 = false

4. 退化路径 ​

场景结果
缺 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 产品实现无关:

  1. DeviceFoundationTests.testReachableNetworkHydrationPublishesRealNameAndPersistsCache 用固定 80ms 等待后台 Network hydration;调度稍慢时仍处于 checking,造成设备名、可达状态、IdentityCache 三个断言一起失败。
  2. 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 = completed

Python 首次全量复跑中,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:

  1. 残余 Host 交付间隙位置未知:一次 2878ms 的 hostReceive 间隙(内含约 599ms 真实 源端呈现不连续)无法归因到七层交付路径中的任何一层,因为 Session 中只有一个末端时间戳。 R8-Fix-6 增加 diagnostics-only 的七层 HostTimingChain 与有界 displayPipelineGapDiagnostics。
  2. 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 结论保持有效。

Perfowl · Performance Observer