外观
R8-Fix-5.1:端到端关闭绝对截止期修复方案
状态:implemented · gate-open / E2 completed / E4 pending
本方案是 R8-Fix-5 的续修批次。 R8-Fix-5 的 Pump 公平调度部分经独立验收确认有效;但同一轮独立验收同时证伪了 R8-Fix-5 的关闭预算:
COLLECTOR_CLOSEOUT_SOFT_BUDGET_SECONDS = 8.0并未覆盖 Stop → Python 进程退出的端到端路径。本批次只修关闭预算,不触碰任何指标公式。
1. P0:8 秒预算不是端到端预算
R8-Fix-5 引入的 8.0s soft budget 实际只裁剪了若干前置阶段:
text
CoreProfile graceful drain
source stop
consumer drain
ingress writer drain <- 到此为止其后的每一个阶段都仍然拥有自己的、互不相关的超时:
text
raw_ingress.stop_worker() # 内部硬编码 wait_for(..., timeout=5.0)
writer_executor.shutdown(wait=True)
recorder.finalize()
metric_recorder.finalize()
session_writer.seal()
online_report.close()
CLI final delivery
WireOutput.close(timeout=5.0) # 又一个独立 5s
Python 解释器退出把这些阶段串起来,最坏情况远不止 8 秒,而 Swift 宿主的 CollectorSupervisor.swift 依旧以 Process.isRunning 为准watchdog 10 秒, 因此 Build 66 的失败形态从未真正被修复,只是把失败点往后推。
2. 独立验收给出的决定性复现
text
SessionWriter.append_batch() 阻塞 2 秒
drain_writer(timeout=50ms) -> ~50ms partial-writer-drain-timeout
stop_worker() -> ~2015ms <- 那 50ms 什么也没限制住drain_writer 诚实地在 50ms 后返回 partial,但紧接其后的 stop_worker() 又自行等待了 2 秒(等待那个仍在同步 append 里的 writer 线程)。 结论:只要任何一个后续阶段还持有私有超时,前面的 deadline 就形同虚设。
3. 根因
3.1 预算没有向下传递
每个阶段各自 asyncio.wait_for(..., timeout=<私有常量>),没有任何单一对象 持有「Stop 时刻 + 总预算」。remaining_budget_seconds() 更进一步:
python
return max(0.05, rem_ns / 1_000_000_000.0 if rem_ns > 0 else 0.0)一旦预算耗尽,每个后续阶段还能再白拿 50ms —— 名义 8 秒、实际无界。
3.2 bounded wait 无法停止一个已经在跑的 Python 线程
python
await asyncio.wait_for(loop.run_in_executor(executor, fn), timeout=t)超时只会取消 asyncio 侧的等待,fn 仍在执行线程里。更致命的是 durable writer 当时跑在 ThreadPoolExecutor(max_workers=1) 上,其 worker 是 非守护线程:CPython 解释器退出时会 join 所有非守护线程,所以即使关闭协程 按时返回,进程依然会一直挂到那次同步写结束为止 —— 正好撞上宿主 watchdog。
3.3 正常封存链在异常关闭后被继续执行
writer 已经无法在预算内 drain 且 join 的情况下,原路径仍然继续跑 recorder.finalize() / metric_recorder.finalize() / session_writer.seal()。 被遗弃的 writer 线程可能仍在写同一个 Raw / metric / report 文件,这不仅是慢, 而是竞态。
3.4 终端输出另有一个硬编码 5 秒
WireOutput.close(timeout=5.0) 在关闭链之后又 join 了 5 秒; 8s + 5s = 13s,宿主 10s watchdog 必杀。
4. 目标与非目标
4.1 必须完成
- Stop → Python 进程退出(worst case) < 9 秒,正常路径 P95 < 3 秒;
- 所有关闭阶段共享同一个绝对 deadline;
- 为终端退出(emit stopped / wire flush / 解释器退出)预留固定 reserve;
- 被遗弃的 writer 不得阻止解释器退出;
- 关闭超时必须 fail-closed:允许 partial,但 reason 必须精确,且禁止在 异常关闭后继续执行正常封存链;
- 诊断必须能回答「哪一阶段吃掉了预算」。
4.2 明确禁止(本批次不动)
- FPS / FrameTime / P95 FrameTime / Jitter / Refresh Rate 公式;
- FrameLifetime 语义;
- ActivityTrace 与 CoreProfile 的 Source Owner;
CapturePlan/MetricRegistry;- Swift 端
gracefulTimeout = 10; - R8-Fix-5 的
pump = 32 events / 1ms与filter = 64 messages / 1ms公平门禁; - 放宽
stale-source; - 用
1000/FPS补 FrameTime; - 把 missing / stale 补成 0 / 60 / 16.67;
- 扩容 queue、删除 undrained / drop 诊断、把 partial 改写成 completed;
- 恢复历史回放进入 live Stop 路径;
reset/stash/clean/checkout当前共享脏工作区。
5. 设计:单一绝对截止期
新增 MacApp/Sidecar/perfowl_collector_core/closeout.py:
python
COLLECTOR_CLOSEOUT_SOFT_BUDGET_SECONDS = 8.0
TERMINAL_EXIT_RESERVE_SECONDS = 0.75CloseoutDeadline 在 stop control frame 被观察到的那一刻创建一次:
text
deadline_ns = stop_control_received_ns + budget并向下传给每一个阶段。关键不变量:
remaining_seconds()绝不发明时间:过期即返回精确的0.0, 不允许max(0.05, …)之类的兜底;- 重阶段(drain / finalize / seal)永远不得花掉 terminal reserve, 即只能用
heavy_remaining_seconds(); - 全部是 O(1) monotonic 运算,无轮询线程、无定时器、无后台任务。
阶段超时统一由截止期裁剪:
python
deadline.stage_timeout(requested, reserve_seconds) # 永不返回负数、永不放大
deadline.heavy_stage_timeout(requested) # 自动保留 exit reserve6. Terminal Exit Reserve
预算的最后 0.75s 属于:
text
最终 diagnostics 组装
最小 partial 状态
emit terminal `stopped`
stdout 终端 flush
从 run_stream 返回
asyncio 拆除
解释器退出这样 8.0 + 0.75 仍远小于 10s watchdog,保证宿主至少留有 1 秒余量。
7. CloseoutLedger:可审计的阶段账本
新增 CloseoutLedger,按顺序记录每个阶段:
text
name / startedNs / endedNs / durationMs / status / timeoutMs / detail对外提供:
text
summary() -> closeoutStageOrder + closeoutStages + closeoutStageStatuses
first_timeout() -> 第一个 timed-out / abandoned 阶段
status_of(name) -> 某阶段最终状态(取最后一次记录)阶段状态词表全站统一:
text
completed / skipped / timed-out / failed / forced / abandoned8. 有界同步阶段:run_bounded_sync_stage
python
async def run_bounded_sync_stage(
*, name, deadline, function, executor, args=(), kwargs=None,
requested_seconds=None, reserve_seconds=0.0, ledger=None,
skip_status=None, skip_detail=None,
) -> BoundedStageResult语义:
timeout_seconds = deadline.stage_timeout(requested, reserve);timeout_seconds <= 0→ 直接记skipped(closeout-deadline-exhausted), 不提交任何任务;- 超时 →
timed-out,future.cancel()(对已在线程中执行的任务是 no-op, 这是刻意的:我们只需要停止等待); - 异常 →
failed,错误信息写进 ledger,不向调用方抛。
关键认识:asyncio.wait_for 本身不能停住已经在跑的 Python 线程; 真正保证进程能退出的是被执行任务的守护线程属性。
9. DaemonSingleWriterExecutor
新增 single_writer_executor.py:与原先 ThreadPoolExecutor(max_workers=1) 等价的单写者语义,但:
- 一个
queue.Queue+ 一个daemon=True线程; - 仍然严格只有一个写者,Raw → metric → report 追加保序、不并发;
submit()只入队并返回真正的concurrent.futures.Future, 任务在被 worker 认领前保持 PENDING,因此cancel_futures=True仍可取消「已入队但未开始」的工作;shutdown(wait=True):正常路径,队列已排空、worker 空闲,立即返回;shutdown(wait=False):异常路径,标记abandoned,丢弃仍排队的工作, 不 join;守护 worker 被遗弃,解释器可自由退出。
abandon_executor(executor) 提供对任意 executor 的 best-effort 遗弃。
10. writer 超时的质量语义
- writer 正常 drain + worker 正常停止 →
writerStopStatus = completed, executor 正常 join; - writer 无法在剩余预算内 drain / join →
writerStopStatus = abandoned、writerExecutorAbandoned = true,Session 封为 partial, reason 精确到阶段(writer-abandoned-at-closeout-deadline/partial-writer-drain-timeout); abandoned只作为异常关闭的兜底,健康 Session 永不依赖它。
11. 禁止全量封存:Minimal Partial Closeout
一旦确认 writer 异常,正常封存链被禁止(而非仅被降速):
text
recorder.finalize() -> skipped
metric_recorder.finalize() -> skipped
session_writer.seal() -> skipped
online_report.close() -> 不执行改为:
- 保留全部诊断、accepted / persisted / undrained 计数、最后durable sequence;
- 生成只读的 minimal partial manifest:不 flush、不 hash、不 close 任何文件,
rawSHA256 = null+rawSHA256Status = "unavailable-abnormal-closeout"(mid-flight digest 绝不冒充 Session 哈希); - Session 状态
partial+dataCompleteness = "partial"; - reason 取自
_MINIMAL_CLOSEOUT_REASON_BY_STAGE:
text
writer-drain -> partial-writer-drain-timeout
writer-stop -> writer-abandoned-at-closeout-deadline
raw-finalize -> partial-raw-finalize-timeout
metric-finalize -> partial-metric-finalize-timeout
report-seal -> partial-report-seal-timeoutLive Stop 走到这里就没有 report seal,CLI 侧按既有 live-report-seal-missing 语义呈现。
12. finalize / seal 的时限控制
raw-finalize/metric-finalize/report-seal全部通过run_closeout_sync_stage跑在独立的、可遗弃的 daemon closeout executor 上;- 该 executor 与 durable writer executor 分离:writer 保持单写者不变量, closeout 的 finalize/seal 不会与半写的 Raw 追加交错;
- 任一阶段超时 →
degrade_to_minimal(stage, reason),此后不再放行任何重阶段; session_writer.seal/online_report.seal与 report close 合并为 同一个report-seal阶段(它们写同一批 report 文件)。
13. CLI 预算
perfowl_collector_core_cli.py 增加:
python
TERMINAL_WIRE_FLUSH_MAX_SECONDS = 0.50
TERMINAL_WIRE_CLOSE_MAX_SECONDS = 0.25_terminal_exit_deadline(stop_event) 用与 core 同一个stop_control_received_monotonic_ns 构造 CloseoutDeadline, 从而终端 flush、wire close 与解释器退出共享同一预算。 若从未收到 stop control(parent EOF / signal / 错误路径),预算从当前时刻起算。
14. WireOutput.close(timeout=5) 的修复
emit_line()返回单调递增 sequence;- writer 线程记录
last_flushed_sequence; - 新增
flush_until(sequence, timeout) -> bool:真正的 fence, 超时返回False并记timed-out诊断,绝不静默成功、绝不无界 join; close(*, timeout)的 timeout 由截止期裁剪(不再是固定 5 秒), 且只join(timeout);writer 线程本身是 daemon,join 不上就遗弃。
15. Terminal wire flush fence
Live Stop 终端路径记录:
text
terminalStoppedSequence
terminalStoppedEmittedMonotonicNs
processExitBeginMonotonicNs
wireFlushStatus / wireFlushDurationMs并发出有界的 closeout_exit 记录。断言不变量:
text
processExitBeginMonotonicNs >= terminalStoppedEmittedMonotonicNs
wireFlushStatus ∈ {completed, timed-out, skipped}16. stopped 已发出 ≠ closeout 已完成
「收到 stop」「发出 stopped」「进程退出」是三件不同的事。 本批次显式区分,并把它们分别记录到诊断里,避免再次用「stopped 已发出」 掩盖「进程还没退出」。
17. Event loop heartbeat 的 P50 / P95 / Max
EventLoopHeartbeatMonitor.snapshot() 补充:
text
eventLoopSchedulerDelayP50Ms / P95Ms / MaxMs
eventLoopTickGapP50Ms / P95Ms / MaxMs
eventLoopHeartbeatTickCount
eventLoopStallsOver200Ms / Over1000Ms统计量保存在有界 deque(maxlen=2048)中,长时间 Session 不会增长内存。
18. stopDiagnostics 完整性
最终 stopDiagnostics 至少包含:
text
stopControlReceivedMonotonicNs / stopObservedMonotonicNs / stopEventObservationLatencyMs
closeoutDiagnosticsPhase / closeoutBudgetMs / closeoutElapsedMs / closeoutRemainingBudgetMs
closeoutTerminalExitReserveMs / closeoutStatus / closeoutFailureStage / closeoutFailureReason
closeoutBudgetExhausted / closeoutAbnormal / closeoutMinimalPartial
sourceDrainStatus / sourceDrainDurationMs / sourceStopStatus
consumerDrainStatus / consumerDrainDurationMs / ingressCloseStatus
writerDrainStatus / writerDrainDurationMs / writerStopStatus / writerStopDurationMs
writerWorkerStatus / writerWorkerStopDurationMs / writerExecutorAbandoned
rawFinalizeStatus / rawFinalizeDurationMs / metricFinalizeStatus / metricFinalizeDurationMs
rawManifestWriteStatus / sealStatus / sealDurationMs
wireFlushStatus / wireFlushDurationMs
terminalStoppedEmittedMonotonicNs / processExitBeginMonotonicNs
ingressAcceptedAtClose / ingressPersistedAtClose / ingressUndrainedAtSeal / dataCompleteness
closeoutStageOrder / closeoutStages / closeoutStageStatuses / closeoutStageTimeoutCountnull 一律表示「尚未观测」,不得写成 0。
19. closeoutStatus 最后计算
closeoutStatus 只在所有受管阶段都已 completed / skipped / timed-out 之后 统一算一次(build_stop_diagnostics(phase="final")), 即 Section 21 的 late binding。中途写下「clean」又在后一阶段挂住, 正是让 Session 报 clean 却仍被宿主杀进程的直接原因。
20. 必须新增/修改的测试(A–G)
新增 MacApp/Sidecar/tests/test_closeout_deadline.py 与 MacApp/Sidecar/tests/test_closeout_process_exit.py。
| 编号 | 场景 | 必须断言 |
|---|---|---|
| A | append_batch() 阻塞 2s + 50ms drain 预算 | 整个 writer closeout < 300~400ms;writerWorkerStatus ∈ {abandoned, timed-out};accepted ≠ persisted;Session = partial;不得再出现 drain 50ms + stop 2s |
| B | 真实 subprocess:writer job sleep(30s) | Python 进程退出 < 1s(不受阻塞 job 影响) |
| C | 正常 writer | accepted == persisted;closeoutStatus = clean;executor 正常 join |
| D | recorder.finalize() 慢 | 有界;partial;closeoutFailureStage = raw-finalize |
| E | seal 慢 | 有界;partial;closeoutFailureStage = report-seal;report 未被 close |
| F | wire writer 阻塞 | terminal flush 有界;closeout_exit 仍发出;进程可退出 |
| G | 宿主契约 | worst-case closeout + wire close 仍留 ≥1s watchdog 余量 |
另需覆盖:deadline 算术(没有免费的 50ms)、ledger 汇总、 daemon executor 的遗弃 / 取消语义。
21. 公平性回归与效率门禁
- 不得使 R8-Fix-5 的
pump = 32 events / 1ms、filter = 64 messages / 1ms公平门禁失效:test_pmd3_subscription_fairness.py必须继续全绿; - 性能门禁必须给出变更前后原始数字(120Hz synthetic、writer stall、 CPU / 内存),不得只写「性能通过」;
- 关闭路径本身必须保持 O(1):不得引入轮询线程、定时器或 per-event 开销。
22. E2 门禁(G1–G17)
text
G1 stop → 进程退出 < 9s(worst case)/ 正常 P95 < 3s
G2 所有阶段共享同一 CloseoutDeadline(代码审计 + ledger 证据)
G3 remaining_seconds() 过期返回精确 0.0,无兜底 floor
G4 重阶段不得花费 TERMINAL_EXIT_RESERVE_SECONDS
G5 OrderedRawIngress.stop_worker() 无私有超时,且被 deadline 裁剪
G6 worker 无法 join 时被遗弃而非 join(daemon)
G7 writer 异常 → 禁止全量 seal,Session 封 partial
G8 minimal partial manifest 不含可用 rawSHA256
G9 WireOutput.close 不再硬编码 5s
G10 terminal wire flush 有 fence,超时记 timed-out
G11 stopped ≠ closeout 完成,三者分开记录
G12 heartbeat 提供 P50/P95/Max 且样本 bounded
G13 stopDiagnostics 字段完整,null 不写成 0
G14 closeoutStatus 在全部阶段结束后最后计算
G15 测试 A–G 全部存在且通过
G16 公平性测试全绿,公式 / Source Owner / CapturePlan 未被修改
G17 Python 全量、Swift 全量、站点构建、git diff --check 全部通过23. 不可接受的「假修复」
text
- 只把 stop_worker 的 5s 降到 2s,而 executor.shutdown(wait=True) 依然阻塞;
- 只在 asyncio.wait_for 超时后就宣称「同步线程已取消」;
- writer 超时后仍然继续执行完整 seal;
- 用扩大 queue / 延长 Swift 超时来掩盖关闭问题。24. 共享工作区保护
当前 worktree 存在并行未提交改动(frame_derivation.py、metric_engine.py、 online_report.py、recording.py、test_perfowl_core_cli.py、 CollectorSupervisor.swift、构建脚本等)。本批次:
- 先
git status --short; - 不 reset / 不 stash / 不 rebase;
- 不覆盖并行改动;同文件冲突先读现状再合并;
- 仅 stage 本批次确认归属的文件。
Implementation Update · 2026-09-18 · R8-Fix-5.1 代码实施与 E2 验证
- 关联方案/问题:R8-Fix-5.1(End-to-End Closeout Absolute Deadline), 直接回应 R8-Fix-5 独立验收给出的「drain 50ms + stop_worker 2s」P0 复现。
- 实际修改范围与关键文件:
- 新增
MacApp/Sidecar/perfowl_collector_core/closeout.py:COLLECTOR_CLOSEOUT_SOFT_BUDGET_SECONDS = 8.0、TERMINAL_EXIT_RESERVE_SECONDS = 0.75、阶段状态词表、CloseoutDeadline(remaining_seconds()过期精确返回0.0,无max(0.05, …)兜底)、CloseoutStage/BoundedStageResult/CloseoutLedger、run_bounded_sync_stage()。 - 新增
MacApp/Sidecar/perfowl_collector_core/single_writer_executor.py:DaemonSingleWriterExecutor(单 daemon 线程 +queue.Queue,shutdown(wait=False)遗弃而非 join)与abandon_executor()。 MacApp/Sidecar/perfowl_collector_core/realtime_ingress.py:drain_writer()/stop_worker()移除私有固定超时,改为接受deadline/reserve_seconds,由CloseoutDeadline裁剪; 新增_resolve_stage_timeout()(既无 timeout 又无 deadline 时显式报错); 新增drain_status/worker_stop_status/undrained_at_seal属性与writerDrainTimeoutMs/writerWorkerStatus/writerWorkerStopDurationMs/writerWorkerStopTimeoutMs诊断。MacApp/Sidecar/perfowl_collector_core/realtime.py: 引入CloseoutDeadline+CloseoutLedger,init_closeout_budget()只创建一次; 移除remaining_budget_seconds()的max(0.05, …)floor; 新增heavy_remaining_seconds()/closeout_stage_budget()/build_stop_diagnostics(phase)/ensure_closeout_executor()/run_closeout_sync_stage()/_seal_and_close_report(); source-drain / source-stop / consumer-drain / ingress-close / writer-drain / writer-stop / raw-finalize / metric-finalize / report-seal 全部走 ledger + deadline;writer-stop在异常路径遗弃 executor; 新增 Minimal Partial Closeout(degrade_to_minimal()、minimal_partial_manifest(),禁止进一步重阶段);closeoutStatus改为在全部阶段结束后最后计算;EventLoopHeartbeatMonitor.snapshot()增加 scheduler delay 与 tick gap 的 P50/P95/Max、tick count 与 stall 计数(有界 deque,maxlen=2048)。MacApp/Sidecar/perfowl_collector_core/recording.py:仅追加只读属性appended_event_count/appended_byte_count供 minimal partial manifest 使用 (未触碰并行改动段)。MacApp/Sidecar/perfowl_collector_core_cli.py:_WireOutput.emit_line()返回 sequence,新增last_flushed_sequence/flush_until(seq, timeout);close(*, timeout)由截止期裁剪(不再固定 5s); 新增TERMINAL_WIRE_FLUSH_MAX_SECONDS = 0.50/TERMINAL_WIRE_CLOSE_MAX_SECONDS = 0.25/_terminal_exit_deadline();run_stream终端路径记录terminalStoppedSequence/terminalStoppedEmittedMonotonicNs/processExitBeginMonotonicNs/wireFlushStatus/wireFlushDurationMs并发出有界closeout_exit记录。MacApp/Sidecar/tests/test_closeout_deadline.py(新增): Test A / C / D / E + deadline 算术、ledger、daemon executor、run_bounded_sync_stage单元门禁。MacApp/Sidecar/tests/test_closeout_process_exit.py(新增): Test B(真实 subprocess,阻塞 writersleep(30s)不阻止进程退出)、 Test F(阻塞 wire 不阻止进程退出)、Test G(宿主 watchdog 契约)。MacApp/Sidecar/tests/test_realtime_ingress.py、MacApp/Sidecar/tests/test_display_ingress_backpressure.py、MacApp/Sidecar/tests/test_perfowl_realtime.py: 适配stop_worker()的显式超时/截止期签名,并把 T8 改写为 「整个 writer closeout(drain + stop)必须落在 400ms 内」; 终端 wire 断言纳入新增的closeout_exit记录与 closeout-exit 遥测字段。
- 新增
- 最终实现与方案偏差:与方案一致,无偏离。未修改任何指标公式、 Source Owner、
CapturePlan、MetricRegistry与 SwiftgracefulTimeout; R8-Fix-5 的pump = 32 / 1ms、filter = 64 / 1ms公平门禁保持原样。 - 验证命令与结果(E2):
- Python 全量(643 项):
PYTHONPATH=. /private/tmp/perfowl-core-py312/bin/python3 -m unittest discover -s tests -p 'test_*.py'→ 见下节 Validation Update 实测结论。 - 关闭专项:
tests.test_closeout_deadline(25 项)+tests.test_closeout_process_exit(4 项) 全绿;其中 Test A 在 0.30s 硬预算下实测整个 writer closeout 远小于 400ms, Test B 在 writer 阻塞 30s 的 subprocess 中实测进程存活时间远小于 8s。 - 静态:
git diff --check干净。
- Python 全量(643 项):
- 当前状态与后续门禁:
- 状态:
implemented · gate-open;证据等级:E2 completed / E4 pending。 - 后续工作:随 Build 67 一起做真机 E4,确认用户 Stop 后
final stopped在 8s 目标内产生、closeoutStatus=clean、writerDrainStatus=completed、ingressUndrainedAtSeal=0, 且不再出现graceful_stop_timeout与宿主 SIGTERM。
- 状态:
Validation Update · 2026-09-18 · Independent audit found CLI post-core deadline gap
本节是独立验收结论的如实记录,不回改上面已经发布的 Implementation Update 历史。
- 独立 clean worktree 复跑 Python:613 / 613 PASS。 上节记录的
643是共享脏工作区数字(其中包含并行批次尚未提交的测试文件), 不作为本批次验收依据。 - Swift(独立副本):182 tests / 3 skipped / 0 failures。
- 站点:Metric Knowledge Gate PASS、VitePress PASS。
- 独立 P0 复现(真实
perfowl_collector_core_cli.run_stream()+finalize_live_candidate_session()换成sleep(30)):父进程等待 3 秒后 Python 子进程仍然 alive;wire 只输出到stop_requested+capture_stopped,stopped从未发出。 - 因此 R8-Fix-5.1 的状态更正为
partial/blocked: Core closeout 已闭环,但 Stop → Python Process EXIT 并未真正落在同一个绝对期限里 ——stream_candidate_session()返回之后的 CLI Post-Core Final Delivery 完全在 deadline 之外。本批次不得被描述为已通过验收。 - 修复由 R8-Fix-5.2 承接: R8-Fix-5.2 · CLI Post-Core Final Delivery Absolute Deadline。
- 同时更正上节的一处事实错误:终端 wire flush fence 实际未生效。
_WireOutput.emit_line()确实返回 sequence,但emit()函数体没有return sequence,于是terminal_stopped_sequence恒为None、flush_until(None, …)的TypeError被suppress(Exception)吞掉,wireFlushStatus恒为skipped—— 即stopped在进程退出前没有任何 有界保证到达宿主。该缺陷已由 R8-Fix-5.2 修复,并被 Test I / Test M 固定为门禁。
Validation Update · 2026-09-18 · R8-Fix-5.2 修复后复验(本轮)
详细方案、实现清单与字段语义见 R8-Fix-5.2 · CLI Post-Core Final Delivery Absolute Deadline。
- 修复要点:CLI 侧把 Post-Core Final Delivery 收敛为单一受管阶段
live-delivery,跑在DaemonSingleWriterExecutor(thread_name_prefix="perfowl-cli-closeout")上,复用同一个_terminal_exit_deadline(stop_event)(锚定原始stop_control_received_monotonic_ns);stopped在任何失败形态下都必须发出, 并携带closeoutStatus=partial/closeoutFailureStage=live-delivery/closeoutFailureReason;manifest["stopDiagnostics"]作为 core 历史事实不回改。 - 独立 P0 复现的正向结果(真实 subprocess + 真实
run_stream):finalize_live_candidate_session阻塞 30s、关闭预算压缩到 0.30s 时, Stop → Process Exit = 0.3012s,wire 上仍然看到stop_requested / capture_stopped / stopped / closeout_exit, 且cliCloseoutWorkerThreadAlive=true、cliCloseoutExecutorAbandoned=true—— 即「慢线程仍 alive、进程已退出」被同时证明。 最坏组合(slow delivery + stdout 阻塞):Stop → Process Exit = 0.3633s,perfowl-core-wire-writer与perfowl-cli-closeout两条阻塞线程均作为 daemon 被遗弃。 生产 8.0s 预算下的最坏值仍为 8.0s + 终端 teardown,Swift 10s watchdog 保持 ≥1s 余量。 - 测试:
test_closeout_process_exit.py扩到 15 项(Test B/F/G/H/I/J/K/L/M/N + 静态门禁),全部通过;R8-Fix-5.2 专项(test_closeout_deadline+test_closeout_process_exit+test_perfowl_realtime+test_live_seal+test_perfowl_core_cli+test_display_ingress_backpressure)105 / 105 OK。 - Python 全量:654 / 654 OK(共享工作区,含并行批次测试文件; 独立 clean worktree 数字以验收方复跑为准)。
- Swift:183 tests / 3 skipped / 0 failures;
CollectorSupervisor.swift仍为runtimeKind == .core ? 10 : 2。 - 站点:知识门禁 PASS、VitePress 构建 PASS(0 错误、0 死链)。
- 静态不变性:
git diff --check干净;本批次未修改perfowl_collector_core/下任何文件,未修改任何指标公式、Source Owner、CapturePlan、MetricRegistry、 Swift 10s、pump 32/1ms、filter 64/1ms。 - 当前状态:
implemented · gate-open;E2 completed / E4 pending。 后续:随 Build 67 做真机 E4,验证terminalCloseout.liveDeliveryStatus == "completed"、liveDeliveryDurationMs远低于 500ms、cliCloseoutExecutorAbandoned == false、overallCloseoutStatus == "clean"、无graceful_stop_timeout与宿主 SIGTERM。
Validation Update · 2026-09-19 · Build 67 真机 E4 暴露的 closeout 分类缺陷
本批次建立的单一绝对 CloseoutDeadline 与 CloseoutLedger 在 Build 67 真机 E4 上继续 成立(Stop → Process Exit 未再撞 Swift 10s watchdog)。但同一轮真机暴露了一个本批次 未覆盖的分类缺陷:
text
dataCompleteness = complete
ingressUndrained = 0
consumerDrainStatus = timed-out (≈500.824ms)
coreCloseoutStatus = timed-out
overallCloseoutStatus = partial
closeoutFailureStage = null
closeoutFailureReason = null数据 100% 完整却被判 partial。根因是 consumer drain 的 500ms 窗口把辅助 sleeper (stream-target-monitor 睡 0.75s、stream-sample-publisher 睡 ~1.0s)与真正的 Source Consumer 混在同一份 tasks 里等待——睡 750ms/1000ms 的任务在任何短于自身睡眠 的窗口里都不可能醒来。注意:这不是本批次 deadline 逻辑的缺陷,CloseoutDeadline 的 裁剪语义与 remaining_seconds() 过期精确返回 0.0 的规则均未被改动,也没有放宽任何 窗口。
由 R8-Fix-6 按 task 语义拆分 source_consumer_tasks / auxiliary_tasks 修复,并把 auxiliary-stop 登记为 advisory 阶段(超时只记录、不降级判决)。本节不改写上面的历史结论与数字。