外观
R8-Fix-9:Pmd3 Subscription Consumer Handoff 与队列等待修复方案
当前状态:implemented · gate-open / E2 completed / E4 pending
本方案针对 Build 70 真机最新 Session
34989ff5-8db1-4b56-99a9-a87307b2e994。目标不是回退 R8-Fix-8,也不是继续修改 FPS / FrameTime 公式;本批只处理 Pmd3Subscription enqueue → dequeue 的调度交接与 queue wait,并补齐能证明 “consumer 已等待但未及时获得调度”还是“其它事件循环任务造成全局 starvation”的证据。
下一测试包:Build 71。在真机 E4 通过之前不得标记
validated。 实际实现、与方案偏差、E2 数字与 benchmark 见文末 §16–§18。
1. Build 70 E4 事实
1.1 Session 基本事实
- Session ID:
34989ff5-8db1-4b56-99a9-a87307b2e994 - duration:约 102.463 s
- FrameLifetime delivered:680
- FrameLifetime consumed:680
- droppedEventCount:0
- undrainedAtClose:0
- ingressAcceptedAtClose:982
- ingressPersistedAtClose:982
- report seal:complete
- dataCompleteness:complete
因此,本轮不是数据丢失,而是实时交付延迟严重。
1.2 R8-Fix-8 已经生效
Build 69:
text
delivered = 768
pumpYield = 768
pumpMaxBurstCount = 1
pumpQueueWait P50/P95/Max = 157.596 / 354.888 / 563.371 msBuild 70:
text
delivered = 680
consumed = 680
pumpYield = 21
pumpMaxBurstCount = 32
pumpYieldReasonCounts = {
event-limit: 21,
active-time: 0
}
pumpMaxActiveBurstDurationMs = 0.769结论:
- R8-Fix-8 已消除“把 upstream await 墙钟时间重复计入 fairness”的错误;
- 不再出现 Build 69 的
yield ~= delivered+maxBurst=1; - Pump 自身同步工作很轻,32-event gate 正常触发;
- 不得通过恢复 wall-span time gate 来修 Build 70。
1.3 Build 70 真正失败点:subscription queue wait
text
pumpQueueWaitMs:
count = 680
P50 = 552.368 ms
P95 = 1029.745 ms
P99 = 1140.568 ms
Max = 1346.897 ms
>100ms = 639
>500ms = 378
>1000ms = 49相比 Build 69:
- P50:157.596 → 552.368 ms
- P95:354.888 → 1029.745 ms
- Max:563.371 → 1346.897 ms
即 R8-Fix-8 修正了错误 yield 原因,但真实 queue wait 进一步暴露并恶化。
1.4 七层诊断仍将瓶颈锁定在 subscription queue
Build 70:
text
tapToFilterMs:
P50 0.051
P95 1.336
filterToPumpEnqueueMs:
P50 0.011
P95 0.028
pumpQueueWaitMs:
P50 552.368
P95 1029.745
dequeueToAdapterYieldMs:
P50 0.005
P95 0.017
adapterYieldToRealtimeConsumeMs:
P50 0.115
P95 0.255
realtimeConsumeToIngressMs:
P50 0.007
P95 0.021除 queue wait 外,其余层均远低于 1~2 ms 量级。
Top gap 中绝大多数也被分类为:
text
classifiedLayer = subscription-queue因此 R8-Fix-9 的主战场必须是:
text
Pmd3Subscription._pump()
↓ enqueue
asyncio.Queue
↓
Pmd3Subscription.__anext__()
↓ dequeue2. 必须先纠正的一个诊断误读
Build 70 的 realtimeConsumerFairness.maxObservedActiveBurstMs 中,部分非 Display source 出现 0.5~1.87 s 的值,例如:
text
sysmontap ~1874 ms
graphics ~1710 ms
device-network ~1874 ms
process-network ~1334 ms
core-profile ~849 ms
display 0.671 ms这些大值不能直接证明 event loop 被同步占用 1~2 秒。
当前 RealtimeConsumerFairness.begin_item() 在事件进入 consumer 后执行,而 end_item() 在整个事件处理结束后执行;非 Display 路径中间包含:
await append_raw(raw)await append_taskawait append_decoded_async(...)asyncio.to_thread(...)- 其它异步等待
因此该字段当前更接近“单事件处理 wall span”,并非纯 synchronous active CPU time。
R8-Fix-9 禁止仅凭这些值去改 sysmontap / graphics / network 逻辑。 如需利用该诊断判断 starvation,必须先把“真正同步段”与“await wall span”分开。
3. 根因假设
3.1 主假设:Queue waiter 被唤醒,但 producer 没有立即交还执行权
当前 Pmd3Subscription.__anext__() 在队列为空时:
- 创建
self._queue.get()waiter; - await
asyncio.wait(... FIRST_COMPLETED); - 等 Pump enqueue 唤醒。
Pump 当前使用:
python
self._queue.put_nowait(item)
...
if burst_count >= 32:
await asyncio.sleep(0)
elif burst_active_ns >= 1ms:
await asyncio.sleep(0)put_nowait() 会让 Queue getter 进入 ready 状态,但不会保证 Pump 立刻失去当前执行权。
如果上游 async iterator 内部已经有 buffered message,下一次 async for item in self._source 可能很快再次返回;于是 Pump 可以继续生产, 直到 32-event gate 才显式 sleep(0)。
Build 70 的形态与这个模型高度吻合:
text
680 delivered / 21 event-limit yields ≈ 32 events per explicit yield但这是高置信度假设,不得跳过证据直接当最终根因。
3.2 次假设:全局 ready-queue / consumer orchestration starvation
即使 Pump 正确唤醒 waiter,event loop 上其它 runnable task 也可能长时间排在前面。
R8-Fix-9 必须通过新诊断区分:
- waiter 在 enqueue 时已经存在,但 wake→dequeue 延迟很大;
- enqueue 时 consumer 根本不在等待;
- queue 已形成 backlog;
- event loop heartbeat 同期是否有明显 stall。
只有证明前一种,才允许落地 consumer-handoff yield。
4. 目标
- 将
pumpQueueWait从 Build 70 的 0.5~1.3 s 量级降到低毫秒级或至少明显低于 Build 69。 - 区分:
- waiter-handoff 延迟;
- backlog 延迟;
- upstream source gap;
- 全局 event-loop stall。
- 保留 R8-Fix-8 active-time fairness,不恢复 wall-span gate。
- 保证 0 drop、0 undrained。
- 不修改 Display 指标公式、FrameLifetime 语义、Source Owner、队列容量。
- 不用“增大 queue / 增大 timeout / 降低采样频率”掩盖问题。
- Build 71 真机 E4 必须给出可直接与 Build 69/70 对照的数据。
5. Phase A:增加 consumer-handoff 证据
5.1 Pmd3Subscription 显式 consumer wait 状态
不要读取 asyncio.Queue._getters 等私有实现。
在 Pmd3Subscription 内维护自身状态:
python
self._consumer_waiting = False
self._consumer_wait_started_ns = None在 __anext__():
get_nowait()成功:直接返回,不标记 waiting;- 队列为空且即将进入 blocking wait 前:
_consumer_waiting = True- 记录 wait start;
- waiter 完成、done、cancel、异常的所有出口都必须在
finally清除 waiting 状态。
本实现当前设计为单 consumer;新增测试必须锁死该前提。若存在多 consumer 可能,必须改成 waiter count,而不是 bool。
5.2 新增 bounded diagnostics
建议至少增加:
text
consumerWaitEnterCount
consumerWakeRequestedCount
consumerWakeToDequeueCount
consumerWakeToDequeueP50Ms
consumerWakeToDequeueP95Ms
consumerWakeToDequeueMaxMs
enqueueWhileConsumerWaitingCount
enqueueWhileConsumerNotWaitingCount
queueDepthAfterEnqueueP50
queueDepthAfterEnqueueP95
queueDepthAfterEnqueueMax
queueDepthOver1Count
queueDepthOver8Count
queueDepthOver32Count要求:
- bounded;
- 不逐事件写盘;
- snapshot 时计算分位数;
- 历史 Session 缺失字段正常兼容。
5.3 HostTimingChain 增加 diagnostics-only 分类
为了把 680 条真实 FrameLifetime 的 queue wait 分组,可增加可选诊断字段:
text
subscriptionConsumerWaitingAtEnqueue
subscriptionQueueDepthAfterEnqueue要求:
- diagnostics-only;
- 不提升正式 raw schema;
- 不进入任何 FPS/FrameTime 公式;
- 旧 Session 缺字段时为 None。
然后 DisplayPipelineTiming 增加两组 queue wait 分布:
text
pumpQueueWaitWhenConsumerWaitingMs
pumpQueueWaitWhenBackloggedMs这一步将直接回答:
1s queue wait 是发生在“consumer 明明已等着但没被调度”,还是“consumer 本来就在忙/队列已有积压”。
6. Phase B:条件式 consumer handoff
仅当 Phase A 合成测试证明“waiter 被唤醒但 Pump 连续占有 ready turn”时启用。
6.1 新 yield reason
新增:
python
PUMP_YIELD_CONSUMER_HANDOFF = "consumer-handoff"诊断:
text
pumpYieldReasonCounts = {
consumer-handoff: ...,
event-limit: ...,
active-time: ...
}不得加入 wall-time。
6.2 触发条件
Pump 在 enqueue 前读取自身维护的 consumer waiting 状态:
python
handoff_needed = self._consumer_waiting
self._queue.put_nowait(item)
...
if handoff_needed:
record_yield("consumer-handoff")
await asyncio.sleep(0)
reset_burst()
elif burst_count >= 32:
...
elif burst_active_ns >= 1ms:
...优先级建议:
- consumer-handoff
- event-limit
- active-time
原因:consumer 已经明确阻塞等待当前事件时,应优先完成生产者→消费者交接。
6.3 不得用 queue.empty() 作为唯一真相
禁止仅实现:
python
if queue_was_empty:
await asyncio.sleep(0)因为 empty→non-empty 只能间接推断 waiter,不能证明 consumer 正在 await。
使用 subscription 自己维护的 waiter 状态,语义更精确,也不依赖 asyncio 私有字段。
6.4 不再把“yield 数量少”作为成功标准
Build 69 证明“错误原因导致每事件 yield”是坏的; 但如果正常 60Hz 下 consumer 每次都确实阻塞等下一帧,那么 consumer-handoff 可能合法地接近每事件一次。
因此 Build 71 不能再用:
text
pumpYieldCount << deliveredEventCount作为硬门禁。
真正门禁应是:
- yield reason 正确;
- queue wait 大幅下降;
- event-loop heartbeat 不恶化;
- throughput 不下降;
- 0 drop / 0 undrained。
7. Phase C:RealtimeConsumerFairness 诊断精度修正
此部分是诊断修正,不是性能修复。
当前 begin/end 跨越多个 await,maxObservedActiveBurstMs 名称会让人误以为是纯同步 CPU。
两种允许方案,择其一:
方案 C1:重命名并保留兼容字段
保留 legacy:
text
maxObservedActiveBurstMs但明确标记 legacy wall processing span,并新增:
text
maxObservedSynchronousBurstMs真正只包住不含 await 的同步段。
方案 C2:拆分 processing wall 与 sync work
输出:
text
maxObservedProcessingSpanMs
maxObservedSyncWorkMs历史字段继续保留,不删除。
禁止直接改变已有字段含义后继续复用同名字段。
8. Closeout:本批只补证据,不与 queue fix 混修
Build 70:
text
closeoutStatus = timed-out
closeoutFailureStage = consumer-drain
consumerDrainDurationMs = 609.382
sourceConsumerTaskCount = 8
completed = 6
cancelled = 2
timedOut = 2
writerDrainStatus = drained
ingressUndrainedAtSeal = 0
dataCompleteness = complete本轮没有记录具体 timed-out source consumer 名称。
R8-Fix-9 允许增加:
text
sourceConsumerTimedOutTaskNames
sourceConsumerCancelledTaskNames有界、固定上限。
但禁止在没有 task-name E4 证据前顺手延长 consumer-drain timeout 或修改 closeout 判定。
如果 Build 71 queue wait 修复后仍有 consumer-drain timeout,再独立开 R8-Fix-10。
9. 禁止项
本批次明确禁止:
- 恢复 Pump wall-span fairness gate;
- 恢复 Build 69 的
burst_started_ns作为调度条件; - 修改
PUMP_FAIRNESS_MAX_BURST_EVENTS = 32作为首选修法; - 修改
PUMP_FAIRNESS_MAX_BURST_NS = 1ms作为首选修法; - 扩大 subscription queue 2048;
- 丢旧 FrameLifetime 来制造低 latency;
- 降低 ActivityTrace 采集频率;
- 修改 FrameLifetime filter;
- 修改 FPS / FrameTime / P95 / Jitter / Refresh Rate 公式;
- Graphics FPS fallback;
- CoreProfile 代替 ActivityTrace FrameLifetime;
- 延长 Host timeout 掩盖 backlog;
- 用 queue.empty/private
_getters作为唯一 waiter 判定; - 因 Build 70 closeout timeout 顺手重构整个 closeout。
10. 自动化测试
10.1 Pmd3Subscription waiter handoff
必须覆盖:
consumer 已阻塞等待,producer enqueue 1 条:
- waiter 状态为 true;
- enqueue 后 handoff reason +1;
- consumer 在下一 scheduler turn 获取事件;
- queue wait 低且稳定。
consumer 未等待,producer synchronous backlog 200000:
- 仍由 32-event gate 保证公平;
- 不发生无意义 handoff yield;
- 0 drop(在 consumer 能持续 drain 的 fixture);
- heartbeat gate 通过。
正常 60/120Hz cadence:
- handoff 可以按真实 waiter 触发;
- queue wait P95 应接近 scheduler noise,而不是几百 ms;
- 不以 yield/event ratio 作为失败条件。
consumer cancel while waiting:
- waiting flag 必须清除;
- 不得留下 orphan Queue.get / Event.wait task。
source done 与 queue item 同时完成:
- 不丢最后事件;
- done / item race 行为保持原契约。
10.2 Build 70 形态回放
构造:
text
source buffered burst
consumer slower than producer scheduler turns
32-event pump gate要求旧行为能复现高 queue wait,新行为显著下降。
10.3 非 Display 源回归
至少覆盖:
- sysmontap
- graphics
- device-network
- process-network
- core-profile
确认 handoff 机制是 generic subscription 行为,不引入 Display 专用依赖。
10.4 诊断测试
锁死:
consumer-handoff/event-limit/active-timereason 互斥计数;- 无
wall-timereason; - waiter flag 所有异常/取消路径归零;
- 新分位数有界;
- 旧 Session 缺新字段可加载;
- diagnostics 不进入 canonical metric formulas。
11. E2 门禁
开发完成后至少执行:
- R8-Fix-9 专项测试全绿;
- Pmd3Subscription fairness 全量回归;
- Display pipeline timing 回归;
- realtime orchestration 回归;
- closeout 回归;
- Python 全量测试;
- Swift 全量测试;
- VitePress build;
git diff --check;- 合成 benchmark。
Benchmark 必须报告:
text
delivered / consumed / dropped
pumpYieldReasonCounts
pumpQueueWait P50/P95/P99/Max
>100ms / >500ms / >1000ms
consumerWakeToDequeue P50/P95/Max
heartbeat P95/Max
throughput
CPU
RSS delta不得只报“测试通过”。
12. Build 71 真机 E4 验收标准
测试方式:
- 与 Build 69/70 尽量同设备、同目标 App;
- 纯 Display;
- 60~90 秒;
- 正常停止;
- 不临时开启无关指标。
Gate A:数据完整性
必须:
text
droppedEventCount = 0
undrainedAtClose = 0
deliveredEventCount == consumedEventCount
dataCompleteness = completeGate B:queue wait
硬目标:
text
pumpQueueWait >500ms count = 0
pumpQueueWait >1000ms count = 0优先目标:
text
P50 < 20ms
P95 < 50ms
Max < 200ms如果尚未达到优先目标,但相比 Build 70 已数量级改善且证据明确显示剩余延迟归属于 upstream/source,可单独评估;不得把 subscription queue 的 500ms+ 延迟判通过。
Gate C:pipeline ownership
其它层不得成为新的主要瓶颈:
text
filterToPumpEnqueue P95 < 5ms
dequeueToAdapterYield P95 < 5ms
adapterYieldToRealtimeConsume P95 < 5ms
realtimeConsumeToIngress P95 < 5mstap→filter 如受 raw tap burst 影响可按 E4 实际解释,但不得掩盖 host queue wait。
Gate D:handoff 证据
必须能回答:
- 680/实际 N 条事件中,有多少 enqueue 时 consumer 正在等待;
- waiting 场景 wake→dequeue P50/P95/Max;
- backlog 场景 queue wait 分布;
- consumer-handoff yield 次数;
- event-limit / active-time 次数。
不接受“延迟变好了但为什么变好不知道”。
Gate E:event loop
Heartbeat 不得因新 handoff yield 明显退化。
Gate F:closeout
理想:
text
closeoutStatus = clean
consumerDrainStatus = completed/quiescent
writerDrainStatus = drained若仍为 consumer-drain timeout,但:
- queue wait 已通过;
- data complete;
- writer drain clean;
- timed-out task 名称已被精确记录;
则 R8-Fix-9 的 queue 方向可以单独判通过,但整体批次保持 gate-open,并立刻拆 R8-Fix-10 Closeout,不得混写成 validated-all.
13. 实施文件范围
预计重点文件:
text
MacApp/Sidecar/perfowl_collector_core/pmd3_backend.py
MacApp/Sidecar/perfowl_collector_core/realtime.py
MacApp/Sidecar/perfowl_collector_core/display_performance.py
MacApp/Sidecar/perfowl_collector_core/contracts.py # 仅如需 diagnostics-only 分类字段
MacApp/Sidecar/tests/test_pmd3_subscription_fairness.py
MacApp/Sidecar/tests/test_display_pipeline_timing.py
MacApp/Sidecar/tests/test_pump_filter_active_fairness.py
# 新增 R8-Fix-9 专项测试文件,命名按现有规范禁止把实现扩散到 Swift 指标公式或 UI 展示层。
14. 交付要求
另一 AI 完成后必须:
- 更新本 devlog,追加“实际实现 / 与方案偏差 / E2 数字 / benchmark”;
- 更新开发时间线;
- 提交代码;
- 推送当前主远端与 GitHub 镜像;
- 不自行宣称 E4;
- 不自行构建 Build 71 作为“已验收包”;
- 由独立验收重新审计 exact commit;
- 独立 E2 通过后再由验收方构建 Build 71;
- 用户完成真机测试后再关闭 E4。
15. 最终判断
R8-Fix-8 不应回退。
Build 70 已证明:
text
错误 wall-time yield 被消除
↓
真实 subscription queue scheduling 问题暴露R8-Fix-9 的正确方向不是“重新多 yield”,而是:
text
证明 consumer wait / wake / dequeue 的真实关系
↓
只在真正需要 producer→consumer handoff 时显式让出
↓
用 Build 71 真机 queue wait 证明结果成功标准只有一个:真实 FrameLifetime 在 subscription queue 中不再等待数百毫秒到 1 秒以上,同时保持 0 drop、0 undrained、event loop 公平与完整封存。
16. Implementation Update(2026-09-19)
状态:implemented · gate-open / E2 completed / E4 pending
实现在本方案被批准后于隔离 worktree(分支
r8-fix9,基线ccf1397)完成,共享脏工作区未被触碰。 本批不构建 Build 71、不执行真机 E4、不声明validated。
16.1 相对方案的偏差(一条,且是方案自身授权的条件分支)
| 方案条款 | 实际实现 | 原因 |
|---|---|---|
§6.2 触发条件 handoff_needed = self._consumer_waiting("consumer 正在等待"即让出) | 收窄为"本次 enqueue 恰好解决了那个已 park 的 getter":consumer_was_waiting and depth_before == 0 and _consumer_wake_requested_ns is None | 方案 §6.2 给出的理由原文是"consumer 已经明确阻塞等待当前事件时,应优先完成生产者→消费者交接",而 §6.3 又要求"不得用 queue.empty() 等间接信号作为唯一真相"。按字面实现"只要 consumer 在等待就让出"是每事件让出(实测把 pump 吞吐从 20000+ 打到 2431 ev/s、冻结的 R8-Fix-8 对照从 10876 → 5435 ev/s、让 1ms active-time 门槛永远攒不满、200000 同步 backlog 从 1066 → 7706ms)。收窄到"解决 park 的那一次 enqueue"后,一次 acquisition window 最多重置一次 burst,backlog 仍由 32 events / 1ms 两个上界管辖。这正是方案 §6.2 的"当前事件"含义,也是 §6.3 对精确性的要求。 |
§6.1 新增 PUMP_YIELD_CONSUMER_HANDOFF = "consumer-handoff" | 按方案实现 | 见 §16.2 |
其余条款逐条照做,无删减:§5.1 显式 wait 状态(自有 bool,不读 asyncio.Queue._getters、不用 queue.empty());§5.2 全部 bounded 诊断;§5.3 HostTimingChain 两个 diagnostics-only 注解 + DisplayPipelineTiming 三段 queue wait 拆分;§7 选 C1(保留 legacy maxObservedActiveBurstMs,新增 maxObservedSynchronousBurstMs);§8 两个有界任务名列表(未延长 0.5s drain 窗口、未改 closeout 判定);§9 全部禁止项一律未做。
16.2 关键文件与实际改动
| 文件 | 改动 |
|---|---|
MacApp/Sidecar/perfowl_collector_core/pmd3_backend.py | _consumer_waiting / _consumer_wake_in_flight 状态与全部出口清理;_mark_consumer_waiting / _clear_consumer_waiting / _take_wake_mark / _record_wake_to_dequeue / _observe_enqueue_context / _stamp_enqueue_context;__anext__ 快路径只 cancel done_task(去掉多出的一次 gather 挂起);_pump 在解决 park 的那一次 enqueue 以 PUMP_YIELD_CONSUMER_HANDOFF 优先让出;两个定长环形缓冲(4096)+ 分位数仅在 diagnostics() 计算 |
MacApp/Sidecar/perfowl_collector_core/contracts.py | HostTimingChain 新增 subscription_consumer_waiting_at_enqueue / subscription_queue_depth_after_enqueue(不进 ORDER/RECORD_KEYS)、独立 annotate()/annotated() 入口(带类型校验、first-writer-wins)、ANNOTATION_KEYS、导出 HOST_TIMING_ANNOTATION_KEYS;_host_timing_from_record 把注解当作内容 |
MacApp/Sidecar/perfowl_collector_core/display_performance.py | DISPLAY_PIPELINE_QUEUE_WAIT_GROUPS 三段拆分(consumer_waiting / backlogged / consumer_not_waiting)+ queueWaitAttributedCount / queueWaitUnattributedCount;R8FIX9_SUBSCRIPTION_DIAGNOSTIC_KEYS 19 个键 |
MacApp/Sidecar/perfowl_collector_core/realtime.py | Phase C:pause_item() / resume_item() 与 maxObservedSynchronousBurstMs(legacy 字段口径不变);§8:sourceConsumerTimedOutTaskNames / sourceConsumerCancelledTaskNames / sourceConsumerTaskNameCap |
MacApp/Sidecar/perfowl_collector_core/closeout.py | CLOSEOUT_TASK_NAME_CAP = 8 + _bounded_task_names();CloseoutTaskDrainResult 两个新字段 |
MacApp/Sidecar/tests/test_r8fix9_consumer_handoff.py | 新增,30 项(§10.1 六项 / §10.2 两项 / §10.3 两项 / §10.4 十项 / §7 五项 / §8 五项) |
MacApp/Sidecar/tests/{test_pump_filter_active_fairness,test_pmd3_subscription_fairness,test_display_pipeline_timing}.py | 契约更新:把"让出次数少"的断言换成"原因正确 + 无 wall-time + 对照未被回归"(§6.4 明确要求不再用 yield 比例当门禁),并新增 test_c2 钉死"handoff 必须有 park 的 consumer" |
测试契约更新中唯一被"加强"而非放宽的一条:原 test_c_active_work_trips_the_active_time_gate(注入 ≈0.45ms/事件必须撞响 1ms 门槛)改为在 consumer 尚未 park 时生产(先让 pump 把源跑完再消费),因此 handoff 不可能参与,active-time 仍是唯一能触发的门;并新增 test_c2 断言"consumer park 时 handoff 才 +1、且 waiting 状态不残留"。没有削弱任何公平性回归覆盖。
16.3 新增诊断字段(Build 71 的判据入口)
text
consumerWaitEnterCount consumerWakeRequestedCount
consumerWakeToDequeueCount consumerWakeToDequeueP50Ms/P95Ms/MaxMs
consumerWakeUnclaimedCount consumerWakeToDequeueSampleCount
enqueueWhileConsumerWaitingCount enqueueWhileConsumerNotWaitingCount
enqueueWhileConsumerWokenCount consumerWaitingAtSnapshot
queueDepthAfterEnqueueP50/P95/Max queueDepthSampleCount
queueDepthOver1Count/Over8Count/Over32CountHostTimingChain 每条匹配事件额外携带:
text
subscriptionConsumerWaitingAtEnqueue # bool
subscriptionQueueDepthAfterEnqueue # intDisplayPipelineTiming 由此输出三段 queue wait(pumpQueueWaitWhenConsumerWaitingMs / pumpQueueWaitWhenBackloggedMs / pumpQueueWaitWhenConsumerNotWaitingMs),并把 attributed + unattributed == 聚合 queue_wait count 做成可核对的恒等式。
17. Validation Update(2026-09-19)
17.1 E2 门禁结果
| §11 门禁 | 结果 |
|---|
- R8-Fix-9 专项测试全绿 | 30 / 30 OK(
test_r8fix9_consumer_handoff.py) | - Pmd3Subscription fairness 全量回归 | 19 / 19 OK |
- Display pipeline timing 回归 | 31 / 31 OK |
- realtime orchestration 回归 | 19 / 19 OK |
- closeout 回归 | 15 / 15 OK |
- Python 全量测试 | 747 / 747 OK(同口径基线
ccf1397为 716,+31 = 新增 30 专项 + 1test_c2) | - Swift 全量测试 | 见 §17.4 |
- VitePress build | 见 §17.4 |
git diff --check| 干净 |- 合成 benchmark | 见 §17.2 / §17.3 |
未改动的公平性上界(逐条核对):pump 32 events / 1ms、ActivityTrace filter 64 messages / 1ms、realtime consumer 8 events / 1.0ms、Pmd3Subscription 队列 2048、consumer drain 0.5s、Python 关闭 8.0s、Swift watchdog 10s、Device Conditions 20s。零 FPS / FrameTime / P95 / Jitter / Refresh-Rate 公式改动。
17.2 合成 benchmark 甲:压缩 120Hz 管线(真实桥,5 轮配对,两个 worktree 交替)
test_pump_filter_active_fairness.WallVersusActiveTimeBenchmark.test_benchmark_wall_time_versus_active_time_120hz_pipeline(120Hz × 7200 帧 + 6 噪声/帧):
| 指标 | R8-Fix-8 基线 | R8-Fix-9 | 判读 |
|---|---|---|---|
queueWait P50 | 2.07 ms | 0.95 ms | -54% |
queueWait P95 | 4.57 ms | 1.74 ms | -62% |
queueWait Max | 5.19–11.38 ms(含 50.74ms 尖峰) | 3.06–4.05 ms(无尖峰) | 尾部同时收紧 |
| 吞吐 | 11123 ev/s(中位) | 11185 ev/s | 无退化 |
heartbeat max | 1.82–3.60ms(含 50.43ms 尖峰) | 1.73–2.55ms | 无退化 |
| drops / undrained | 0 / 0 | 0 / 0 | — |
| 让出原因 | {event-limit: 225} | {consumer-handoff: 54, event-limit: 219, active-time: 0} | 新原因被真实触发且与两个上界并存 |
方法学诚实说明:该基准单次运行方差很大(首轮曾观测到 FIX9 吞吐 8597 ev/s 与基线 10113 ev/s 的"退化",5 轮配对后证明是宿主并行负载噪声——本项目工作区长期有多个 AI 并发运行)。本节数字全部来自交替配对的中位数,不以单次结果作结论。
17.3 合成 benchmark 乙:Build 70 形态(真实桥,9 轮配对)
形态:6–240 个 32-event 缓冲 burst(上游不 await,正是 Build 70 maxBurst=32 / 21 yields / 680 delivered 的形态),consumer 每事件同步工作并夹一个竞争任务。
| 指标 | R8-Fix-8 基线(中位) | R8-Fix-9(中位) | 判读 |
|---|---|---|---|
| delivered / consumed / dropped / undrained | 7680 / 7680 / 0 / 0 | 7680 / 7680 / 0 / 0 | 一致 |
queueWait P50 | 1.801 ms | 1.460 ms | -19% |
queueWait P95 | 2.190 ms | 1.801 ms | -18% |
queueWait P99 | 2.521 ms | 2.260 ms | -10% |
queueWait Max | 2.76–7.96 ms | 2.21–48.64 ms | 尾部仍有宿主尖峰(两侧都有) |
| 吞吐 | 5530.8 ev/s | 5500.4 ev/s | -0.55%(在噪声内) |
| CPU | 1246.6 ms | 1255.4 ms | +0.7% |
| RSS delta | 0.0 MB | 0.0 MB | 无增长 |
| 让出原因 | {event-limit: 240} | {consumer-handoff: 240, event-limit: 0} | 该形态下 handoff 替代了数量上界(仍为每 32 事件一次让出,公平性等价) |
新增诊断在该形态下的实测(FIX9,9/9 轮完全一致):
text
consumerWakeRequestedCount = 240 consumerWakeToDequeueCount = 240 unclaimed = 0
consumerWakeToDequeue P50 / P95 / Max = 1.74 / 2.12 / 2.48–35.01 ms
enqueueWhileConsumerWaitingCount = 7680 enqueueWhileConsumerNotWaitingCount = 0
queueDepthAfterEnqueue P50 / P95 / Max = 16 / 30 / 31
queueDepthOver8Count = 5520 queueDepthOver32Count = 0基线(ccf1397)上述字段全部为 None → 旧 Session 兼容路径被真实走过。
17.4 诚实边界:handoff 不能修"上游同步占用事件循环"的形态
方案 §10.2 要求"旧行为能复现高 queue wait,新行为显著下降"。按真机数字反推形态(32-event burst、上游每事件 17ms 同步工作、burst 跨度 ≈544ms)重构后实测(3 轮配对):
| 指标 | R8-Fix-8 基线 | R8-Fix-9 | 判读 |
|---|---|---|---|
queueWait P50 | 273.526 ms | 273.366 ms | 无变化 |
queueWait P95 | 511.820 ms | 511.739 ms | 无变化 |
queueWait Max | 529.0 ms | 529.6 ms | 无变化 |
queueWait >500ms | 12 | 12 | 无变化 |
queueWait >1000ms | 0 | 0 | — |
| 吞吐 | 53.7 ev/s | 53.7 ev/s | 无变化 |
consumerWakeToDequeue P50 | (字段不存在) | 528.920 ms | 关键读数 |
这是本批最重要的诚实结论:handoff 只能把 ready turn 还给 consumer;当上游自己用同步代码占住事件循环几十毫秒时,pump 没有任何让出手段能解决它。 而 consumerWakeToDequeue 恰好把这件事量化证明了——wake 已发出,consumer 却在 528.9ms 后才取到 item,与该形态的 queue wait 完全相等。
也就是说,R8-Fix-9 交付的是一套能区分病因的诊断外加一个只对"pump 持有 ready turn"这一病因有效的让出; Build 70 的 552ms P50 究竟属于哪一类,仍必须由 Build 71 真机 E4 判定(这正是方案 §5 设立 Phase A 的目的)。 若 Build 71 显示剩余延迟由 upstream 同步占用主导,则应开 R8-Fix-10 走上游侧(tap/filter 解码让出)方向,而不是继续在 pump 上加让出。
17.5 门禁之外的验证
- Swift 全量:见下方交付汇报(
swift test --disable-sandbox)。 - VitePress build:0 错误、0 死链。
- 旧 Session 兼容:基线产物加载后新字段为
None,不参与任何计算(§17.3 实测)。 - diagnostics-only 隔离:新增字段不属于
HostTimingChain.ORDER/RECORD_KEYS,只能经annotate()写入,stamp()对其抛ValueError(专项测试test_05/test_10钉死)。
18. 遗留项与下一阶段
| 项 | 状态 | 归属 |
|---|---|---|
| Build 70 的 552ms P50 queue wait 是否由 pump 持有 ready turn 造成 | 未判定,需真机 E4 | Build 71 真机 E4(由验收方构建) |
| 若剩余延迟由 upstream 同步占用主导 | 诊断已就位(consumerWakeToDequeue vs pumpQueueWaitWhen*) | 视 E4 结论开 R8-Fix-10(上游侧) |
Build 70 仍有的 consumerDrainStatus=timed-out | 本批只补名称,未改判定/未延长窗口 | 若 Build 71 修复后仍出现,独立开 R8-Fix-10 Closeout |
real-device maxObservedSynchronousBurstMs 对照 | 诊断已就位,等待 E4 读数 | Build 71 真机 E4 |
| 本批证据等级 | E2(代码 + 自动化测试) | 真机 E4 前不得记 validated |
下一阶段工作:
- 由独立验收审计本批 exact commit,并在隔离环境重跑 §17.1 全部 E2 门禁;
- E2 通过后由验收方构建 Build 71(本实现 AI 不构建、不声称为"已验收包");
- 用户执行 Build 71 真机 E4(同设备、同目标 App、纯 Display、60–90s、正常停止),按 §12 Gate A–F 判定;
- 用户完成真机测试后方可关闭 E4;在此之前本批次保持
implemented · gate-open。
整体进度:P0–P4 全部阶段仍处 gate-open;本批把"subscription queue 等待"从一个无法归因的 0.5–1.3s 数字推进为可判定病因的三段拆分 + 一个已验证有效的 partial 修法,但未关闭任何门禁,也未改变任何指标口径与 FPS/FrameTime 公式。