外观
Build 73 多指标并采稳定性统一修复方案
2026-09-20 独立验收更新:Build 74 首轮独立 E2 未通过。Device Network source-side aggregate 已位于 generic queue 前,但 ready backlog 下 aggregate loop 缺少 cooperative fairness,可连续占用 event loop。GPU 500ms closeout 在宿主高负载下出现墙钟失败,先受控复验。后续执行以 site/devlog/p4-build74-acceptance-followup-repair-plan.md 为准,未修复前不得进入 E4。
交付给开发 AI 的唯一执行方案。 基线为 Build 73 / a8b8f39735fec068af82effe0d52c29c82ccd206。 主 E4 Session:0a098c81-58de-4e20-a937-fb7092ded94c。 测试条件:约 303 秒、46 个 requested metrics、截图序列开启。
当前状态:Build 74(P0-A / P0-B / P0-C / P0-D)implemented · gate-open — E2 completed / E4 pending。 代码实施与 E2 门禁见文末 §15 Implementation Update 与 §16 Validation Update。 Build 75(P1/P2:Display coverage ledger / Sysmontap compact raw / Screenshot 1Hz)未启动,按本文 §0 的拆分必须基于已通过 E4 的 Build 74 commit 再进行。
本批不是重新修改已通过纯 Display E4 的 R8-Fix-11,而是解决多个 Source 同时开启后的数据正确性、event-loop 竞争、停止与资源成本问题。
0. 交付拆分
禁止一次把七类问题全部混改。拆成两个可归因批次。
Build 74:P0 数据正确性与并采稳定性
只包含:
- Device Network source drop 修复;
- GPU Counter 协议/data-row 取证与修复;
- 多 Source event-loop starvation 定位与修复;
- GPU / Network source consumer closeout timeout 修复;
- source-specific diagnostics。
Build 75:P1/P2 账本、磁盘和截图
只有 Build 74 E4 通过后才进入:
- Display report-derived metric coverage 账本;
- Sysmontap production raw compact retention;
- Screenshot 1 Hz cadence。
这样可以保证真机结果能归因,不会把 Network、GPU、Display、磁盘和截图的变化混成一个无法分析的大包。
1. Build 73 全指标 E4 基线
主 Session:0a098c81-58de-4e20-a937-fb7092ded94c。
整体:
- duration:303.051 s
- requestedMetricCount:46
- screenshotsEnabled:true
- collector eventCount:5043
- metric consumedRawEventCount:5043
- ingressUndrainedAtSeal:0
- dataCompleteness:complete
最终 seal 完整,但运行期存在真实 source loss 和 closeout timeout,不能因为 complete 就认为所有 Source 数据可靠。
1.1 Display under load
纯 Display Build 73:
- wake P50/P95/Max:0.124 / 0.388 / 1.037 ms
- queue P50/P95/Max:0.124 / 0.388 / 1.037 ms
- heartbeat scheduler P50/P95/Max:19.114 / 33.952 / 55.872 ms
200ms stall:0
全指标 Build 73:
- wake P50/P95/Max:3.998 / 83.897 / 443.254 ms
- queue P50/P95/Max:3.998 / 83.897 / 443.254 ms
- heartbeat scheduler P50/P95/Max:50.887 / 229.832 / 926.802 ms
200ms stall:148
FrameLifetime 本身仍持续:
- FrameLifetime:986
- valid:975
- frameTime index:980
- frameFPS index:980
- valid 最大 host gap:约 2.859 s
结论:当前是多 Source 并采导致 event-loop 竞争和实时延迟,不是旧“FrameTime 采一点后永久停流”复发。
1.2 ActivityTrace 被其它 Source 拖慢
纯 Display:
- parse row P50/P95:8.256 / 13.565 ms
- Max:37.818 ms
20ms:24 / 8047,约 0.30%
全指标:
- parse row P50/P95:12.065 / 25.549 ms
- Max:189.959 ms
20ms:808 / 11169,约 7.23%
同一 parser 在打开其它 Source 后明显恶化,因此本批禁止继续调整 R8-Fix-11 parser fairness 常量。
2. P0-A:Device Network source drop
2.1 已确认事实
network.device.raw:
- persisted raw:1357
- span:约 298.42 s
- derived device network samples:298
- subscription droppedEventCount Max:10576
Build 73 的 Pmd3Subscription 所有 Source 共用 event_queue_size=2048;队列满时主动丢最旧事件并增加 dropped_event_count。
所以 10576 是 Perfowl Host 自己真实丢掉的 upstream event,不是设备端统计字段。
2.2 为什么影响准确性
Metric Engine 对 ConnectionUpdateEvent 的 tx_bytes / rx_bytes 做逐条累加,再按 elapsed 得出 device send/receive rate。
因此丢 ConnectionUpdateEvent 会少累计字节,曲线即使连续,也可能系统性低估设备网络流量。
2.3 禁止的伪修复
禁止:
- 只把全局 queue 从 2048 改大;
- 延长 closeout 等 backlog;
- drop 后继续标 candidate;
- 对丢失区间做插值;
- 用 process network 替代 device network;
- 靠降低采样频率掩盖问题。
2.4 推荐实现:DeviceNetworkBurstAccumulator
在通用 Raw/JSON/Writer 路径之前增加 source-specific accumulator。
数据流:
NetworkMonitor upstream → 极轻量 accumulator → 短窗口 aggregate → compact aggregate Raw → Metric Engine
第一版 aggregation window 建议 100ms。它只是 Host 聚合窗,不改变产品 1 秒 sample clock。
ConnectionUpdateEvent 每条只做 O(1) 工作:
- inputEventCount +1
- txBytes 累加
- rxBytes 累加
- first/last adapterSequence
- first/last source monotonic
- malformed count
Interface / ConnectionDetection 低频事件不参与 byte sum,可原样传递或 compact 为 diagnostics。
新增明确事件类型,例如 network.device.aggregate.raw,禁止伪装成一条普通 ConnectionUpdateEvent。
Aggregate payload 至少包含:
- aggregationWindowMs
- inputEventCount
- connectionUpdateCount
- txBytes
- rxBytes
- firstAdapterSequence
- lastAdapterSequence
- firstSourceMonotonicNs
- lastSourceMonotonicNs
- malformedInputCount
Metric Engine 同时兼容 legacy network.device.raw 和 aggregate raw。数学语义保持窗口内 Σbytes / elapsed,但 source contract 已改变,需要新的 formula/source version,并在知识库说明与旧公式的等价关系。
完整 per-event Network raw 只允许 debug 开关开启,生产默认关闭。
2.5 Network diagnostics
新增:
- networkInputEventCount
- networkAggregateFlushCount
- networkAggregateInputCount
- networkAggregateMalformedCount
- networkAggregateTxBytes
- networkAggregateRxBytes
- networkSourceDropCount
- networkQueueHighWater
- networkMaxAggregateWindowMs
硬门禁:聚合输入 + 非 Update 输入 + malformed 必须与总输入对账;source drop 必须为 0。
3. P0-B:GPU Counter 协议/data-row
3.1 最新证据
本轮 gpu.counter.raw 共 1337 条:
- candidate:4
- missing:1333
1333 条 missing 全部是同一个 shape:
- pythonType=list
- length=2
- itemTypes=[int,int]
- decodeStatus=unsupported-message
所以问题已经从“未知 GPU 协议很多”收敛为“一个稳定重复的两整数消息合同没有被理解”。
派生 GPU Counter 只有 70 个 value,并有约 143.76 秒空洞。
3.2 Configure 参数合同必须重新验证
当前 Build 73 调用形态:
- counters = min-collection-interval
- counterProfile = 3
- interval = 1
- windowLimit = 0
- tracingPID = -1
公开 Instruments/DTX 实现存在不同形态:
- counters = 0
- counterProfile = 3
- interval = 0
- windowLimit = -1
- tracingPID = 0xFFFFFFFF
外部线索来自公开项目 dkw72n/idb 的 instrument_service.py,仅作为 hypothesis,禁止直接照抄。
重点验证当前把 min-collection-interval 放进 counters 参数是否合同错误。
3.3 先做 decoder 前 raw capture
在 GPUCounterDecoder 之前增加 bounded diagnostics:
- deliveryRoute:dispatch / notification
- selector(如存在)
- payload type / length
- auxiliary count / item types
- 两整数 first / second
- first 64 / last 64
- unique pair count
- top-N pair frequency
- schemaReceivedAt
- firstDataRowAt
- lastDataRowAt
- unsupportedCount
- dataRowCount
- maxDataGapMs
禁止无限写 1333 条重复诊断。
3.4 Configure A/B 只允许内部诊断模式
实现显式 variant:
- current:保持 Build 73 参数;
- reference:按经过核验的公开参数/type 复现。
不得 production runtime 自动试多个 variant,也不得一次 Session 内切换后择优。两个 variant 必须独立 Session。
3.5 GPU shipping 成功门禁
只有满足以下事实,才允许提升为 shipping:
- device-info 正常
- schema 正常
- 持续 data row
- unsupported-message 不再占主导
- counter timestamp 单调
- counter values 有真实动态变化
- 5 分钟无 >30s data gap
- stop / flush clean
如果两整数最终只是 heartbeat/control ack,应显式标 control,不产生 metric,也不记 missing。
如果两整数是 data row index/handle,必须证明它与后续 payload/auxiliary 的关联后才能解码。
禁止直接把两个 int 当 counter value。
4. P0-C:多 Source event-loop starvation
4.1 冻结 R8-Fix-11
不得修改:
- ActivityTrace parser 32 rows / 1ms active
- Filter 64 messages / 1ms
- Pump 32 events / 1ms
- Realtime 8 events / 1ms
- direct waiter
- consumer-handoff=0
纯 Display E4 已经证明这套组合成立。
4.2 当前最可疑同步工作:Sysmontap payload normalization
本 Session:
- sysmontap.raw:598 events
- raw JSON:约 62.78 MB
- 平均约 107.5 KB/event
当前 _RawStreamAdapter.raw_events 在 asyncio event-loop 中同步执行 payload conversion;super()._payload 会递归 json_safe_raw。
也就是说约 100KB 的深层 plist 会先在 event-loop 线程同步转换,writer offload 发生在它之后。
这与全指标时 ActivityTrace row P95/Max 明显恶化方向一致。
4.3 必须先补 per-source stage timing
每个 source consumer 记录:
- sourceNextWaitMs
- payloadNormalizeActiveMs
- rawEnvelopeBuildActiveMs
- appendEnqueueMs
- projectorConsumeActiveMs
- postProcessActiveMs
- P50/P95/P99/Max
1ms / >5ms / >20ms count
按 adapterID 分组。
同时记录 consumer 当前 stage:
- waiting-source
- normalizing
- append
- projector
- post-process
- stopping
- done
Closeout timeout 必须把 adapterID + stage + stage duration 写入 diagnostics。
4.4 Sysmontap normalization worker
只有 stage timing 证明 json_safe_raw 是主要 owner 后才执行。
第一版只针对 pmd3.sysmontap.raw,不要把所有小 Source 都 to_thread。
推荐固定小型 executor,max_workers=1,保证同一 Sysmontap source 严格顺序。
约束:
- source window 不变
- adapterSequence 不变
- target PID 逻辑不变
- cancellation 可终止
- 不把 stateful MetricProjector 整体丢到 worker
4.5 修复顺序
严格按:
Network flood 收敛 → GPU unsupported flood 收敛 → 重测 heartbeat → 若仍失败,再根据 stage timing 优化 Sysmontap
禁止继续叠加全局每-event sleep(0)。当前 generic consume 已经在每条 Raw 后显式 yield,新的 yield 不能解决单次同步 step 内部 20~200ms 占用。
5. P0-D:closeout timeout
本轮:
- closeoutStatus=timed-out
- failureStage=consumer-drain
- source consumers=10
- completed=8
- cancelled=2
- timedOut=2
两个超时任务:
- stream-perfowl.gpu-counter.raw
- stream-pmd3.device-network.raw
writerDrainStatus=drained、ingressUndrainedAtSeal=0、dataCompleteness=complete,所以问题是 source consumer 没在 500ms 内退出,不是 writer 丢 final record。
5.1 禁止延长 500ms
SOURCE_CONSUMER_DRAIN_TIMEOUT 继续冻结为 500ms。现有代码已经明确 timeout value is not the bug。
5.2 正确修复
利用 §4 的 stage diagnostics,先证明 timeout 卡在哪。
Build 74 必须输出:
- timedOutConsumerNames
- timedOutConsumerStages
- timedOutConsumerStageDurations
Network stop:
- 停止接受新 upstream;
- flush 当前 aggregate;
- terminal;
- consumer drain;
- 不留下 per-event backlog。
GPU stop:
stopCollectingCounters → flushRemainingData → terminal → consumer exits
必须检查当前 GPU aexit、adapter.stop、backend.close 顺序。如果 service stop 只有 backend.close 才发生,而 consumer-drain 在 backend.close 前等待,则增加 source-specific graceful stop hook,例如 request_stop_for_drain / wait_source_terminal,不要延长 timeout。
6. Build 74 E2
6.1 Device Network burst
构造至少 100,000 ConnectionUpdateEvent,随机 tx/rx bytes。
必须:
- inputCount=100000
- aggregateInputCount=100000
- sourceDrop=0
- Σaggregate.tx == Σinput.tx
- Σaggregate.rx == Σinput.rx
- malformed=0
- memory bounded
混入 Interface/Detection event 后 byte sum 仍必须一致。
6.2 Network replay equivalence
同一 deterministic fixture 分别经过 legacy per-event 和 aggregate path,最终 1s send/recv rate 只允许浮点舍入差。
6.3 GPU fixtures
至少覆盖:
- device-info
- schema
- 两整数消息
- notification payload + auxiliary
- 已成功 data row fixture
- unknown shape
- stop/flush
Unknown 必须 explicit missing/control,不能猜值。
6.4 Multi-source synthetic load
同时模拟:
- Sysmontap 2Hz、100KB payload
- Device Network burst
- GPU notifications
- Graphics 1Hz
- Energy
- Process Network
- Display FrameLifetime
- sample publisher
- heartbeat
目标:
- source drop=0
- heartbeat P95<100ms
- heartbeat Max<250ms
- Display wake P95<20ms
- no task growth
6.5 Closeout
Network/GPU 有 backlog 时 stop:
- consumer drain <500ms
- timedOut=0
- cancelled=0
- aggregate final flush exactly once
- GPU flush exactly once
- release exactly once
6.6 全量回归
必须跑:
- R8-Fix-11
- R8-Fix-10
- R8-Fix-9
- Display pipeline timing
- Network metric
- GPU Counter
- Energy
- Sysmontap
- Python 全量
- Swift 全量
- VitePress
- git diff --check
7. Build 74 E4 矩阵
E4-A:纯 Display 回归
60–90 秒。
必须:
- wake P95<20ms
- queue P95<20ms
- heartbeat P95<100ms
200ms stalls=0
- drop=0
确保多指标修复不破坏 R8-Fix-11。
E4-B:全指标、不截图,5 分钟
这是主门禁。
Network:
- sourceDrop=0
- aggregateInputCount 对账
- device network sample span>=295s
- 无 >5s 非业务 gap
GPU(若协议已完成):
- unsupported-message 不再占主导
- 连续 data row
- max data gap<30s
- stop/flush clean
若 GPU 协议仍未证明,只允许标记 diagnostic gate 未通过,不得伪称修复。
Event loop:
- scheduler P50<50ms
- P95<100ms
- Max<250ms
200ms stalls=0
Display under load:
- wake P50<5ms
- wake P95<20ms
- wake Max<100ms
- queue P50<5ms
- queue P95<20ms
- queue Max<100ms
- Display drop=0
- FrameLifetime 持续
Closeout:
- closeoutStatus=clean
- consumerDrainStatus=completed
- sourceConsumerTimedOutCount=0
- sourceConsumerCancelledCount=0
- writerDrainStatus=drained
- ingressUndrainedAtSeal=0
E4-C:全指标 + 截图,5 分钟
只有 E4-B 通过后再跑。
Build 74 暂不要求截图完美 1Hz,但开启截图后 heartbeat / Display 仍必须满足 E4-B。截图 cadence 的产品门槛放到 Build 75。
8. Build 75-A:Display coverage ledger
当前 coverage 错误标记:
- ios.display.refresh_rate:missing / sampleCount 0
- ios.frame.pacing_jitter:missing / sampleCount 0
- ios.frame.time.p95:missing / sampleCount 0
但同 Session:
- frameTime index=980
- frameFPS index=980
- Frame report 已产生 Avg/Std/Var/Jank
这是 coverage ledger bug,不是采集缺失。
正确设计:Realtime Display Projector 进入产品 snapshot 的 display metrics,由同一个 snapshot publication path 记 coverage。
至少记录:
- metricID
- publishedValueCount
- missingCount
- firstPublishedMonotonicNs
- lastPublishedMonotonicNs
- quality
- reason
禁止 final report 阶段看到有 FrameTime 就猜 sampleCount;真实发布多少次就记多少次。
Gate:Workbench/metrics.csv 中存在真实 value 时,coverage.sampleCount 必须 >0 且 valueQuality 不能是 missing。
9. Build 75-B:Sysmontap compact raw
本 Session:
- whole session:约 111 MB / 5min
- raw-events.jsonl:约 71.6 MB
- screenshots:约 21.3 MB
- Sysmontap raw:约 62.78 MB / 598 events / 平均 107.5KB
- CoreProfile retained JSON:约 0.06 MB
FTI-003 CoreProfile raw 优化已生效,现在磁盘主因是 Sysmontap。
新增策略:
- target-system:production default
- full:debug
默认只保留:
- target PID process row;
- 请求指标需要的 SystemCPU/SystemAttributes;
- PerCPUUsage;
- VM free / enabled CPU 等公式依赖;
- timestamp/source-window/identity/provenance。
删除无关进程完整 row 和 CapturePlan 不需要的大字段。
字段白名单必须由 Metric Registry / Source family 依赖驱动,禁止手工随意删字段。
新增 diagnostics:
- sysmontapObservedBytes
- sysmontapPersistedBytes
- sysmontapRetentionRatio
- sysmontapRetainedProcessCount
- sysmontapDroppedUnrequestedFieldCount
Compact raw replay 必须得到与 live 相同的 requested metrics。
Storage 目标:
- raw-events.jsonl 5min <=20 MB
- whole session 5min <=50 MB(页面截图体积差异时作为目标,不作为准确性硬失败)
禁止靠降低正式指标采样频率省空间。
10. Build 75-C:Screenshot cadence
本 Session:
- span≈301.33s
- persisted=232
- failure=0
1.5s interval=60
- max gap=3.346s
图片质量和黑图问题未复发,当前只是并采压力下 1 秒 cadence 不稳。
新增 diagnostics:
- scheduledTickCount
- captureRequestCount
- captureCompletedCount
- persistedCount
- captureRTT P50/P95/Max
- encodeMs P50/P95/Max
- persistMs P50/P95/Max
- interval P50/P95/Max
- missedTickCount
- inFlightSkipCount
调度改为 monotonic fixed-rate:T0+1s、T0+2s、T0+3s……
如果 capture 跨过下一个 tick,跳过过期 tick、记录 missedTick,禁止 burst 补拍和并发发多个 DTX screenshot request。
E4 目标:
- 5min persisted>=285
- failure=0
- interval P50≈1s
- P95<=1.5s
- Max<=3s
若设备 screenshot service 本身达不到门槛,必须用 captureRTT 证明 source limit,不能伪装 1Hz。
11. Startup 边界
本 Session Initial Frame Rendering missing、UIKit Scene Creation partial、startup window 有一个 device lost marker。这属于既有 Startup lifecycle 概率缺失方向。
Build 74/75 不修改 Startup 获取机制,只要求不得比当前更差。
12. 禁止项
禁止:
- 修改 R8-Fix-11 parser fairness 常量;
- 恢复 consumer-handoff;
- 扩大全局 queue 作为 Network 最终修复;
- 延长 consumer-drain 500ms;
- Network drop 后插值/补值;
- GPU 两整数直接猜语义;
- GPU runtime 自动试多个 configure variant 并择优;
- 用 0 替代 missing;
- 为省磁盘降低正式采样频率;
- 删除 replay 所需 Sysmontap 字段;
- screenshot burst 补拍;
- 同时修改 Startup;
- 把全指标 heartbeat 回退继续归因于已经通过纯 Display E4 的 ActivityTrace wrapper。
13. 开发提交与审计
建议独立 commit:
- test/diagnostics: source-stage and subscription telemetry
- fix(network): lossless device-network aggregation
- fix(gpu): protocol capture/config contract + decoder evidence
- fix(realtime): isolate proven heavyweight normalization
- fix(closeout): source-specific graceful terminal
- fix(report): display coverage ledger
- perf(storage): compact sysmontap raw retention
- fix(screenshot): fixed-rate cadence diagnostics/scheduler
- docs/tests
Build 74 只包含 1–5;Build 75 包含 6–8,并基于已经通过 E4 的 Build 74 commit。
每个批次必须:
- exact clean worktree
- exact commit
- Python / Swift / VitePress
- git diff --check
- 双远端 main 对齐
- Developer ID 签名
- ZIP 解包后二次验签
- 保存 SHA-256
14. FTI-011 关闭门槛
必须同时满足:
- Device Network source drop=0
- Device Network byte aggregation 100% 对账
- GPU 若支持则持续 data row、无长空洞;若协议未证明则明确 candidate/unavailable,不伪成功
- 全指标 heartbeat P95<100ms
200ms stall=0
- 全指标 Display wake/queue P95<20ms
- Display drop=0
- closeout clean
- source consumer timeout=0
- Display coverage 无 false missing
- Sysmontap raw 不再主导磁盘,raw-events 5min <=20MB target
- Screenshot 5min >=285 target、无黑图/失败
产品原则不变:
先保证数据不丢、不伪造、可证明,再优化实时性和磁盘;不能为了让曲线“看起来连续”牺牲真实数据语义。
15. Implementation Update(2026-09-20)
范围:只做 §0 划分出的 Build 74(P0-A / P0-B / P0-C / P0-D)。§8–§10 的 Build 75(Display coverage ledger、Sysmontap compact raw、Screenshot 1Hz cadence)一行未动。
改动清单(MacApp/Sidecar,tracked 部分 4 文件 +612/−18):
| 文件 | 变更 |
|---|---|
perfowl_collector_core/device_network_aggregation.py | 新增(421 行)P0-A 聚合器与 aggregate 流 |
perfowl_collector_core/gpu_counter_forensics.py | 新增(376 行)P0-B 前置 census、configure 契约、§3.5 门禁 |
perfowl_collector_core/source_stage_timing.py | 新增(280 行)P0-C per-source stage 计时与注册表 |
perfowl_collector_core/adapters.py | +163:接入聚合流、stage 括号、request_stop_for_drain、network_diagnostics() / gpu_diagnostics() |
perfowl_collector_core/pmd3_deep_services.py | +168:PerfowlNetworkMonitor、GPUCounterStream 的 drain 终止与 census |
perfowl_collector_core/realtime.py | +266:consumer 侧 stage 括号、source-terminal pass、manifest 诊断接线 |
perfowl_collector_core/metric_engine.py | +27/−6:device-network source contract v1→v2,legacy 形状保留 |
tests/test_build74_*.py | 新增 5 个套件共 49 项(见 §16) |
另有规范合同与知识库的同步改动(§2.4 强制要求,详见 §15.5): MacApp/Sources/PerfowlCore/PerformanceModels.swift、 site/data/metric-contracts.json、site/knowledge/calculation/metric-contract-index.md、 site/knowledge/metrics/device-network-rate.md,以及本 devlog 与 site/devlog/index.md。
15.1 P0-A:无损 device-network 聚合
DeviceNetworkBurstAccumulator 插在 Raw/JSON/Writer 通用路径之前:上游 NetworkMonitor → O(1) 累加器 → 100ms 对齐窗 aggregate → compact Raw → Metric Engine。上游每条 ConnectionUpdateEvent 只做整数加(不分配、不额外 读表),窗口常量 DEVICE_NETWORK_AGGREGATE_WINDOW_NS = 100_000_000——它只是 host 聚合窗,产品 1 秒 sample clock 未变。
- 新事件类型
network.device.aggregate.raw(eventClass = DeviceNetworkAggregate), 不是伪装成一条普通ConnectionUpdateEvent,回放能区分"折叠后的字节窗"与 "源端单行";InterfaceDetectionEvent/ConnectionDetectionEvent原样透传 (不参与 byte sum)。 - 对账恒等式是硬门禁,且逐项暴露以便诊断而非只给一个布尔:
networkInputEventCount == networkConnectionUpdateCount + networkPassthroughEventCount + networkAggregateMalformedCount(networkInputBalanced),以及networkAggregateTxBytes/RxBytes == networkSourceTxBytes/RxBytes(networkBytesBalanced)。 - 单窗输入上限
MAX_INPUT_EVENTS_PER_AGGREGATE = 65536,状态有界;完整逐事件 Raw 只在显式 debug 开关PERFOWL_DEVICE_NETWORK_PER_EVENT_RAW下追加输出, 生产默认关闭。未扩大全局队列、未插值/补值、未降低采样频率(§12)。 - 诊断字段:
networkInputEventCount/networkConnectionUpdateCount/networkPassthroughEventCount/networkAggregateMalformedCount/networkAggregateFlushCount/networkAggregateInputCount/networkAggregate{Tx,Rx}Bytes/networkSource{Tx,Rx}Bytes/networkMaxAggregateWindowMs/networkMeanAggregateWindowMs/networkPerEventRawEnabled,加上 transport 侧的networkSourceDropCount/networkQueueHighWater(在 subscription 释放之前取快照,否则 stop 后就 再也测不到丢数)。 - Metric Engine:
network.device.raw(legacy)与network.device.aggregate.raw(v2)同一个累加点,因此混合 Session 既不 会双计也不会漏窗;算术不变(窗口内 Σbytes ÷ 真实 elapsed),network-window-rate.v1→network-window-rate.v2是 source-contract 版本号变化而非数值重定义。但它不是"只差浮点舍入"(见 §15.6 偏差 1): 聚合行的 host 时间戳是窗尾而非逐事件到达时刻,实测同一 5 秒 fixture 下 字节总量完全守恒,而逐 1 秒窗 rate 差 ≤ 0.16%、累计差 0.05%(约合 边界上 1–2 条源事件的字节)。Build 73 及更早 Session 的 legacy 逐事件回放 继续支持。
15.2 P0-B:GPU Counter 协议取证(不猜值、不自动择优)
GPUCounterCaptureCensus是 decoder 之前的有界普查:PAIR_CAPACITY = 256/SAMPLE_CAPACITY = 64/SELECTOR_CAPACITY = 16/SHAPE_CAPACITY = 16/TOP_PAIR_COUNT = 8。两整数消息按形状签名TWO_INT_PAIR_SIGNATURE = "list:2:[int, int]"计数(含 overflow 计数), 只记录形状、绝不当成 counter 值解出;dispatch / notification 路由分开 统计,因此"两整数"到底属于哪条路由是可回答的,而不是被合并掉。- configure 显式命名契约:
GPU_CONFIGURE_VARIANTS = ("current", "reference"),current复现 Build 73 的形态、reference是公开文档形态;resolve_gpu_configure_variant()对未知值 fail-closed(抛错),并且 不在 Session 内切换、不 runtime 试多个 variant 择优;若要 A/B,按 §3.4 走 独立 Session +PERFOWL_GPU_CONFIGURE_VARIANT,两次结果各自属于一个独立 Session record。 - §3.5 shipping 门禁落成 8 个显式布尔(
GPUShippingGate:gpuDeviceInfoOk/gpuSchemaOk/gpuContinuousDataRows/gpuUnsupportedNotDominant/gpuTimestampMonotonic/gpuValuesDynamic/gpuNoLongDataGap/gpuStopFlushClean→gpuPromotable),阈值常量与门禁 放在一起(GPU_UNSUPPORTED_DOMINANCE_RATIO = 0.5、GPU_MAX_DATA_GAP_MS = 30_000.0)以便读者看到判据而不只是结论。 门禁不提升任何东西:它只报告"某次 Session 是否具备提升所需的证据"。 新增测试test_r_build_73_evidence_is_not_promotable明确钉死 Build 73 的 证据不足以提升——即 GPU 指标在协议被证明之前只能是 candidate/unavailable, 不得伪成功。
15.3 P0-C:多 Source event-loop 归因(并冻结 R8-Fix-11)
source_stage_timing.py 为每个 source 记录两段而不仅是总量:
- 同步阶段耗时键
STAGE_KEYS = (sourceNextWait, payloadNormalizeActive, rawEnvelopeBuildActive, appendEnqueue, projectorConsumeActive, postProcessActive),加"当前处于哪一阶段" (waiting-source/normalizing/append/projector/post-process/stopping/done)——这样一次 stall 能读到"卡在 append",而不是只能从 heartbeat 退化反推。 - 百分位在保留窗(
STAGE_HISTORY_CAPACITY = 2048)上计算,max 与 over-threshold 计数(1ms / 5ms / 20ms)对整个 Session 精确;时钟回退的 样本被丢弃而不是记成负时长;stopping与done可与真实工作区分;observe_source_wait()只测"两个 item 之间"的等待,不会把自身 normalize 算进等待。 - 注册表按
adapter_id复用同一份记录(SOURCE_STAGE_REGISTRY_CAP = 64有界), 并有任务名桥adapter_id_for_task_name()让 closeout 超时能直接说出 "是哪个 adapter 卡在哪个阶段"。 - 消费侧括号落在
realtime:appendEnqueue包住await append_task,display 快路径与普通路径各自补projectorConsumeActive/postProcessActive。 - R8-Fix-11 冻结:parser 公平性常量与让出判据未改、
consumer-handoff键 保留且恒为 0、未恢复 wall-time fairness、未给 Pump 增加任何新调度 heuristic。 本次对多源竞争的处置是加观测,不是再改调度。
15.4 P0-D:source-specific graceful terminal
病因是停止序列而非窗口:Build 73 里 stream-pmd3.device-network.raw 与 stream-perfowl.gpu-counter.raw 卡在 service queue 上,而 service 只在 channel teardown 才停——teardown 在 consumer-drain 窗之后。
PerfowlNetworkMonitor/GPUCounterStream新增request_stop_for_drain():先stop_monitoring()/stop_collecting_counters()让上游停止产生新行,再用shutdown_queue(events, immediate=False)关队列。immediate=False把停止 哨兵放在已有行之后,所以 consumer 先把 backlog 读干净、再遇到终止;immediate=True会静默丢在途行,直接破坏 byte 守恒。__aiter__捕获QueueShutDown并转为StopAsyncIteration:否则Pmd3Subscription会把它当成 fatal,一次正常 Stop 会被记录成 source 失败。__aexit__只在"没有请求过 drain"时才再调一次 stop,避免重复;GPUflush_remaining_data()保持恰好一次且幂等。realtime在 consumer-drain 之前跑一趟 source-terminal pass,用单一 deadlinemin(SOURCE_TERMINAL_MAX_SECONDS = 0.5, heavy_remaining_seconds() * 0.3)对每个 adapter 有界化,并记录sourceTerminalStatus/sourceTerminalRequestedAdapterIDs/sourceTerminalFailedAdapterIDs/sourceTerminalDurationMs与closeout_ledger的source-terminal阶段。CoreProfile 与 prestarted core 被 排除在外——它们有自己的 drain 契约。SOURCE_CONSUMER_DRAIN_TIMEOUT_SECONDS恒为 0.5,未放宽(§5.1 / §12)。 超时路径新增sourceStageTimingAtTimeout,逐条给出 taskName / adapterID / stage / stageDurationMs / itemCount,让"哪个 source 的哪个阶段没退出来"不再只能靠猜。- manifest 接线:
sourceStageTiming、sourceAdapterDiagnostics(逐 adapter 调network_diagnostics()/gpu_diagnostics(),逐 adapter 容错,单个 adapter 抛错记为<err>DiagnosticsError而不拖垮整份 manifest)。
15.5 规范合同与知识库同步(§2.4 要求)
方案 §2.4 要求"需要新的 formula/source version,并在知识库说明与旧公式的等价 关系"。规范版本号的唯一事实源是 Swift Registry (MacApp/Sources/PerfowlCore/PerformanceModels.swift),知识门禁由它推导出 site/data/metric-contracts.json 快照并强制两者一致,因此三处必须同步改:
| 文件 | 变更 |
|---|---|
MacApp/Sources/PerfowlCore/PerformanceModels.swift | 两条 device-network 指标的 definitionVersion network-interval.v1 → network-window-rate.v2;rawUnit → bytes-per-host-aggregate-window;normalization → sum host-aggregated 100ms windows / elapsed seconds。sourceKey 不变——被求和的物理量仍是 ConnectionUpdateEvent.{tx,rx}_bytes(聚合窗的 txBytes/rxBytes 就是它们的和),变的是聚合契约,所以由 definitionVersion 承载 |
site/data/metric-contracts.json | 由 python3 site/scripts/metric-knowledge-gate.py --write-snapshot 重新生成(不手改),仅两处 definitionVersion 变化 |
site/knowledge/calculation/metric-contract-index.md | 第 15/16 行同步版本与"100ms host 聚合窗"说明 |
site/knowledge/metrics/device-network-rate.md | 规范页:§2 合同表、§3 原理、§4 实现位置(新增 device_network_aggregation.py 与折叠点在通用队列之前)、§5 两种输入形状与对账恒等式、§9 聚合层自对账与 metric 级 quality 分离、§14 新增 v1↔v2 等价关系(含实测偏差)、§15 版本历史新增 v2 行 |
未改 site/retrospective/genesis-and-metric-provenance.md:该页 frontmatter 下方明确声明自己是"历史架构快照,不承担当前 Registry 合同职责",其 network-interval.v1 是历史事实,按"历史节点不改写"的纪律保留。
15.6 E2 期间发现并修复的本批真实回归
_underlying_prepared_stream() 只遍历 Perfowl 自己的几个流包装器;P0-A 新增的 DeviceNetworkAggregateStream 没被登记,于是 prepare_source_transports() 在 transport 预热阶段够不到底下的 pinned NetworkMonitor,抛 RuntimeError("pinned stream lacks transport-only connection contract")。 后果不是"少拿数据"而是覆盖账本会把 pmd3.device-network.raw 记成 failed source——一个把正确性缺陷伪装成"设备不支持"的形态。
它是被既有测试 test_source_preparation. test_pinned_real_adapter_factories_prepare_without_sampling 抓到的 (7 != 8 prepared,pmd3.device-network.raw 的 reason: RuntimeError)。 修复=把该包装器加入遍历链(stream._source),并新增 test_h_aggregate_wrapper_stays_transparent_to_transport_preparation 钉住 "预热能穿过聚合器、且穿过是只读的"。这条回归说明"新增一层包装器"必须同时 检查所有按类型遍历旧包装器的地方。
15.7 与方案的偏差(逐条,含理由)
偏差 1(§6.2 的数值口径,实测未达字面要求)。 方案 §6.2 写"最终 1s send/recv rate 只允许浮点舍入差"。实测做不到:整段 fixture 的字节总量完全 守恒(5 094 112 == 5 094 112),但逐 1 秒窗的 rate 并不 bit-identical—— legacy [1009833, 1014319, 1042184, 1012572] vs aggregate [1011425, 1012907, 1042497, 1014100],逐窗差最大 1 592 B/s(≈0.16%)、 累计差 2 021 B/s(≈0.05%)。按其量级,残差约等于边界上 1–2 条源事件的 字节(fixture 平均 ≈1 019 B/事件),不是系统性丢失,这正是字节总量仍然精确 相等的原因。
根因是聚合的host 时间戳语义变了:legacy 每条 Raw 带的是事件到达时刻, 聚合行带的是窗尾(这正是让边界对齐窗落在 1 秒窗同一侧的原因,见 §15.1); 两者在 1 秒 flush 边界上的归属可以差一条窗。要逐窗 bit-identical,就得让 100ms 聚合窗与 1 秒 sample clock 共享同一个锚点,而 §2.4 明确要求聚合窗"只是 host 聚合窗,不改变产品 1 秒 sample clock"——因此本批不朝那个方向改,而是:
(i) Metric Engine 的 v1→v2 注释从"只差浮点舍入"改成"不是 bit-identical, 并写出实测上界"; (ii) 测试断言写成实际成立的三个性质:字节累计守恒精确、两条路径 1 秒窗 数量相同、逐窗差 ≤ 一个聚合窗字节(由 fixture 派生,非手挑常数; 实测只用掉该上界的 1.3%); (iii) 测试类文档与本节都点明这是对方案 §6.2 字面的偏差,而不是"已按 §6.2 通过"。是否要求逐窗 bit-identical 属于需要重新决策的产品问题,留给独立验收 判断,本批不擅自改口径。
偏差 2(§6.5 未规定 backlog 规模)。 方案只要求"有 backlog 时 stop", 未给规模。本批按实测选 4 000(GPU)/ 20 000(Network),理由与数据见 §16.2 (二):20 000 的 GPU backlog 会占满冻结窗口的 0.53–0.81,属于把测量钉在阈值 边缘。窗口本身未放宽。
偏差 3(§4.4 明确未执行)。 方案 §4.4 的 Sysmontap normalization worker 是 条件项——"只有 stage timing 证明 json_safe_raw 是主要 owner 后才执行"。 本批只交付 §4.3 的 stage timing 本身(P0-C),没有把 Sysmontap 放到 to_thread。按 §4.5 的顺序,这一步要等真机重测 heartbeat 后由数据决定。
16. Validation Update(2026-09-20)
16.1 E2 门禁结果(§6 逐条)
| § | 内容 | 结果 |
|---|---|---|
| §6.1 | Device Network burst(100,000 事件、混入 Interface/Detection、malformed) | test_build74_device_network_aggregation A–H 通过:inputEventCount = aggregateInputCount = 100000、sourceDrop = 0、Σaggregate.tx/rx == Σinput.tx/rx 精确相等、malformed 计数显式、状态对任意洪水长度有界;尾部 aggregate 在 source terminal 恰好发出一次 |
| §6.2 | Network replay equivalence | DeviceNetworkReplayEquivalenceTests:同一 deterministic fixture(5 000 事件 @1kHz,跨 5 个 1 秒窗)分别走 legacy per-event 与 aggregate 两条路径。字节累计守恒是精确的(legacy_tx == aggregate_tx、rx 同)、两条路径给出相同数量的 1 秒窗(4/4)、逐窗差有派生上界(一个聚合窗的字节,实测最大 1 592 B/s ≈ 0.16%,远未触及 121 623 的上界)、累计差 2 021 B/s ≈ 0.05%。注意:未达到方案 §6.2 字面的"只允许浮点舍入差",原因与实测见 §15.6 偏差 1 |
| §6.3 | GPU fixtures(device-info / schema / 两整数 / notification+auxiliary / data row / unknown shape / stop-flush) | test_build74_gpu_counter_forensics A–T 全过(20 项):形状分离、宽度不匹配与 schema 缺失一律 missing 而非部分解码、未知形状显式 missing 不猜值、census 有界且 overflow 计数、drain 终止幂等、flush 恰好一次 |
| §6.4 | Multi-source synthetic load | test_build74_multi_source_event_loop:source drop = 0、byte 守恒、heartbeat P95 < 100ms / Max < 250ms、display wake P95 < 20ms、无 task 增长、GPU census 存活、source terminal 真实执行(shutdown_immediate == [False]) |
| §6.5 | Closeout | test_build74_closeout_source_terminal A–E 全过:窗口常量冻结校验(== 0.5)、有 backlog 时真实 drain_closeout_tasks 报 completed 且 timedOut = 0 / cancelled = 0、aggregate 尾部与 GPU flush 各恰好一次、subscription release 恰好一次 |
| §6.6 | 全量回归 | Python 827/827 OK(基线 778,本批 +49)、Swift 182 tests / 3 skipped / 0 failures(本批为 §2.4 要求同步了 Swift Registry 的 definitionVersion/rawUnit/normalization,套件数量与结果与基线持平)、知识门禁 [metric-knowledge-gate] PASS: 66 Registry contracts + VitePress 构建 PASS(sync-archive 249 files,build 46.97s)、git diff --check 干净 |
16.2 两条与主机负载相关的测量,以及为什么都不是本批回归
(一)既有的 test_pump_filter_active_fairness. test_g_rejected_flood_keeps_message_bound_fairness(登记在册的负载敏感 flake)。 判据是 reasons[message-limit] > total // 64 - 2(200000 // 64 - 2 = 3123)。 按"交错配对"验证(base a8b8f39 树 ↔ 本批树,同一时段的同一台机):
| 轮 | BASE | SHIP | 当时 load |
|---|---|---|---|
| 1 | 3124 ✅ | 3123 ❌ | 10.9–11.3 |
| 2 | 3124 ✅ | 3124 ✅ | 10.2–10.9 |
| 3 | 3124 ✅ | 3123 ❌ | 9.9 |
| B | 3121 ❌ | 3124 ✅(load 10.04) | 10.0 / 59.5 |
| C | 3102 ❌ | 3102 ❌ | 55.6 / 58.0 |
| D | 3120 ❌ | 3101 ❌ | 51.8 / 45.8 |
| E | 3124 ✅ | 3112 ❌ | 43.1–45.8 |
结论:两棵树在同一高负载轮次里给出几乎完全相同的读数(C 轮 3102 / 3102), 且两棵树都各自既通过又失败(B 轮 BASE 失败而 SHIP 在低负载下通过)。 active-time 是墙钟阈值:宿主停顿落在某个 64 消息 burst 内时,本该记在 message-limit 的那一次让出会被记到 active-time(观测到的 active-time 在各轮间从 0 摆到 43,而 message-limit + active-time 的总和 在 3125 附近浮动)。未放宽任何阈值,也不应把它改成本批判据。 同一时段复跑全量 Python(负载已升到 17.4)为 827/827 OK。
(二)本批新增 closeout 门禁的规模选择。 初版把 GPU backlog 定为 20 000, 实测真实 drain_closeout_tasks 耗时 265–405ms,即占冻结 500ms 窗口的 0.53–0.81;全量套件在 load ≈14 时它就翻成 cancelled。这属于本批自身的 测量设计缺陷(把机器测量钉在阈值边缘),通过改规模而不是改窗口修正:
| GPU backlog | 实测 drain | 是否仍留下真实 backlog |
|---|---|---|
| 1 000 | 0.2–0.3ms | 否(已被 50ms 预热窗吸收) |
| 2 000 | 0.2–111ms | 3/4 次已被吸收 → 不再证明"Stop 时有 backlog" |
| 4 000 | 20–47ms | 是,未观测到被吸收;余量 10–25× |
| 8 000 | 90–132ms | 是,但余量仅 4–5× |
| 20 000 | 351–405ms | 是,但余量 0.5–0.8× ← 初版 |
因此 GPU 用 4 000、Network 保留 20 000——后者恰好是 P0-A 的证据:20 000 条 源端行的 network 场景 drain 只需 0.17–0.19ms,因为聚合已经在 subscription 之下把它们折成约 200 个窗,压力根本没到队列。余量复核:同一 4 000 档在 宿主 load 升到 ≈26 时实测 97.9ms(仍为窗口的 0.20,约 5× 余量), 而在 load ≈7 时是 15–47ms——余量随负载线性收缩,但不再贴边。同时把 P0-D 的策略证明 与窗口证明拆开:新增 test_e_gpu_terminal_semantics_do_not_depend_on_the_drain_window 在不施加 500ms 窗口的前提下证明同一终端的语义(backlog 完整送达、 dropped = 0、immediate = False、flush 恰好一次、流自行结束),这样 "终端的语义"不再依赖任何墙钟测量;窗口本身仍由 Test D 用未放宽的 0.5s 断言。另外修掉测试自身一处缺陷:Task.exception() 在任务被 cancel 时会抛CancelledError,原先的写法会把"超时被取消"报成 ERROR + traceback,而不是 一条指明 drain 结果的断言失败——改为 _task_finished_cleanly()。
16.3 状态与遗留
- 状态
implemented · gate-open(E2 completed / E4 pending)。 不声明validated;不构建 Build 74 真机包;不自跑 E4。 - 下一步(交给用户/独立验收,不是实现 AI):按 §7 矩阵做真机 E4, 重点是——
sourceConsumerTaskCount全 completed /timedOut = 0、closeoutStatus不再timed-out、networkSourceDropCount = 0且networkBytesBalanced = true、gpuPromotable诚实取值(不允许为了好看而 提升)、sourceStageTiming能指出多源竞争落在哪一层、sourceStageTimingAtTimeout为空(因为不应再有超时)。 - 残留风险:GPU 协议本身仍未证明——本批只做到"不猜值、可对账、判决显式", 真正回答"两整数是什么"要靠真机 raw capture(§3.3)与 §3.5 证据齐备。