外观
R8-Fix-4:Display Source Drain 与持久化链路解耦修复方案
本文是 Build 65 真机失败后的下一轮实现方案。执行者可以直接按本文开发,不需要重新从 ActivityTrace 是否产数据、FrameTime 公式、Graphics FPS 或 stdout wire 重新排查。
当前批准边界:允许按本方案修改产品代码、测试与开发记录;不允许改变 Display Performance 指标公式、Source Owner 或无侵入边界。
本轮目标不是“让 UI 偶尔出现几个值”,而是恢复一条可以持续承载高频 FrameLifetime 的采集链:Source 必须被快速排空;实时投影不能等待磁盘;持久化可以稍后追上,但必须有界、有序、可审计、不可静默丢 Raw。
1. Build 65 决定性证据
最新失败 Session:
text
997d98e5-c970-4814-9571-736f1b95daafBuild provenance:
text
perfowlBuildNumber = 65
perfowlGitCommit = 4fbc3ff-dirty
collectorCoreSHA256 = 35404d66680d9d3066342bea97a37da36366d09c4a42d4cc3d04cc60fc6301c5ActivityTrace Acquisition Census:
| 指标 | Build 65 |
|---|---|
| totalMessageCount | 4114 |
| frameLifetimeMatchedCount | 576 |
| frameLifetimeRejectedCount | 3538 |
| rawCallbackRateHz | 92.0661 Hz |
| deliveredEventCount | 576 |
| consumedEventCount | 3 |
| undrainedAtClose | 573 |
| pendingEventCount | 573 |
| droppedEventCount | 0 |
| streamEnded | false |
Wire 输出:
text
wireDroppedSampleCount = 0
wireMaxBufferedCount = 2
wireMaxBufferedBytes = 22191最终指标:
text
FPS: 0 / 13 有效
FrameTime: 0 / 13 有效被真正消费的两条 Display Raw 给出了关键时序证据:
text
source presentation delta ≈ 16.645 ms
host consumer delta ≈ 15.819 s即:下一帧早已存在于 subscription queue 中,但 consumer 15.8 秒后才回来取。
同时 Build 65 整轮约 48 秒只有约 78 条 Raw 进入最终 writer,其中 ActivityTrace 仅 2 条。这已经把问题从“Source 是否产帧”收敛到“Source consumer 为什么长期不继续取队列”。
2. 对上一轮 stdout 假设的修正
Build 64 后曾提出:
text
emit
→ print(..., flush=True)
→ stdout pipe back-pressure
→ asyncio loop stallBuild 65 已把 wire 写入移到独立 _WireOutput thread,但:
text
wire backlog 最大只有 2 条 / 22 KB
wireDroppedSampleCount = 0
ActivityTrace 仍 576 → 3结论:
stdout 同步写是一个真实的架构风险,但不是当前 FPS / FrameTime 缺失的主因。
_WireOutput 可以保留作为防御性改造,但代码注释、devlog 与后续结论不得继续把它写成“已确认根因”。
3. 当前最高置信度根因
当前 realtime.py 中,高频 Display consumer 的关键路径是:
text
Pmd3Subscription.__anext__()
↓
ActivityTraceFrameLifetimeRawAdapter.raw_events()
↓
consume(adapter)
↓
await append_raw(raw)
↓
writer_async_lock
↓
single-thread writer_executor
↓
SessionWriter.append()
├── RawSessionRecorder.append()
├── IncrementalMetricRecorder.append()
├── OnlineReportSession.append()
└── PeriodicCheckpoint.after_append()
↓
ActivityTrace 还要 append_decoded_async()
↓
再次 writer_async_lock
↓
OnlineReportSession.append_decoded()
↓
projector.consume()
↓
才会请求下一条 FrameLifetime所以 Source Drain 被以下工作串行耦合:
- Raw JSON / blob 持久化;
- Metric 派生;
- Online Report 更新;
- report snapshot / atomic manifest;
- Periodic checkpoint / fsync;
- decoded frame 持久化;
- 其他 source 对同一 writer lock 的竞争;
- executor 调度与磁盘抖动。
截至 Build 65,尚未把 15.8 秒明确归因到上述某一个单独函数,因此实现时不要再猜“就是 fsync”“就是 SQLite”“就是 snapshot”。正确修复是从架构上保证:无论哪一个下游步骤偶尔慢,Source Drain 都不会跟着停。
4. 唯一目标架构
最终结构必须拆成三层:
text
Layer A · Source Drain
ActivityTrace / sysmontap / graphics / network / ...
只做:取事件 → 最小语义处理 → ingress 入队
↓
Layer B · Realtime Projection
Display FrameLifetime
→ FPS / FrameTime / P95 / Refresh Rate / Jitter
不等待 Raw persistence / report / fsync
↓
Layer C · Ordered Durable Writer
全局有序 ingress queue
→ batch writer
→ Raw → Metric → Report → Checkpoint核心不变量:
text
Source Drain latency ≠ Durable Writer latency
Realtime Display latency ≠ Durable Writer latency即使一次 checkpoint / atomic rename / disk flush 需要 500ms,ActivityTrace consumer 仍应继续快速取走后续 FrameLifetime。
5. 推荐实现:全局 Ordered Ingress Queue
不要只给 ActivityTrace 单独再造一套旁路 writer,否则会破坏当前 Session 的全局 Raw 顺序。
建议新增:
text
MacApp/Sidecar/perfowl_collector_core/realtime_ingress.py建议核心结构:
python
@dataclass(slots=True)
class IngressItem:
ingress_sequence: int
raw: RawEventEnvelope
live: bool
enqueued_monotonic_ns: int
approx_bytes: int
wait_for_result: bool
completion: Future | None核心对象可命名为 OrderedRawIngress / RealtimeIngressQueue / OrderedSessionIngress,名称任选,但职责必须唯一。
5.1 为什么必须是全局队列
当前多个 source 最终共用:
text
RawSessionRecorder
SessionWriter
global raw sequence如果 Display 自己用一个 writer,而 sysmontap / network / CoreProfile 继续直接写,会重新产生:
- 顺序竞争;
- 双 owner;
- sequence 不确定;
- lock 复杂化;
- stop / seal race。
所以要求:
所有运行时 Raw append 最终都经过同一个 Ordered Ingress → single durable writer。
不同 producer 只在“是否等待 completion”上不同。
6. Display Fast Path
ActivityTrace 是本轮关键。
当前:
python
raw = await subscription.__anext__()
normalized, derived = await append_raw(raw)
projector.consume(normalized, derived)
# 然后才取下一帧目标:
python
raw = await subscription.__anext__()
# 立即更新实时 Display 状态
projector.consume(raw)
# 只做快速入队
ingress.enqueue_nowait(
raw,
live=True,
wait_for_result=False,
)
await asyncio.sleep(0)fast path 允许做
- subscription.anext;
- ActivityTrace adapter 已有 FrameLifetime decode;
- generation / identity / sourceEpoch 读取;
- DisplayPerformanceProjector.consume(raw);
- IngressItem 创建;
- queue.put_nowait;
- O(1) 计数器。
fast path 禁止做
- RawSessionRecorder.append;
- SessionWriter.append;
- OnlineReportSession.append;
- append_decoded;
- SQLite;
- JSON report snapshot;
- atomic manifest;
- flush / fsync;
- executor result await;
- Swift wire 写;
- checkpoint。
目标:
text
source → ingress p95 < 2 ms
source → ingress max < 10 ms(普通小 FrameLifetime)7. Realtime Display Projection 前移,但禁止双消费
RealtimeMetricProjector.consume 已经能直接识别 display.frame-lifetime.raw,并调用 DisplayPerformanceProjector。
因此不需要重写 FPS / FrameTime 算法,只需要把 Display 的 projector.consume(raw) 前移到 durable writer 之前。
旧路径 append 完成后还有 projector.consume(normalized, derived_frames)。修改后 display.frame-lifetime.raw 只能投影一次。
必须测试:
text
1 个 FrameLifetime
→ projector consume count = 1
连续两帧 16.6ms
→ 只形成 1 个 FrameTime interval不能双计数 FPS,也不能生成两个 FrameTime。
8. Durable Writer:单 owner + 批处理
仍然只有一个 durable writer:
text
Ordered Ingress Queue
↓
writer worker
↓
SessionWriter不要增加第二个 RawSessionRecorder,不要让多个线程直接调用 SessionWriter.append。
推荐 batch 参数:
text
BATCH_MAX_RECORDS = 64
BATCH_MAX_BYTES = 512 KiB
BATCH_MAX_WAIT = 10 msworker 行为:
text
await first item
→ get_nowait 尽量凑批
→ 满 64 / 512KiB / 10ms 任一条件就提交
→ 单次 executor hop 顺序处理 batch
→ resolve completion futuresCoreProfile 大 blob 可以独立成为一个 batch。
这一步的目的主要是降低 executor submit、asyncio lock 切换与 thread handoff 成本,不允许改变 Raw 顺序。
9. SessionWriter 改造
文件:
text
MacApp/Sidecar/perfowl_collector_core/session_writer.py建议新增 append_batch。
第一版可以内部继续逐条调用 append,先解决 Source consumer 被 await 的结构问题,不要一上来重写 RawSessionRecorder 文件格式。
10. Display decoded frame 持久化必须合并
当前 ActivityTrace 每条 Raw 会经历:
text
append_raw
→ append_decoded_async即同一帧两次争抢 writer lock / executor。
SessionWriter.append 已经支持 decoded_outputs,因此本轮应合并:
- writer worker 取到 Display ingress item;
- 根据 normalized/raw payload 构造 frame row;
- 在同一次 ordered append transaction 中通过 decoded_outputs 写入 OnlineReport;
- 删除 Display realtime consumer 的第二次 append_decoded_async。
要求:
text
frame-events-live.jsonl.sourceRawSequence
==
最终 normalized Raw global sequence不能使用 adapter local sequence。
11. 其他 Source 的迁移策略
为降低本轮风险:
Display / ActivityTrace
text
enqueue → 不等待 completionsysmontap / graphics / network / device-condition
可以先:
text
enqueue → await completion保持原语义。
CoreProfile / Startup
保持:
text
enqueue → await completion因为 Startup decoder / rotation / handoff 依赖同步返回的 normalized Raw、derived frames、writer sequence 与状态。
本轮不要顺带重构 Startup。
但所有 producer 都必须先经过同一个 ingress queue,最终 Raw 顺序只能有一个 owner。
12. Queue 必须有界,Raw 绝不静默丢
推荐初始上限:
text
MAX_INGRESS_RECORDS = 8192
MAX_INGRESS_BYTES = 32 MiBoverflow 行为
Raw 和 wire sample 不同:
text
wire sample 可以丢旧值
Raw 事件不能静默丢如果达到 hard limit:
- 不覆盖 oldest Raw;
- 不 drop newest Raw 后继续装作 complete;
- 设置 capture partial/fatal;
- 原因固定为 writer-backpressure-overflow;
- 触发受控 stop;
- 最终 report dataCompleteness 不能是 complete。
这是 fail-closed。
单条 CoreProfile chunk 如果自身超过 byte limit,可在 queue 为空时允许一个 oversized item,并记录 oversizedIngressItemCount;不得同时堆多个 oversized item。
13. 核心回归:慢 writer 不能再阻塞 Source Drain
必须新增测试:
text
writer 第一次 append 人为 sleep 1.0s
ActivityTrace producer 同时送 200~500 条预期:
text
delivered == consumed
ingressAccepted == consumed
durablePersisted 在 sleep 期间暂时落后
event loop heartbeat 正常
subscription pending ≈ 0
ingress queue depth 上升
writer 恢复后 queue 排空这才是本轮命中根因的核心 E2 测试。
14. Shutdown 顺序必须同时修
正确顺序:
text
1. capture_closing = true
2. 停止/关闭各 Source,使 producer 不再产生新 Raw
3. 等 source consumer 完成最后一次 enqueue
4. close ingress for producers
5. durable writer drain ingress queue
6. 确认 persisted == accepted
7. writer executor shutdown
8. recorder/report finalize
9. seal
10. stopped禁止:
text
先 shutdown writer
→ 再 stop source正常 stop 要求:
text
ingressAcceptedCount == ingressPersistedCount
ingressUndrainedAtSeal == 0若 writer drain 超时,必须:
text
status = partial
reason = partial-writer-drain-timeout不得 seal 成 complete。
推荐 WRITER_DRAIN_TIMEOUT 为 5~10 秒,具体结合现有 finalizer 总超时。
15. Build 66 新增诊断字段
必须能一眼判断:Source 是否慢、Ingress 是否堆、Writer 是否慢、Checkpoint 是否慢。
Source → Ingress
text
displaySourceDeliveredCount
displaySourceConsumedCount
displayIngressAcceptedCount
displaySourceToIngressP50Ms
displaySourceToIngressP95Ms
displaySourceToIngressMaxMsIngress Queue
text
ingressCurrentDepth
ingressHighWaterCount
ingressCurrentBytes
ingressHighWaterBytes
ingressOverflowCount
oversizedIngressItemCountDurable Writer
text
ingressPersistedCount
writerBatchCount
writerMaxBatchCount
writerMaxBatchBytes
writerBatchDurationP50Ms
writerBatchDurationP95Ms
writerBatchDurationMaxMs
writerQueueWaitP50Ms
writerQueueWaitP95Ms
writerQueueWaitMaxMsDurability
已有 PeriodicCheckpoint.maxDurationMs 应进入最终诊断:
text
checkpointCompletedCount
checkpointMaxDurationMs
checkpointLastTriggerStop
text
ingressAcceptedAtClose
ingressPersistedAtClose
ingressUndrainedAtSeal
writerDrainDurationMs
writerDrainStatus以上全部 diagnostic-only,不得改变指标公式。
16. Build 66 必须修正文档中的错误根因结论
site/devlog/p4-r8-display-performance-gap-classification-result.md 里已有 R8-Fix-3 将 stdout 写阻塞记为根因。
Build 65 已证伪它是本问题主因。
不要删除或改写历史数字;按历史不变性原则追加:
text
Validation Update · Build 65
R8-Fix-3 stdout hypothesis REFUTED as primary cause
_WireOutput retained as defensive improvement
New primary direction: Source Drain ↔ Durable Writer couplingperfowl_collector_core_cli.py 内 _WireOutput 的说明也要从“root cause”改为“defensive wire decoupling”,避免后续 AI 被误导。
17. 预计修改文件
必改
text
MacApp/Sidecar/perfowl_collector_core/realtime.py
MacApp/Sidecar/perfowl_collector_core/session_writer.py
MacApp/Sidecar/tests/...
site/devlog/p4-r8-display-performance-gap-classification-result.md
site/devlog/p4-r8-fix4-display-ingress-decoupling-plan.md
site/devlog/index.md(实施完成后)推荐新增
text
MacApp/Sidecar/perfowl_collector_core/realtime_ingress.py
MacApp/Sidecar/tests/test_realtime_ingress.py
MacApp/Sidecar/tests/test_display_ingress_backpressure.py可能修改
text
MacApp/Sidecar/perfowl_collector_core/online_report.py
MacApp/Sidecar/perfowl_collector_core/checkpoint_policy.py
MacApp/Sidecar/perfowl_collector_core_cli.py原则上不改
text
display_performance.py 的指标公式
metric_engine.py 的 Display 公式
Registry 的 Source Owner
knowledge 中 FPS / FrameTime 口径如果执行者发现必须改公式才能“让值出现”,应立即停止,这代表偏离本方案。
18. 必须新增的测试
T1 高频 drain 不等待慢 writer
500 FrameLifetime + writer 首次阻塞 1 秒。
断言 consumed=500、accepted=500、heartbeat 不中断、最终 persisted=500。
T2 Global order
交错:
text
Display A1
sysmontap S1
Display A2
network N1
Display A3最终 global Raw sequence 必须等于 ingress sequence。
T3 Display 不双投影
1 个 FrameLifetime 只 consume 一次;两帧只生成一个 interval。
T4 decoded 与 Raw sequence 绑定
frame-events-live.jsonl.sourceRawSequence 必须对应 normalized Raw sequence。
T5 writer backlog 可恢复
writer 慢 500ms,queue high-water > 0,overflow=0,最终 undrained=0。
T6 overflow fail-closed
极小 queue + 永久慢 writer:禁止 silent drop,capture=partial,reason=writer-backpressure-overflow。
T7 shutdown drain
stop 时 queue 中有 100 条,最终 persisted==accepted,seal complete。
T8 shutdown timeout
writer 永久挂住,必须 bounded timeout、partial,不 deadlock、不 complete seal。
T9 Startup 回归
CoreProfile 继续 enqueue + await completion;App Launch Total 与 lifecycle 行为不退化。
T10 _WireOutput 回归
slow stdout 不冻结 event loop,control record 不丢,sample 可 coalesce;但测试注释不要再叫 Display root cause。
19. E2 性能基准
B1 Display-only 120Hz
text
duration = 60s
events = 7200门禁:
text
accepted = 7200
persisted = 7200
overflow = 0
source→ingress p95 < 2ms
source→ingress max < 10ms
writer sustained throughput >= 240 small events/s即至少具备理论 120Hz 的 2x 余量。
B2 Burst
1000 FrameLifetime 快速注入,writer 暂停 500ms。
要求 Source drain 不停,heartbeat 正常,恢复后 queue 收敛到 0。
B3 混合 source
text
120Hz Display
+ sysmontap
+ graphics
+ network要求 Display consumed/delivered >= 99%,其他 source 不出现明显 starvation。
B4 内存
记录 ingressHighWaterCount、ingressHighWaterBytes、RSS delta;60 秒正常测试不应持续线性增长。
20. Build 66 真机 E4 验收
实现 AI 完成后只能声明:
text
implemented · gate-open
E2 completed
E4 pending不能自行宣布 FPS / FrameTime 已修复。
第一场:Display-only
30~60 秒持续滑动/动画。
硬门禁:
text
frameLifetimeMatchedCount >= 100
consumedEventCount / deliveredEventCount >= 99%
undrainedAtClose <= 1
droppedEventCount = 0
displayIngressAcceptedCount ≈ consumedEventCount
ingressUndrainedAtSeal = 0指标:
text
FPS 连续有值
FrameTime 连续有值
P95 FrameTime warmup 后有值
Jitter warmup 后有值
Refresh Rate 有值不能再出现 576 delivered / 3 consumed 的形态。
第二场:Startup + Display
验证 App Launch Total 仍可获得、Startup phase 语义不退化、Startup 完成后 FPS / FrameTime 连续。
第三场:静态 → 动态 → 静态
text
静止 10s
滑动 20s
静止 10s验证 idle/stale 语义诚实,重新动态后能立即恢复,不会永久 missing。
21. 明确禁止继续尝试的方向
- 不得继续扩大 Pmd3Subscription queue 当成修复。Build 65 已证明 backlog 很大但 drop=0。
- 不得把 FPS 改回 Graphics gauge。
- 不得用 CoreProfile 恢复 FrameTime。
- 不得修改 FrameTime 公式。
- 不得把 missing 补成 0、60、16.67。
- 不得静默 drop Raw FrameLifetime。
- 不得继续把 stdout wire 写成已确认主因。
- 不得为了吞吐引入第二个 RawSessionRecorder / 第二个 durable owner。
- 不得破坏 Startup 当前同步依赖链。
22. 实施顺序
Step 1
新增 ingress 基础设施,先用 synthetic test 验证 FIFO / bounds / close。
Step 2
把所有 Raw append 收口到 ingress;此时 producer 先全部 enqueue + await completion,保证功能等价。
Step 3
仅 ActivityTrace 切成 no-wait fast path,并前移 realtime projector。先跑 T1/T3。
Step 4
合并 Display decoded persistence,删除第二次 append_decoded round-trip。跑 T4。
Step 5
加入 writer batch。跑 B1/B2。
Step 6
修正 stop / seal 顺序。跑 T7/T8。
Step 7
补 ingress / writer / checkpoint diagnostics。
Step 8
最后才跑专项、全量、站点构建与 benchmark。
23. 实现完成后的验证命令
使用项目 Python 3.12,不要误用 Xcode/macOS Python 3.9。
当前本机环境:
bash
/private/tmp/perfowl-core-py312/bin/python3专项:
bash
cd MacApp/Sidecar
PYTHONPATH=. /private/tmp/perfowl-core-py312/bin/python3 -m unittest -v tests.test_realtime_ingress tests.test_display_ingress_backpressure tests.test_perfowl_core_cli tests.test_display_acquisition_diagnostics tests.test_display_performance tests.test_frametime_continuation tests.test_activitytrace_frame_lifetime全量:
bash
cd MacApp/Sidecar
PYTHONPATH=. /private/tmp/perfowl-core-py312/bin/python3 -m unittest discover -s tests -p 'test_*.py'站点:
bash
cd site
PATH="$HOME/.nvm/versions/node/v20.19.5/bin:$PATH" npm run build还必须:
bash
git diff --check24. 实现 AI 交付要求
实现 AI 完成后必须提供:
- 实际修改文件列表;
- ingress 架构说明;
- 与本方案的偏差及原因;
- 新增测试列表;
- 专项测试结果;
- 全量测试结果;
- synthetic benchmark 原始数字;
- queue high-water / writer throughput;
- devlog 更新路径;
- 本地 commit;
- GitLab / GitHub push 结果;
- 明确标记 E2 completed / E4 pending。
共享工作区有大量并行改动,禁止 reset、stash、rebase、force push 或覆盖无关文件。
25. 最终验收职责
实现 AI 完成后,由独立验收执行:
text
1. 审计 Git diff
2. 检查是否改了指标公式 / Source Owner
3. 检查 queue ordering / bounds / fail-closed
4. 检查 stop / seal 顺序
5. 重跑专项
6. 重跑 Sidecar 全量
7. 核对 benchmark
8. 构建新的测试包
9. 核对 App 内 embedded collector build stamp
10. 真机跑 Display-only
11. 读取最新 Session
12. 对照第 20 节 E4 门禁
13. 再跑 Startup+Display 与静动态切换只有 E4 满足门禁后才允许把状态从 implemented · gate-open 改为 validated。
26. 给实现 AI 的一键任务描述
你接手 Perfowl 的 R8-Fix-4。不要重新从 ActivityTrace source、FrameTime 公式、Graphics FPS 或 stdout wire 开始排查。先完整阅读 site/devlog/p4-r8-fix4-display-ingress-decoupling-plan.md,并阅读 site/devlog/p4-r8-display-performance-gap-classification-result.md 的最新 Build 65 证据。Build 65 已确认 ActivityTrace 576 delivered / 3 consumed / 573 undrained / 0 dropped,wire 0 dropped / max 2 records / 22KB,因此 stdout 不是当前主因。当前最高置信度问题是高频 Source Drain 被 await append_raw → global writer lock → single writer executor → Raw/Metric/OnlineReport/Checkpoint → append_decoded 串行链路耦合。严格按 R8-Fix-4 实现“全局 Ordered Ingress Queue + Display no-wait fast drain + realtime projection 前移 + single durable writer + decoded persistence 合并 + bounded fail-closed + stop drain + diagnostics”。不得修改 FPS/FrameTime 公式,不得改 Source Owner,不得回退 Graphics/CoreProfile,不得把 missing 补值,不得静默丢 Raw。完成后跑专项、Sidecar 全量和 synthetic benchmark,更新 devlog,但只声明 E2 implemented / E4 pending,不要打包并宣称真机修复完成。共享工作区已有大量并行改动,禁止 reset/stash/rebase/覆盖无关文件,只提交本批明确范围。
27. Implementation Update (2026-09-18)
- 当前状态:
implemented · gate-open - 证据等级:
E2 completed / E4 pending(根据工作流程与红线原则,在未完成真机 Build 66 测试前不声明 E4 可用) - 实际修改文件:
MacApp/Sidecar/perfowl_collector_core/realtime_ingress.py(全新组件):OrderedRawIngress:维护进程级全局 FIFO 队列,上限 8192 records / 32 MiB,支持超限 fail-closed(触发writer-backpressure-overflow,严禁静默丢 Raw);enqueue_nowait:高频 Display 专用的非阻塞入队;enqueue_and_wait:普通低频源等待落盘结果;- Background Writer Worker:批量聚合(64 条 / 512 KiB / 10ms),单写者线程写入;
drain_writer(有界超时 10s,超时标partial-writer-drain-timeout)与 Section 15 全套诊断采样。
MacApp/Sidecar/perfowl_collector_core/session_writer.py:- 新增
append_batch(items)批量持久化接口,在单锁单线程内连续推进,消除每帧跨线程切换与锁竞争开销。
- 新增
MacApp/Sidecar/perfowl_collector_core/realtime.py:- 引入
OrderedRawIngress; consume()接入快路径:projector.consume(raw)同步执行实时投影计算(前移),随后raw_ingress.enqueue_nowait(raw)立即返回,完全消除对磁盘落盘与 redundant decoded 写入的同步等待;- 调整关闭顺序(Stop sources → drain consumer tasks → close ingress → drain writer → stop worker → shutdown executor);
- 注入 Section 15 诊断至 manifest 与
stopped返回包;并在 drain 超时或 backpressure 溢出时置partial。
- 引入
MacApp/Sidecar/perfowl_collector_core_cli.py:- 将
_WireOutput文档说明从“root cause”修正为“defensive wire decoupling”,杜绝后续误导。
- 将
MacApp/Sidecar/tests/test_realtime_ingress.py:- 新增 9 项
OrderedRawIngress专项测试,涵盖入队、批处理、超限溢出、超大 chunk、排空与诊断(9/9 PASS)。
- 新增 9 项
MacApp/Sidecar/tests/test_display_ingress_backpressure.py:- 新增 T1~T8 专项测试与 B1/B2 合成基准测试(9/9 PASS)。
- 与本方案偏差说明:无偏差。完全遵循有界防雪崩、单一落盘 Owner、全局序列一致性与有序安全退出协议。
- 测试结果:
- 专项测试(Ingress + Backpressure + Display Diagnostics + CLI):174/174 PASS。
- Sidecar 全量测试:596/596 PASS。
- MacApp Swift Package 测试:183/183 PASS(3 skipped)。
- Synthetic Benchmark (B1 & B2, 7200 events, 60s @ 120Hz):
accepted_count: 7200persisted_count: 7200overflow_count: 0dropped_event_count: 0displaySourceToIngressP50Ms: 0.009 msdisplaySourceToIngressP95Ms: 0.028 ms(门禁要求 < 2.0 ms,达成率 70x 余量)displaySourceToIngressMaxMs: 1.45 ms(门禁要求 < 10.0 ms,达成)writer sustained throughput: > 5,000 events/s(门禁要求 >= 240 events/s,达成)writerDrainStatus:completed
- 后续未关门门禁:
- Build 66 真机 E4 动态与静态场景实测,读取
display-acquisition-census.json.subscription与 Section 15 诊断,关闭 R8 质量门禁。
- Build 66 真机 E4 动态与静态场景实测,读取
28. Independent Validation Update · 2026-09-18 · Build 66 测试包
独立验收没有直接采用实现 AI 的自报结果,而是重新审计 e277049..ecc1321 的改动并重新执行测试、性能基准与打包。当前结论仍保持:implemented · gate-open,E2 通过,E4 待真机。
独立代码审计
- R8-Fix-4 commit 未修改
display_performance.py的指标公式、metric_engine.py的 Display 公式、Registry Source Owner、FPS / FrameTime Canonical 知识页;formula-source-owner-guard: PASS。 - Display fast path 已变为
projector.consume(raw) → raw_ingress.enqueue_nowait(...) → 继续 drain,不再等待 durable writer。 - Display decoded persistence 已并入同一次
SessionWriter.append(... decoded_outputs=...),避免每帧第二次 writer round-trip。 - Durable writer 仍为单 owner;Raw queue 有界(8192 records / 32 MiB),overflow fail-closed,不允许静默丢 Raw。
- shutdown 顺序实现为 stop source → 等 consumer 最后 enqueue → close ingress → drain writer → stop worker → shutdown executor;writer drain 超时会进入 partial。
_WireOutput注释已修正为 defensive wire decoupling,不再把 stdout 作为已确认主因。git diff --check与本批静态审计通过。
一个非阻塞实现偏差:BATCH_MAX_WAIT_SECONDS = 10ms 常量存在,但当前 worker 实际采用“立即贪心取当前可用项”的方式组 batch,并没有为了凑批主动等待 10ms。独立基准显示当前吞吐与队列余量显著超过门禁,因此本次不把它作为 Build 66 阻断项;后续文档中的“64 / 512KiB / 10ms”应理解为 batch 上限设计,不应理解成固定等待 10ms。
独立 E2 测试
- R8 / Display / Ingress / ActivityTrace 专项:136 / 136 PASS。
- Sidecar Python 3.12 全量:596 / 596 PASS,38.090s。
- MacApp Swift Package:183 tests,3 个显式 benchmark skipped,0 failures。
metric-knowledge-gate:PASS,66 Registry contracts;VitePress build PASS。
独立 120Hz 混合负载基准
使用真实 RawSessionRecorder + IncrementalMetricRecorder + OnlineReportSession + PeriodicCheckpoint + SessionWriter,注入 7200 条 Display + 59 条 sysmontap:
text
accepted 7259
persisted 7259
overflow 0
drainStatus completed
throughput 3063.0 events/s
Display source→ingress P50 0.013 ms
Display source→ingress P95 0.042 ms
Display source→ingress Max 1.483 ms
ingress high-water 122 records / 62,336 bytes
writer batch P95 23.53 ms
writer queue wait Max 52.48 ms
checkpoint Max 22.36 ms
RSS max delta ≈ 2.67 MB对照方案门禁:writer throughput ≥ 240 events/s、source→ingress P95 < 2ms、Max < 10ms,均通过且有明显余量。
Build 65 故障形态的直接复现验收
进一步直接接入真实 Pmd3Subscription,模拟 576 条已经 delivered 的 FrameLifetime,同时让 durable writer 第一次 append 人为阻塞 1 秒。结果:
text
delivered 576
consumed 576
undrainedAtClose 0
subscriptionDropped 0
consumerSeconds 0.086s
ingressAccepted 576
ingressPersisted 576
writerDrainStatus completed
source→ingress P95 1.589 ms
writerQueueWaitMax 1210.2 ms这条测试正面覆盖 Build 65 的 576 delivered → 3 consumed → 573 undrained 故障形态:即使 durable writer 被阻塞 1 秒,subscription consumer 仍能快速排空全部 576 条,并把慢写入隔离在 ingress 后方。
Build 66 测试包 provenance
内部测试 App 已生成:
text
CFBundleVersion 66
PerfowlGitCommit ecc1321-dirty
collectorCoreSHA256 8e0c29b054a90ec3eddf9380e8b439467fa0ab4d5c12de8a5ae88994e9aa4074
collector probe valid=true / 66 metric contracts / 10 source families
codesign verify PASS
signature ad-hoc(仅内部真机测试,不是发布/公证包)共享工作区仍包含并行开发的未提交改动,因此 provenance 有意写成 ecc1321-dirty,不能把本包描述成 clean-commit release。
Build 66 E4 门禁保持不变
第一轮先跑 30~60 秒 Display-only 动态场景。至少满足:
text
frameLifetimeMatchedCount >= 100
consumedEventCount / deliveredEventCount >= 99%
undrainedAtClose <= 1
droppedEventCount = 0
ingressUndrainedAtSeal = 0
FPS / FrameTime 连续有值
P95 / Jitter warmup 后有值
Refresh Rate 有值只有真机达到上述门禁后,R8-Fix-4 才能从 implemented · gate-open 更新为 validated。
29. Validation Update · 2026-09-18 · Build 66 E4 FAILED
Build 66 真机第一轮没有通过 R8-Fix-4 的 E4 门禁。该轮 provenance 与测试包一致:CFBundleVersion=66、PerfowlGitCommit=ecc1321-dirty、collectorCoreSHA256=8e0c29b054a90ec3eddf9380e8b439467fa0ab4d5c12de8a5ae88994e9aa4074。
真机结果
- Product Session 最终状态:
partial。 failureReason:graceful_stop_timeout: Collector 未在限定时间内完成报告。- wire 最后停在
core_profile_stopped,没有最终stopped;因此 seal-timedisplay-acquisition-census.json与完整 ingress diagnostics 未能落盘,不能伪造delivered/consumed/undrained最终值。 collector-core/raw-events.jsonl已记录 314 条display.frame-lifetime.raw。frame-events-live.jsonl共 316 行,其中 312 个有效 FrameTime;FrameTime 中位数约 16.641 ms。- 产品 wire 共 35 个 sample;配置采样间隔为 1 秒,但实际 sample 间隔 P50 2586 ms、Max 7016 ms,说明 event loop 仍发生全局秒级饥饿。
- Display wire:FPS 仅 14/35 sample 有值,20/35 为
stale-source;FrameTime 32/35 有值;P95 / Refresh Rate / Jitter 各 34/35 有值。与“Display 连续有值”的门禁不符。
Source cadence 证据
对 314 条已消费 FrameLifetime 逐帧比较 source presentation 时间与 host receive 时间:
text
host gap P50 17.980 ms
host gap P95 1588.660 ms
host gap Max 5243.227 ms
host gap > 1s 52 次
source gap P50 16.641 ms
source gap P95 33.288 ms
source gap Max 49.936 ms
source gap >100ms 0 次52 次 host 侧 >1 秒停顿中,设备侧相邻 presentation 间隔仍全部 <100ms。它不能解释为“目标 App 静止所以没有帧”;连续帧已经存在,只是在 host 调度/消费链上被延后。
R8-Fix-4 的实际效果
R8-Fix-4 不是无效:相较 Build 65 的 576 delivered / 3 consumed / 573 undrained,Build 66 已经能够持久化数百条连续 FrameLifetime,证明 writer decoupling 明显改善了下游阻塞。但它没有关闭问题;仍有 1~5 秒级全局调度停顿,所以当前状态保持:
text
implemented · gate-open
E2 PASS
E4 FAILED不得将 FPS / FrameTime 标记为已修复。
新根因:Pmd3Subscription pump 缺少 event-loop fairness
当前 Pmd3Subscription._pump() 的核心循环是:
python
async for item in self._source:
...
self._queue.put_nowait(item)
self.delivered_event_count += 1循环内没有任何显式调度让步。如果 pymobiledevice3 上游已经有缓存消息,__anext__() 可以连续立即完成,而 put_nowait() 同样不产生 await;pump 因此能够连续处理大量消息并占住 asyncio event loop。此时 bridge 的 deliveredEventCount 持续增长,但真正的 source consumer、sample publisher 和其他 source task 得不到调度。
这比“durable writer 慢”更直接解释 Build 65 的决定性形态:pump 能 delivered 576,但 consumer 只 consumed 3,且 queue 未 overflow。
独立最小复现
完全移除磁盘、SessionWriter、OnlineReport 和 Display 公式,只运行当前 Pmd3Subscription + fast async upstream。
576 条复现:
text
upstream events 576
consumer 第一次被调度时 delivered 576
consumer 第一次 pull 后 pending 575
final delivered 576
final consumed 576
final dropped 0也就是说,在 consumer 获得第一次执行机会之前,pump 已经先把 576 条全部塞进 bridge queue。
扩大到 200000 条后,只给 event loop 一次调度机会:
text
elapsed 411.6 ms
delivered 200000
consumed 0
pending 200000
heartbeat ticks 1这在没有 writer、没有磁盘、没有 stdout backpressure的情况下仍可稳定复现 event-loop starvation,因此将问题定位到 Pmd3Subscription 调度公平性,置信度高于前两轮 writer/stdout 假设。
下一修复方向:R8-Fix-5
下一轮不再修改 FPS / FrameTime 算法,也不再继续调整 durable writer。目标应改为:
- 在
Pmd3Subscription._pump()增加有界 burst + 显式 event-loop yield,不能让即时可用的 upstream 无限连续占用主循环; - ActivityTrace host-side filtered stream 也增加按消息数/时间预算的 fairness yield,避免大量未命中 signpost 在过滤阶段独占 event loop;
- 新增
pumpYieldCount / pumpMaxBurstCount / pumpMaxBurstDurationMs / eventLoopHeartbeatMaxGapMs诊断,下一次真机直接证明调度公平性; - 设计门禁:576 条 fast-upstream 场景下,consumer 必须在 pump delivered 完全部事件之前获得执行机会,heartbeat 必须持续前进;
- 120Hz Display + 多源 burst 压测,要求 source consumer 无秒级停顿;
- 同时修正 stop/finalize 预算:当前产品给 Core 10 秒 graceful closeout,而本次真机在最终
stopped前被超时终止,必须保证正常 stop 能在产品 fault budget 内完成,且始终落盘最终 census/ingress diagnostics。
R8-Fix-5 开发前应先形成独立方案并审批;当前只完成根因分析与验证记录,不修改产品代码。