planP4E4implemented · gate-open2026-09-20

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:

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:

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

Build 71:

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 不是无效修改,但仍未满足:

>500ms count = 0

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

2. Build 71 最关键的新证据

2.1 consumer-handoff 已真实触发

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 本身仍然是数百毫秒

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 条均成功归因:

queueWaitAttributedCount = 480
queueWaitUnattributedCount = 0

consumer waiting

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

backlogged

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

consumer not waiting

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

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

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

当前逻辑:

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:

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

实际唤醒链不是:

Pump
→ consumer

而是:

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:

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

其中 _parse() 是同步 Generator:

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 高度一致:

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

eventLoopStallsOver200Ms = 198
eventLoopStallsOver1000Ms = 0

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

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

使用当前 Pmd3Subscription,构造:

当前实现:

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

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

item = await queue.get()

结果:

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

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

改成:

parent consumer
  ↓
direct await queue.get()

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

8. 推荐实现

8.1 私有 terminal sentinel

每个 subscription 自有:

self._terminal_item = object()

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

8.2 __anext__ 快路径

保留现有:

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

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

8.3 direct queue waiter

队列为空:

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。

不得:

8.4 source done / error 的 race

必须避免:

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

asyncio 单线程下,从:

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

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

Pump finally:

self._done.set()

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

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

如果 consumer 未 parked:

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

8.5 terminal 不算业务事件

terminal:

它只是一种控制唤醒。

8.6 error 语义

Pump 保存:

self._error = error

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

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 自身。此时继续执行:

if handoff_needed:
    await asyncio.sleep(0)

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

同一 worktree 交替对照实测:

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 路径改为:

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

因此:

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

10. 诊断要求

保留:

新增可选计数:

consumerDirectWaitCount
consumerTerminalWakeCount

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

禁止增加逐事件日志。

11. 自动化测试

至少新增/修改:

  1. upstream sync-block-after-handoff regression
  1. parked consumer single event
  1. source naturally done while consumer parked
  1. source raises while consumer parked
  1. source done with backlog
  1. aclose while consumer parked
  1. aclose with backlog
  1. full queue
  1. 200000-event backlog
  1. task leak

12. 明确禁止

本批禁止:

13. E2 门禁

必须:

合成 benchmark 必须报告:

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 同条件:

Gate A 完整性

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

Gate B queue wait

硬门槛:

>500ms = 0
>1000ms = 0

目标:

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

Gate C wake→dequeue

R8-Fix-10 的核心门槛:

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

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

Gate D 其它 host layers

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

Gate E heartbeat

不得比 Build 71 恶化。

Build 71 基线:

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:

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

15. 最终判断

Build 71 已证明:

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

合成实验则证明:

同一 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 组合应为:

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.py | R8FIX9_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.py | test_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 个订阅诊断键全部保留,本批新增两个:

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

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

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 P50 | 25.919 / 26.080 / 26.093 ms | 0.160 / 0.192 / 0.176 ms | 约 140x | consumerWakeToDequeue P95 | ≈ P50(样本同值) | 0.226 / 0.236 / 0.227 ms | — | consumerWakeToDequeue Max | 26.258–26.730 ms | 0.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 让出总数 | 273 | 225 | −48 | 让出原因 | {consumer-handoff: 54, event-limit: 219} | {consumer-handoff: 0, event-limit: 225} | handoff 让出被消除,事件上界独立接管 | filter 让出 | 787(3/3 轮完全相同) | 787(3/3 轮完全相同) | filter 路径零变化 | queueWait P50 | 0.852 / 0.954 / 0.899 ms | 0.496 / 0.495 / 0.512 ms | 中位 0.899 → 0.496 ms(−45%) | queueWait P95 | 1.441 / 2.272 / 1.624 ms | 0.985 / 1.037 / 0.999 ms | 中位 1.624 → 0.999 ms(−38%) | queueWait Max | 2.788 / 3.821 / 2.568 ms | 2.035 / 2.841 / 2.042 ms | 同量级 | 吞吐 | 12795 / 10492 / 11500 ev/s | 12943 / 12098 / 13091 ev/s | 三轮配对均不低,中位 +12% | matched / consumed / drops | 7200 / 7200 / 0 | 7200 / 7200 / 0 | 一致 | CPU | 562 ms | 579 ms | 同量级 | rssDelta | 0.5 MB | 0.4 MB | 无增长 |

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

指标R8-Fix-9 基线R8-Fix-10

耗时 | 5817.8 / 6269.8 / 5757.6 ms | 3996.0 / 3927.2 / 3937.2 ms(中位 −32%) | 吞吐 | 34377 / 31899 / 34737 ev/s | 50051 / 50927 / 50798 ev/s(中位 +48%) | consumed / drops / undrainedAtClose | 200000 / 0 / 0 | 200000 / 0 / 0 | maxBurst | 32 | 32(上界未被放宽) | 让出原因 | 含 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 轮交替配对:

基线 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_fairness | 200000 拒绝洪水;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_benchmark | R8-Fix-5 时期已记录的负载敏感 flake | 基线全量运行唯一失败项(本批全量运行中该项通过) |

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

17.5 门禁之外的验证

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 公式。