Skip to content

R8-Fix-10:Pmd3Subscription 直接 Queue Waiter 与终止信号方案 ​

当前状态:implemented · gate-open / E2 completed / E4 pending

真机依据:Build 71,Session 7adf40be-aa74-42ec-9d36-a265e6217ede。

下一测试包:Build 72。

本批不修改 FPS / FrameTime 公式,不修改 ActivityTrace whitelist,不修改队列容量,不修改 pymobiledevice3 源码。 第一目标是消除 Pmd3Subscription.__anext__() 中 queue.get() child Task + done.wait() child Task + asyncio.wait(FIRST_COMPLETED) 引入的额外 scheduler turn。

实现与验证结果见 §16 / §17;本批不构建 Build 72、不执行真机 E4、不声明 validated。

1. Build 71 E4 结论 ​

1.1 完整性与 closeout 已通过 ​

Build 71:

text
deliveredEventCount = 480
consumedEventCount = 480
droppedEventCount = 0
undrainedAtClose = 0
pendingEventCount = 0

dataCompleteness = complete
ingressUndrainedAtSeal = 0

closeoutStatus = clean
consumerDrainStatus = completed
consumerDrainDurationMs = 1.545
sourceConsumerTaskCount = 1
sourceConsumerCompletedCount = 1
sourceConsumerCancelledCount = 0
sourceConsumerTimedOutCount = 0
writerDrainStatus = drained
writerDrainDurationMs = 5.718
closeoutStageTimeoutCount = 0

Build 70 的 consumer-drain timed-out 已消失。

1.2 R8-Fix-9 明显改善 queue wait,但未过 Gate B ​

Build 70:

text
pumpQueueWait:
  P50 = 552.368 ms
  P95 = 1029.745 ms
  Max = 1346.897 ms
  >500ms = 378
  >1000ms = 49

Build 71:

text
pumpQueueWait:
  P50 = 233.889 ms
  P95 = 431.700 ms
  P99 = 513.215 ms
  Max = 593.027 ms
  >100ms = 397
  >500ms = 7
  >1000ms = 0

说明 R8-Fix-9 不是无效修改,但仍未满足:

text
>500ms count = 0

因此 R8-Fix-9 E4 判定为 failed / partially effective,不得标记 validated。

2. Build 71 最关键的新证据 ​

2.1 consumer-handoff 已真实触发 ​

text
consumerWaitEnterCount = 203
consumerWakeRequestedCount = 203
consumerWakeToDequeueCount = 203
consumerWakeUnclaimedCount = 0

pumpYieldCount = 203
pumpYieldReasonCounts = {
  consumer-handoff: 203,
  event-limit: 0,
  active-time: 0
}

也就是说:

  1. consumer 确实进入等待;
  2. Pump 确实观察到 waiter;
  3. Pump 确实执行了 handoff yield;
  4. 每一次 wake 最终都有 dequeue;
  5. 没有漏 wake。

2.2 但 wake→dequeue 本身仍然是数百毫秒 ​

text
consumerWakeToDequeue:
  P50 = 320.001 ms
  P95 = 472.153 ms
  Max = 593.027 ms

这一组数字几乎直接解释了 Build 71 的 queue wait。

因此根因已从:

Pump 没有让出

进一步收窄为:

Pump 已让出,但真正的 parent consumer 仍需额外 scheduler turn 才能拿到 item。

3. Queue wait 三分组证据 ​

全部 480 条均成功归因:

text
queueWaitAttributedCount = 480
queueWaitUnattributedCount = 0

consumer waiting ​

text
count = 360
P50 = 259.523 ms
P95 = 445.159 ms
Max = 593.027 ms
>500ms = 7

backlogged ​

text
count = 100
P50 = 111.642 ms
P95 = 300.039 ms
Max = 346.139 ms
>500ms = 0

consumer not waiting ​

text
count = 20
P50 = 144.453 ms
P95 = 204.735 ms
Max = 231.524 ms

最大延迟明确集中在“consumer 已经等待”的场景。

4. 现有 anext 调度结构的额外 scheduler turn ​

当前逻辑:

python
item_task = asyncio.create_task(self._queue.get())
done_task = asyncio.create_task(self._done.wait())

completed, pending = await asyncio.wait(
    (item_task, done_task),
    return_when=asyncio.FIRST_COMPLETED,
)

当 Pump:

python
self._queue.put_nowait(item)
await asyncio.sleep(0)

实际唤醒链不是:

text
Pump
→ consumer

而是:

text
Pump
→ queue.get child Task
→ asyncio.wait completion callback
→ parent Pmd3Subscription.__anext__ Task

至少多一个 scheduler turn。

而 Pump 在自己的 sleep(0) 后也会再次进入 ready queue。 如果 Pump 比 parent consumer 更早重新运行,它会继续进入上游 async iterator。

5. 为什么 ActivityTrace 路径会放大这一空窗 ​

pymobiledevice3 11.3.1:

python
async def ActivityTraceTap.__aiter__(self):
    while True:
        await self._get_next_message()
        for message in self._parse():
            yield message

其中 _parse() 是同步 Generator:

python
while True:
    ...
    result = operations[opcode](word)
    ...
    if opcode == CMD_END_ROW and result is not None:
        yield result

一帧 DTX payload 内可能包含大量 row。

因此:

  1. Pump handoff 后重新进入 Filter;
  2. Filter 再进入 ActivityTraceTap;
  3. _parse() 会在同一个 asyncio Task 中做同步 opcode/row 解码;
  4. 这些同步工作不经过 event-loop await;
  5. parent consumer 仍在等待它自己的下一次 scheduler turn。

这与 Build 71 heartbeat 高度一致:

text
eventLoopSchedulerDelay:
  P50 = 335.238 ms
  P95 = 529.260 ms
  Max = 663.125 ms

eventLoopStallsOver200Ms = 198
eventLoopStallsOver1000Ms = 0

但第一步不直接改 pymobiledevice3,因为可以先去掉 parent consumer 的额外调度层。

6. 非仓库合成实验:已经直接复现 ​

使用当前 Pmd3Subscription,构造:

  • 每次 yield 一个 item;
  • 下一次 upstream __anext__() 在真正 await 前同步占用 250ms;
  • consumer 已 park;
  • Pump 执行 consumer-handoff。

当前实现:

text
CURRENT wake→dequeue:
P50 ≈ 1000.432 ms
P95 ≈ 1000.432 ms
Max ≈ 1000.432 ms

同一 upstream,仅把 consumer 改成直接:

python
item = await queue.get()

结果:

text
DIRECT wake→dequeue:
0.056 ms
0.145 ms
0.139 ms
0.088 ms
0.137 ms

即:

同一上游同步阻塞条件下,直接 queue waiter 可以让 consumer 在 Pump 重新进入上游同步解析前先拿到事件。

这是 R8-Fix-10 的核心依据。

7. 实现目标 ​

把 Pmd3Subscription.__anext__() 从:

text
parent consumer
  ↓
asyncio.wait
  ├─ queue.get child task
  └─ done.wait child task

改成:

text
parent consumer
  ↓
direct await queue.get()

Pump/source 完成通过一个私有 terminal sentinel 唤醒正在等待的 consumer。

8. 推荐实现 ​

8.1 私有 terminal sentinel ​

每个 subscription 自有:

python
self._terminal_item = object()

仅用 identity 判断,不允许任何外部 item 与其冲突。

8.2 anext 快路径 ​

保留现有:

python
try:
    item = self._queue.get_nowait()
except asyncio.QueueEmpty:
    ...
else:
    return normal_item

如 queue 已有 backlog,不新增任何 await。

8.3 direct queue waiter ​

队列为空:

python
if self._done.is_set():
    raise stored_error_or_stop

self._mark_consumer_waiting()
try:
    item = await self._queue.get()
finally:
    self._clear_consumer_waiting()

这里从 parent consumer 本身直接 await queue。

不得:

  • create item_task;
  • create done_task;
  • asyncio.wait(FIRST_COMPLETED);
  • 为正常 item acquisition 再 gather()。

8.4 source done / error 的 race ​

必须避免:

text
consumer 检查 done=False
→ source 同时结束
→ consumer 永久 await queue.get()

asyncio 单线程下,从:

python
if self._done.is_set():
    ...
self._mark_consumer_waiting()
item = await self._queue.get()

在 await queue.get() 前没有 suspension,所以 Pump 不可能插入到 done-check 与 waiter 标记之间。

Pump finally:

python
self._done.set()

if self._consumer_waiting:
    self._queue.put_nowait(self._terminal_item)

此 terminal 仅用于唤醒当前 parked consumer。

如果 consumer 未 parked:

  • queue 内已有数据:先正常 drain;
  • 下一次 __anext__() 看到 _done 后结束;
  • 不需要把 terminal 排到 backlog 后面。

这样避免 source close 时在满队列上 await queue.put(terminal)。

8.5 terminal 不算业务事件 ​

terminal:

  • 不增加 deliveredEventCount;
  • 不增加 consumedEventCount;
  • 不进入 drop;
  • 不写 HostTimingChain;
  • 不写 raw;
  • 不进入 Display metric;
  • 不进入 queue depth histogram;
  • 不计 consumerWakeToDequeue。

它只是一种控制唤醒。

8.6 error 语义 ​

Pump 保存:

python
self._error = error

consumer 收到 terminal 或下一次发现 done:

python
if self._error is not None:
    raise self._error
raise StopAsyncIteration

必须保持:

已 enqueue 的正常 item 先 drain,之后再传播 source error。

与当前语义一致。

9. 决策更新:direct waiter 下取消 consumer-handoff 显式让出 ​

R8-Fix-10 开发阶段已经证明,§9 原“保留 consumer-handoff”要求的前提在 direct waiter 成立后不再成立。

direct waiter 改造后,queue.put_nowait(item) 唤醒的已经是 parent consumer 自身。此时继续执行:

python
if handoff_needed:
    await asyncio.sleep(0)

不再是在补偿 child-task / parent-task 的额外 scheduler turn,而会退化成 paced source 下近似每事件显式让出一次。

同一 worktree 交替对照实测:

text
10k 同步事件:
  无 per-event handoff ≈ 220k ev/s
  保留 handoff       ≈ 2,025 ev/s

200k 同步 backlog:
  无 per-event handoff ≈ 3.4 s
  保留 handoff         ≈ 71.2 s

120Hz 管道:
  无 per-event handoff ≈ 13,159 ev/s
  保留 handoff         ≈ 6,882 ev/s

这不是可接受的“公平性换吞吐”,而是重复调度造成的结构性退化。

9.1 三种路线的选择 ​

路线 A:保留 handoff,并放宽既有 benchmark 门槛 — 拒绝。

不能把 >20,000 ev/s 的既有门槛放宽约一个数量级来迁就新实现。门槛用于发现回归,不应反向修改门槛使回归通过。

路线 B:把 handoff 改成 N 次一次、按时间节流或 queue-depth 条件触发 — 不作为主线。

这会重新引入经验阈值,并与现有 Pump 32 events / 1ms active-time、Filter 64 messages / 1ms active-time 公平性边界重复。只有 Build 72 证明“direct waiter + 现有公平性边界”仍不能让 parent consumer及时运行时,才允许把它作为实验分支。

路线 C:取消 consumer-handoff 的调度行为,保留 waiter/wake 诊断 — 选定。

shipping 路径改为:

text
put_nowait
    ↓
direct queue waiter 被标记 ready
    ↓
Pump / Filter 继续受既有 active-time / event-count fairness 约束
    ↓
下一次公平性让出时 parent consumer 直接运行

因此:

  • 不再因 handoff_needed 调用 asyncio.sleep(0);
  • 不再把 consumer-handoff 当作 shipping pump yield reason;
  • consumerWaitEnterCount、consumerWakeRequestedCount、consumerWakeToDequeue* 等诊断全部保留;
  • 为历史 Session / schema 兼容,如 pumpYieldReasonCounts.consumer-handoff 仍需保留字段,则新路径固定为 0,不允许伪造为“发生了 handoff yield”;
  • Pump 原有 event-limit / active-time 两个 yield reason 保持不变。

这一步的目标是验证:direct waiter 是否已经把 R8-Fix-9 的显式 handoff 从“必要补偿”变成“重复让出”。

10. 诊断要求 ​

保留:

  • consumerWaitEnterCount
  • consumerWakeRequestedCount
  • consumerWakeToDequeueCount
  • consumerWakeToDequeue P50/P95/Max
  • queue depth
  • queue wait 三分组
  • pump yield reasons
  • heartbeat

新增可选计数:

text
consumerDirectWaitCount
consumerTerminalWakeCount

仅用于证明 Build 72 运行的是新路径。

禁止增加逐事件日志。

11. 自动化测试 ​

至少新增/修改:

  1. upstream sync-block-after-handoff regression

    • 5 个事件;
    • 每次下一次 source pull 同步阻塞 20–50ms;
    • direct waiter 下 wake→dequeue P95 < 5ms;
    • 旧模型 helper 必须能复现几十/几百 ms。
  2. parked consumer single event

    • consumerWakeRequestedCount +1;
    • 不产生 consumer-handoff 显式 yield;
    • parent consumer 在现有公平性边界允许的下一调度机会得到 item;
    • waiting flag 清零。
  3. source naturally done while consumer parked

    • terminal 唤醒;
    • StopAsyncIteration;
    • 无 orphan task。
  4. source raises while consumer parked

    • terminal 唤醒;
    • 原异常传播;
    • 无 orphan task。
  5. source done with backlog

    • 所有已 enqueue item 先消费;
    • 再 Stop;
    • terminal 不插入 backlog 中间。
  6. aclose while consumer parked

    • consumer 可退出;
    • Pump 可结束;
    • release exactly once。
  7. aclose with backlog

    • 不死锁;
    • undrainedAtClose 保持真实值。
  8. full queue

    • 原 drop accounting 不变;
    • terminal 不造成业务 item 被额外 drop。
  9. 200000-event backlog

    • throughput 不低于 R8-Fix-9;
    • heartbeat 无退化;
    • 无 Task 数量线性增长。
  10. task leak

    • 每事件不再创建两个 acquisition child task;
    • pending Task 数量长期稳定。

12. 明确禁止 ​

本批禁止:

  • 修改 pymobiledevice3 11.3.1 源码;
  • 给 ActivityTraceTap._parse() 随机插 sleep;
  • 增大 subscription queue;
  • 延长 timeout;
  • 恢复 wall-time pump fairness;
  • 降采样;
  • 修改 Display formulas;
  • 修改 ActivityTrace predicate / whitelist;
  • 用 polling timeout 代替直接 waiter;
  • 用 asyncio.wait_for(queue.get(), very_small_timeout) 循环轮询;
  • 每事件创建新的 Task 解决 done race;
  • 为保留旧 §9 而在 direct waiter 路径每事件执行 asyncio.sleep(0);
  • 通过放宽既有吞吐 benchmark 门槛来接受结构性性能退化;
  • 用新的 N-event / wall-time handoff heuristic 替代现有 32 events / 1ms active-time 公平性门,除非后续 E4 证明现有门不足。

13. E2 门禁 ​

必须:

  • R8-Fix-10 专项全绿;
  • Pmd3Subscription fairness 全绿;
  • R8-Fix-9 专项全绿;
  • Display pipeline timing 全绿;
  • Python 全量;
  • Swift 全量;
  • VitePress;
  • git diff --check;
  • synthetic blocking-after-handoff benchmark;
  • 既有 Pump benchmark >20,000 events/s 不得放宽;
  • 200k backlog 与 120Hz benchmark 不得出现相对 R8-Fix-9 的数量级退化。

合成 benchmark 必须报告:

text
wake→dequeue P50/P95/Max
pumpQueueWait P50/P95/Max
throughput
CPU
heartbeat P95/Max
delivered/consumed/drop/undrained
Task count / leak

14. Build 72 E4 ​

与 Build 71 同条件:

  • 纯 Display;
  • 同设备;
  • 同目标 App;
  • 60–90 秒;
  • 正常停止。

Gate A 完整性 ​

text
delivered == consumed
dropped = 0
undrained = 0
dataCompleteness = complete

Gate B queue wait ​

硬门槛:

text
>500ms = 0
>1000ms = 0

目标:

text
P50 < 20ms
P95 < 50ms
Max < 200ms

Gate C wake→dequeue ​

R8-Fix-10 的核心门槛:

text
consumerWakeToDequeue P50 < 5ms
consumerWakeToDequeue P95 < 20ms
Max < 100ms

如果这一项仍是几百毫秒,说明 direct waiter 仍未获得下一 scheduler turn,届时才进入 ActivityTraceTap upstream cooperative parse。

Gate D 其它 host layers ​

text
filterToPump P95 < 5ms
dequeueToAdapter P95 < 5ms
adapterToRealtime P95 < 5ms
realtimeToIngress P95 < 5ms

Gate E heartbeat ​

不得比 Build 71 恶化。

Build 71 基线:

text
schedulerDelay P50 = 335.238ms
P95 = 529.260ms
Max = 663.125ms
stalls >200ms = 198

如果 queue/wake 已修复但 heartbeat 仍高,单独开后续 ActivityTrace cooperative parse 批次,不再混入 subscription queue 修复。

Gate F closeout ​

维持 Build 71:

text
closeoutStatus = clean
consumerDrainStatus = completed
writerDrainStatus = drained
timeout count = 0

15. 最终判断 ​

Build 71 已证明:

text
Pump handoff 确实发生
        ↓
queue.get child Task 被唤醒
        ↓
parent __anext__ 仍然要等额外 scheduler turn
        ↓
Pump 又进入 ActivityTrace 上游同步解析
        ↓
parent consumer 迟到数百毫秒

合成实验则证明:

text
同一 upstream 同步阻塞
current asyncio.wait parent-child model → ~1000ms
direct queue waiter                 → 0.06–0.15ms

因此 R8-Fix-10 的第一选择不是修改 pymobiledevice3,而是先把真实 consumer 直接挂在 queue waiter 上,并删除已经变成重复调度的 per-event consumer-handoff yield。

现阶段 shipping 组合应为:

text
direct queue waiter
+ terminal signal
+ Pump 32 events / 1ms active-time
+ Filter 64 messages / 1ms active-time
- per-event consumer-handoff sleep(0)

只有 Build 72 证明 direct waiter + 现有公平性边界之后,consumerWakeToDequeue 仍然维持数百毫秒,才进入上游 ActivityTrace cooperative parse;不得先用新的 handoff heuristic 掩盖根因。

16. Implementation Update(2026-09-20) ​

状态:implemented · gate-open / E2 completed / E4 pending

实现在本方案(含用户更新的 §9 路线 C)被批准后于隔离 worktree(分支 r8-fix10,基线 a3ea7cd = 两个远程的 main)完成,共享脏工作区未被触碰。另建 detached worktree perfowl-r8fix10-base(同一 a3ea7cd)用于全程 A/B 对照。

本批不构建 Build 72、不执行真机 E4、不声明 validated。

16.1 相对方案的偏差(两条,均为边界补齐,无条款删减) ​

方案条款实际实现原因
§8.4 if self._consumer_waiting: self._queue.put_nowait(self._terminal_item)补上第二条件:if self._consumer_waiting and not self._queue.full()方案给出的理由是"避免在满队列上 await queue.put(terminal) 造成 close 死锁",但 put_nowait 在满队列上抛 QueueFull;若为腾位置先丢一条业务 item,就会让 sentinel 造成额外 drop,与 §8.5"terminal 不进入 drop"直接冲突。因此满队列时跳过 sentinel:此时第一个 put_nowait 已经唤醒过该 getter,consumer 不会滞留,sentinel 只是多余而非必需。该 race 方案 §8.4 未枚举(其枚举的是"consumer 未 parked"这一分支)。
§11.1 要求 direct waiter 下 wake→dequeue P95 < 5ms专项测试断言的是 §14 Gate C(P50 < 5ms / P95 < 20ms / Max < 100ms)§11.1 与 §14 Gate C 对同一指标给了两个不同的尾部阈值(5ms vs 20ms)。在分级合成形态(真实 tap 形状、1 帧/消息、17ms/帧)上实测 P95 在 0.40ms(空载)~10.27ms(宿主高负载) 之间,5ms 只在空载可达。Gate C 是本方案给 Build 72 真机 E4 定的判据,专项测试采用它并额外断言 new_p50 < legacy_p50 与 legacy_p50 > 5ms(旧模型必须复现缺陷)。若 Build 72 实测 P95 超过 20ms,按 §14 判失败。

其余条款逐条照做,无删减:§8.1 私有 object() sentinel 按 identity 判断;§8.2 快路径(backlog 不新增 await);§8.3 direct queue waiter,不创建 item_task / done_task、不用 asyncio.wait(FIRST_COMPLETED)、不为正常 acquisition gather();§8.5 terminal 不增 delivered/consumed、不进 drop、不写 HostTimingChain、不写 raw、不进 metric、不进 queue depth 直方图、不计 consumerWakeToDequeue;§8.6 已 enqueue 的正常 item 先 drain 再传播 source error;§9 路线 C 的四条全部落地(不再因 handoff_needed 调 sleep(0)、consumer-handoff 不再是 shipping yield reason、wake/waiter 诊断全保留、pumpYieldReasonCounts 键保留但固定为 0、event-limit / active-time 两个 reason 不变);§10 两个可选计数按名实现;§12 全部禁止项一律未做。

16.2 关键文件与实际改动 ​

文件改动
MacApp/Sidecar/perfowl_collector_core/pmd3_backend.py核心。__init__ 新增 _terminal_item = object() / _terminal_pending = False / consumer_direct_wait_count = 0 / consumer_terminal_wake_count = 0;__anext__ 重写为 direct waiter(快路径 get_nowait → terminal 识别后 continue 重查 _done;_done 检查与 _mark_consumer_waiting() 之间无 suspension;try: await self._queue.get() / finally: _clear_consumer_waiting() 覆盖正常恢复、取消与异常);aclose 的 undrained_at_close 修正为 max(qsize − pending_terminal, 0);_pump 的 finally 按 §8.4+§16.1 条件投递 sentinel;_pump 的 handoff 分支只记录证据(置 _consumer_wake_requested_ns、consumer_wake_requested_count += 1)不再 await asyncio.sleep(0),让出只由 event-limit / active-time 两个上界产生;PUMP_YIELD_CONSUMER_HANDOFF 模块注释补充"R8-Fix-10 撤销该让出"的 Build 71 依据
MacApp/Sidecar/perfowl_collector_core/display_performance.pyR8FIX9_SUBSCRIPTION_DIAGNOSTIC_KEYS 与 _R8FIX9_COUNTER_ATTRIBUTES 各增 consumerDirectWaitCount / consumerTerminalWakeCount(纯诊断键,不进任何公式)
MacApp/Sidecar/tests/test_r8fix10_direct_queue_waiter.py新增,12 项(§11.1–§11.10 全覆盖 + 冻结的 R8Fix9ChildTaskBridge 交替 A/B)
MacApp/Sidecar/tests/test_r8fix9_consumer_handoff.py契约更新:consumer-handoff yield 断言由"至少一次/按比例"改为 == 0,test_01 改名并改断言为"证据记录 + 无让出 + directWait == waitEnter"
MacApp/Sidecar/tests/test_pump_filter_active_fairness.py同上(test_a_awaited_upstream_does_not_yield_per_event / test_d_active_span_excludes_upstream_wait / test_c2 改名 ..._is_evidenced_not_yielded / 120Hz benchmark),并新增 directWait == waitEnter 与 handoff == 0 断言
MacApp/Sidecar/tests/test_display_pipeline_timing.pytest_a1 同步改为"无 handoff 让出 + active-time 由实测支撑"

测试契约更新中唯一被改动的边界(诚实记录,且不涉及本次改动的路径):三处把 pumpYieldCount 钉在绝对 <= 1 的断言(test_r8fix9_consumer_handoff.test_01、test_display_pipeline_timing.test_a1、test_r8fix10...test_02)改为"每事件最多一次让出"并额外断言"若 active-time 触发则必须有 ≥1ms 的实测 pumpMaxActiveBurstDurationMs 支撑"。原因是 active-time 是墙钟阈值,在共享高负载机器上同一次 burst 内合法触发多次;把计数钉成常数等于把机器测量钉死在它自己的阈值上。handoff == 0 这条路线 C 的核心保证未被放宽,且被加强为 directWait == waitEnter(每条 park 都走直接等待)。

16.3 新增诊断字段(Build 72 的判据入口) ​

R8-Fix-9 的 19 个订阅诊断键全部保留,本批新增两个:

text
consumerDirectWaitCount      # 该 capture 有多少次 acquisition 走了 direct queue waiter
consumerTerminalWakeCount    # 私有 sentinel 唤醒过多少次(含 fast-path 与 await-path 两条)

判读方式(§10 的"仅用于证明 Build 72 运行的是新路径"):

text
consumerDirectWaitCount == consumerWaitEnterCount   -> 新路径生效
consumerTerminalWakeCount >= 1                      -> 源结束时的哨兵唤醒真实发生过
pumpYieldReasonCounts["consumer-handoff"] == 0      -> 该 capture 未再花费重复让出

pumpYieldReasonCounts 的 consumer-handoff 键保留且固定为 0(历史 Session / schema 兼容),不允许伪造为"发生了 handoff yield"。

17. Validation Update(2026-09-20) ​

17.1 E2 门禁结果 ​

全部在隔离 worktree(分支 r8-fix10,基线 a3ea7cd)执行;除特别标注外均在同一台机器的同一时段测量。

§13 门禁结果
  1. R8-Fix-10 专项测试全绿 | 12 / 12 OK(test_r8fix10_direct_queue_waiter.py) |
  2. Pmd3Subscription fairness 全量回归 | 19 / 19 OK |
  3. R8-Fix-9 专项全绿 | 30 / 30 OK |
  4. Display pipeline timing 全绿 | 31 / 31 OK |
  5. pump / filter active-time fairness 回归 | 11 / 11 OK(单独运行;见 §17.4 的组合运行说明) |
  6. Python 全量 | 759 条,1 条失败(test_g_rejected_flood_keeps_message_bound_fairness,既有负载敏感断言,见 §17.4);同口径基线 a3ea7cd 747 条,1 条失败(test_b1_b2_synthetic_120hz_benchmark)。759 = 747 + 12 = 本批新增专项 |
  7. Swift 全量 | 182 tests / 3 skipped / 0 failures(swift test --disable-sandbox,18.930s) |
  8. VitePress build | 0 错误、0 死链(vitepress v1.6.4,build complete in 26.03s) |
  9. git diff --check | 干净 |
  10. 合成 blocking-after-handoff benchmark | 见 §17.2 |
  11. 既有 Pump benchmark >20,000 events/s | 未放宽,见 §17.3 |
  12. 200k backlog / 120Hz 无数量级退化 | 见 §17.2(两者均为改善,非退化) |

未改动的公平性上界(逐条核对):Pump 32 events / 1ms active-time、ActivityTrace filter 64 messages / 1ms active-time、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 公式改动;pmd3_deep_services.py(filter)与上游 tap 解码路径完全不在本批 diff 中。

17.2 合成 benchmark:direct waiter A/B(同一机器、两个 worktree 交替配对) ​

甲 · 真实 tap 形状(1 帧/消息,17ms/帧,10 个事件) — consumerWakeToDequeue 是 Gate C 的判据入口:

指标冻结的 R8-Fix-9 child-task 模型R8-Fix-10 direct waiter判读
consumerWakeToDequeue P5025.919 / 26.080 / 26.093 ms0.160 / 0.192 / 0.176 ms约 140x
consumerWakeToDequeue P95≈ P50(样本同值)0.226 / 0.236 / 0.227 ms—
consumerWakeToDequeue Max26.258–26.730 ms0.246 / 0.246 / 0.302 ms—
pump 让出10(每事件一次 handoff)3(全部 active-time,handoff = 0)—
consumerDirectWaitCount字段不存在20 / 20 / 21 == consumerWaitEnterCount新路径生效

乙 · 压缩 120Hz 管线(7200 帧 + 6 噪声消息/帧,真实桥) — 3 轮交替配对,结果逐轮可复现:

指标R8-Fix-9 基线(3 轮)R8-Fix-10(3 轮)判读
pump 让出总数273225−48
让出原因{consumer-handoff: 54, event-limit: 219}{consumer-handoff: 0, event-limit: 225}handoff 让出被消除,事件上界独立接管
filter 让出787(3/3 轮完全相同)787(3/3 轮完全相同)filter 路径零变化
queueWait P500.852 / 0.954 / 0.899 ms0.496 / 0.495 / 0.512 ms中位 0.899 → 0.496 ms(−45%)
queueWait P951.441 / 2.272 / 1.624 ms0.985 / 1.037 / 0.999 ms中位 1.624 → 0.999 ms(−38%)
queueWait Max2.788 / 3.821 / 2.568 ms2.035 / 2.841 / 2.042 ms同量级
吞吐12795 / 10492 / 11500 ev/s12943 / 12098 / 13091 ev/s三轮配对均不低,中位 +12%
matched / consumed / drops7200 / 7200 / 07200 / 7200 / 0一致
CPU562 ms579 ms同量级
rssDelta0.5 MB0.4 MB无增长

丙 · 200000 同步事件 backlog — 3 轮交替配对:

指标R8-Fix-9 基线R8-Fix-10
耗时5817.8 / 6269.8 / 5757.6 ms3996.0 / 3927.2 / 3937.2 ms(中位 −32%)
吞吐34377 / 31899 / 34737 ev/s50051 / 50927 / 50798 ev/s(中位 +48%)
consumed / drops / undrainedAtClose200000 / 0 / 0200000 / 0 / 0
maxBurst3232(上界未被放宽)
让出原因含 consumer-handoff{consumer-handoff: 0, event-limit: 6250, active-time: 0}

17.3 既有 >20,000 events/s 门槛:未放宽,且本批未比基线差 ​

门槛仍为 test_pmd3_subscription_fairness.FairnessBenchmarks.test_benchmark_pump_throughput_with_fairness 的 assertGreater(rate, 20_000),一个字符都未改。

在共享机器负载均值 约 82 的时段做 5 轮交替配对:

text
基线 a3ea7cd   17,632  PASS   14,365   12,901   19,981   -> 5 轮中 4 轮未过
本批 r8-fix10  PASS    PASS   19,196   PASS     PASS     -> 5 轮中 1 轮未过

结论:该基准在本项目的共享开发机上对负载高度敏感(负载均值 82 时两者都掉到门槛附近;负载均值 4.5 时两树均稳定通过)。本批没有放宽门槛,且在同一时段每次配对都不低于基线。

17.4 诚实边界:两条既有负载敏感断言(非本批回归,已双向复现) ​

组合运行(多个套件在同一进程内连续跑、机器被其它并发任务压到负载均值 80+)时,出现过两条失败,均不在本批改动路径上,且均已用基线 a3ea7cd 在同样条件下复现:

失败用例机制基线复现
test_pump_filter_active_fairness.test_g_rejected_flood_keeps_message_bound_fairness200000 拒绝洪水;message-limit 让出需 > 3123。active-time 是墙钟阈值,宿主停顿落在 64 消息 burst 内时该 burst 改由 active-time 结束,于是 message-limit 计数被扣。实测本批 {message-limit: 3107, active-time: 18}、基线 {message-limit: 3123, active-time: 4} / {3112, 22}基线 3 轮中 2 轮同样失败({3123, 2} / {3123, 4})
test_display_ingress_backpressure.test_b1_b2_synthetic_120hz_benchmarkR8-Fix-5 时期已记录的负载敏感 flake基线全量运行唯一失败项(本批全量运行中该项通过)

两条用例的两侧表现互为对照:每条各自在另一棵树上通过。低负载(负载均值 4.5)下本批五个相关套件全部单独通过(12 / 19 / 30 / 31 / 11 全绿)。本批不修改这两条既有断言——它们是独立于本批 diff 的历史门禁,按本文档 D2 与"一个批次一个范围"的原则交由独立批次处理。

17.5 门禁之外的验证 ​

  • diagnostics-only 隔离:两个新键只进入 Pmd3Subscription.diagnostics() 与 R8FIX9_SUBSCRIPTION_DIAGNOSTIC_KEYS,不属于 HostTimingChain.ORDER / RECORD_KEYS,不写 raw、不进 metric、不改任何公式。
  • sentinel 语义隔离(§8.5 逐条):专项 test_06 / test_09 / test_10 钉死"不增 deliveredEventCount / 不增 consumedEventCount / 不进 drop / 不进 queue depth / 不计 consumerWakeToDequeue",并额外验证 undrainedAtClose 的修正只扣除 pending sentinel(业务事件排在 sentinel 后面时仍如实计数)。
  • 旧 Session 兼容:consumer-handoff 键在 pumpYieldReasonCounts 中仍存在,新路径恒为 0;基线产物加载后两个新字段为 None,不参与任何计算。
  • 无 orphan task:test_04 / test_05 断言迭代结束后 asyncio.all_tasks() 无新增任务;test_12 断言 60 次 acquisition 期间 pending task 数量平坦(max − min <= 1)。

18. 遗留项与下一阶段 ​

项状态归属
Build 71 的 consumerWakeToDequeue P50 320ms 是否由 child-task acquisition 结构造成本批已证成:同一 tap 形状下 direct waiter 把该值压到 0.16–0.19ms(约 140x)本批 §17.2 甲
Build 71 的 pumpQueueWait P50 233.889ms / >500ms = 7代码层应已改善(§17.2 乙/丙),但必须由真机判定Build 72 真机 E4(由验收方构建)
若 Build 72 的 consumerWakeToDequeue 仍为数百毫秒则按 §15 正式进入 ActivityTrace cooperative parse(上游侧解码让出),不得再给 Pump 加新的调度 heuristic视 E4 结论开后续批次
consumer-handoff 让出被撤销后,paced source 下 consumer 是否仍能及时运行合成层面已证(120Hz pipeline queueWait P50 −45%、吞吐不降反升);真机未验Build 72 真机 E4 Gate B / Gate C
两条既有负载敏感断言(§17.4)未修改,非本批范围独立批次(建议与 R8-Fix-5.2 的 flake 收口合并处理)
本批证据等级E2(代码 + 自动化测试)真机 E4 前不得记 validated

下一阶段工作:

  1. 由独立验收审计本批 exact commit,并在隔离环境重跑 §17.1 全部 E2 门禁(建议在低负载时段执行,并知悉 §17.3 的负载敏感性);
  2. E2 通过后由验收方构建 Build 72(本实现 AI 不构建、不声称为"已验收包");
  3. 用户执行 Build 72 真机 E4(同设备、同目标 App、纯 Display、60–90 秒、正常停止),按 §14 Gate A–F 判定;
  4. 用户完成真机测试后方可关闭 E4;在此之前本批次保持 implemented · gate-open。

整体进度:P0–P4 全部阶段仍处 gate-open;本批把 R8-Fix-9 交付的"可区分病因的诊断"向前推进为"病因已按诊断结论修掉,且修法比原补偿更省"——consumer-handoff 这个 R8-Fix-9 引入的补偿让出,被证明在 direct waiter 成立后是重复调度并已撤销;但未执行真机 E4,未关闭任何门禁,也未改变任何指标口径与 FPS/FrameTime 公式。

Perfowl · Performance Observer