Skip to content

Build61 综合 E4 后续修复方案 ​

唯一交接文档。 本文件在原 Build59 Screenshot E4 后续方案基础上继续扩展,合并 2026-09-16 Build60 真机复测暴露的 FrameTime 长时连续性与 GPU Counter 数值链问题。交给其他 AI 时只需要读取并执行本文件;不要再把 Screenshot、FrameTime、GPU Counter 拆成互相独立且可能冲突的方案。

0. 本轮范围与最终目标 ​

本轮同时处理五个目标,但必须保持三个子链相互隔离、分别验证:

  1. Screenshot Bug A:未勾选 Screenshot 时,顶部只显示一张图片原地替换,没有实时截图序列;
  2. Screenshot Bug B:勾选 Screenshot 后,落盘截图显示黑屏;真机证据表明实际是 Worker -> Swift PNG 二进制传输损坏,不是设备真的返回黑屏;
  3. Screenshot Optimization C:不保存原始全分辨率 PNG,保持清晰前提下统一压缩为 HEIC;
  4. FTI-008 · FrameTime 长时连续性:Build60 已修复 continuation 顶层 early-return 和旧值无限重复,但真机长窗仍存在秒级 Presented Frame 空洞,必须定位并修复真实逐帧源连续性;
  5. FTI-009 · GPU Counter 数值链与运行中添加语义:GPU Counter 在 Start 时已进入 CapturePlan 的第二轮测试中,GPU 服务、device-info、schema 均成功,但 data row 仍为 0;同时冻结“采集中添加未武装数据源型指标只影响 UI/下次 Session,不动态修改本次 CapturePlan”的产品语义。

Screenshot 最终产品语义保持:

text
Session Active
  -> Screenshot Worker 长连接始终 1 FPS 抓图
  -> 每帧都进入实时截图序列
  -> 未勾选 Screenshot:只保留轻量内存缩略图,不写 Session
  -> 勾选 Screenshot:同一帧额外生成清晰 HEIC 并写入 Session

采集生命周期总原则保持:

text
Prepare = 只准备 transport / capability
Start   = 冻结本 Session CapturePlan + ArtifactPolicy
Recording 期间添加卡片 = 默认只改变显示,不偷偷改变已经冻结的 source activation

Screenshot 子链不得改动 Performance DTX/CoreProfile;FrameTime 子链允许针对 CoreProfile 做有证据的最小修改,但必须保持单 kperf owner,禁止未经验证回退 stop/drain/restart rotation;GPU Counter 子链不得把未知消息形状猜成业务数值。


1. E4 现场事实 ​

1.1 未勾选 Screenshot 的真机 Session ​

  • Session:529997cd-8094-477a-bc28-2959ba08018a
  • 时长约 27.6 s
  • screenshotObservedCount = 25
  • screenshotPersistedCount = 0
  • screenshotPersistenceEnabled = false
  • Session 总大小约 6.16 MiB
  • screenshots/ 无图片序列落盘

结论:采集与“未勾选时不落盘”是正确的;错误发生在 UI 展示语义。

当前 PerfowlWorkbenchView.screenshotPreview 只有在 selectedMetricIDs.contains(.screenShot) 时进入 screenshotTimelinePreview;未勾选时进入 idleScreenshotPreview,只绑定 latestScreenshotData,因此每秒覆盖同一位置。

1.2 勾选 Screenshot 的真机 Session ​

  • Session:a8408f33-9fd0-4796-b607-ace63746268e
  • 时长约 11.9 s
  • observed / persisted = 10 / 10
  • screenshots 目录约 15.48 MiB
  • Session 总大小约 17.98 MiB

10 张文件都有合法 PNG signature,但多张在 IDAT chunk 出现 CRC 错误,Pillow/ImageIO 类解码路径会报 broken PNG / unrecognized data stream contents。

结论:不是“设备截图为黑色”,而是 Worker -> Swift 的二进制 payload 在传输/拼接链中损坏。

当前 Swift readabilityHandler 对每个 pipe chunk 单独创建 Task { await handleIncomingBytes(chunk) }。不同 Task 进入 actor 的调度顺序不能作为字节流顺序契约,因此这条链存在 chunk 重排风险。该设计必须移除。


2. Bug A 修复:未勾选时也显示实时截图序列 ​

2.1 UI 不再根据 Screenshot 是否勾选决定“单图 / 序列” ​

采集中统一:

text
performance.isActive == true
-> 永远显示 screenshotTimelinePreview

Screenshot 是否勾选只改变文案和持久化状态:

text
未勾选:ScreenShot · 实时预览 · 未保存
已勾选:ScreenShot · 实时预览 · 已保存

不得再使用:

text
selectedMetricIDs.contains(.screenShot)

作为是否显示实时序列的条件。

2.2 新增独立的 transient preview timeline ​

不要依赖持久化 events 构造实时序列,因为未勾选 Screenshot 时本来就不应写 screenshot event。

建议 Controller 新增:

swift
LiveScreenshotPreviewItem {
    id
    sequence
    capturedAt
    elapsedSeconds
    thumbnailData
    quality
}

@Published liveScreenshotPreviewItems: [LiveScreenshotPreviewItem]

每个 valid capture 都 append 一项,无论 persistScreenshotSequence 是否为 true。

2.3 只保存压缩 thumbnail Data,禁止长期保存 NSImage / CGImage ​

UI preview 使用:

text
最大边 900 px
HEIC quality 0.60

基于历史真机正常截图 10 张实测:

  • 平均约 24.9 KiB / 张
  • 30 分钟 1800 张约 43.8 MiB 压缩数据

因此实时序列可以完整保留典型 30 分钟测试,而不会出现全分辨率 PNG 的 GB 级内存占用。

禁止将 [NSImage] / [CGImage] 长期放入数组;SwiftUI 用 LazyHStack,只对可见 thumbnail Data 解码。

建议增加双门禁:

text
previewHistoryMaxCount = 3600   // 1 FPS 约 1 小时
previewHistoryMaxBytes = 96 MiB

超过任一上限时只淘汰最旧的预览缩略图;不能影响 Session 已持久化的 Screenshot artifact。典型 30 分钟不应触发淘汰。


3. Bug B 修复:Screenshot binary IPC 必须严格保序且可验证 ​

3.1 移除“每个 chunk 一个 Task” ​

禁止继续:

swift
readabilityHandler {
    let chunk = availableData
    Task { await handleIncomingBytes(chunk) }
}

推荐改成唯一、长期、串行 Reader:

text
1 个 reader loop
-> 顺序 read pipe bytes
-> 顺序 append 到同一个 framing buffer
-> 顺序解析完整 frame
-> 完整 frame 才交给 actor / capture continuation

实现可以使用一个专用串行 DispatchQueue 或一个长期 reader Task;关键门禁是:从 Pipe 读取到 parser buffer 的字节顺序必须与 stdout 原始顺序完全一致,不能存在 per-chunk 并发 Task。

3.2 Worker header 增加 payload 完整性摘要 ​

Python Worker 对每张源 PNG 计算:

json
{
  "byteLength": 2036591,
  "contentSHA256": "..."
}

contentSHA256 只针对 PNG bytes,不包含 header。

Swift 收到完整 frame 后必须依次验证:

  1. pngData.count == byteLength
  2. SHA-256 == contentSHA256
  3. ImageIO 能完整创建 image source / image
  4. 图像宽高 > 0

四项全部成功后才允许:

text
quality = valid

任意一步失败:

text
quality = failed
不生成 thumbnail
不写 Session screenshot
failureCount += 1
下一秒继续采集

不能再以“PNG signature 正确 + 长度够了”作为 valid 条件。

3.3 framing 防御 ​

对 header / image 长度增加合理上限,例如:

text
header <= 64 KiB
image <= 32 MiB

出现非法长度直接 fail-close 当前 Worker 协议,重新建立 Screenshot Worker;不能让错误长度把 parser buffer 无限撑大。


4. Optimization C:产品截图统一改为 HEIC,不保存原始 PNG ​

4.1 原图只允许作为瞬时数据 ​

完整链路:

text
iPhone DVT Screenshot
-> 原始 PNG bytes
-> SHA / decode 完整性验证
-> Mac 端 ImageIO 缩放与 HEIC 编码
-> 原始 PNG 立即释放

禁止:

text
shot-xxxx.png 长期保存
PNG + HEIC 双份保存
原始 PNG 全量进入内存历史数组

4.2 持久化质量参数冻结 ​

推荐默认:

text
format = HEIC
maxPixelSize = 1800 px
lossyQuality = 0.75

对当前 1179×2556 真机截图,相当于约 830×1800,足够看清 App 页面、按钮和文字。

历史正常截图 10 张实测:

text
HEIC ~828px 宽 / Q75
平均约 62.8 KiB / 张
30 min @ 1 FPS ≈ 110.4 MiB

因此验收目标:

text
30 分钟 Screenshot 持久化
preferred: 100~150 MiB
hard gate: <= 200 MiB

相比当前全分辨率 PNG 约 2.7~3.5 GiB / 30 分钟,预计下降约 95%+。

4.3 实时 Timeline thumbnail 参数 ​

无论是否勾选 Screenshot,UI timeline 使用:

text
format = HEIC
maxPixelSize = 900 px
lossyQuality = 0.60

历史样本实测约 24.9 KiB / 张,30 分钟完整 1 FPS 序列约 43.8 MiB 压缩内存。

4.4 编码位置 ​

缩放和 HEIC 编码必须在 Mac Host 完成,不增加被测 iPhone observer effect。

推荐直接使用系统 ImageIO/CoreGraphics,不新增 Pillow runtime、不启动 sips 子进程。

使用一个低优先级串行 Screenshot Encode Queue;每次 capture 必须完成校验/编码后再进入下一拍调度。若处理超过 1 秒:

text
不追帧
不积压 encode job
从完成后重新计算下一次 capture deadline

4.5 HEIC 不可用时的降级 ​

首次编码时检测 HEIC destination 是否可用。

优先级:

text
HEIC Q75
-> 若本机系统编码器明确不可用:JPEG Q80

禁止 fallback 到原始 PNG 持久化,因为会重新引入 GB 级空间占用。

Manifest 必须记录最终实际格式和参数。


5. Session / Review / Export 兼容 ​

新 Session ​

建议文件:

text
screenshots/shot-000001.heic

SessionScreenshot.relativePath 不得再假设 .png。

Manifest 建议增加可选审计字段:

text
screenshotStorageFormat
screenshotStorageMaxPixelSize
screenshotStorageQuality
screenshotOriginalBytesObserved
screenshotPersistedBytes
screenshotPreviewThumbnailBytes

历史 Session ​

旧 .png 必须继续可读,不迁移、不重写。

Review 通过 relativePath / ImageIO 泛型加载 PNG / HEIC / JPEG,不能硬编码 PNG。

如果某个 HTML / 外部导出目标不支持 HEIC:

text
导出时临时转 JPEG

不能因此在 Session 内永久保存 JPEG + HEIC 两份。


6. 推荐修改文件 ​

重点检查并修改:

text
MacApp/Sidecar/perfowl_collector_core/screenshot_worker.py
MacApp/Sources/PerfowlCore/PersistentScreenshotWorker.swift
MacApp/Sources/PerfowlCore/PerformanceSessionController.swift
MacApp/Sources/PerfowlCore/PerformanceSessionStore.swift
MacApp/Sources/PerfowlCore/PerformanceModels.swift
MacApp/Sources/PerfowlCore/SessionReview.swift
MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift
MacApp/Sources/PerfowlApp/SessionReviewViews.swift
MacApp/Tests/PerfowlCoreTests/PersistentScreenshotWorkerTests.swift
MacApp/Sidecar/tests/test_screenshot_worker.py

实施前必须先 git status / git diff。Build60 FrameTime/GPU Counter UI 已落地,但当前仓库仍有其它未提交研究/文档改动,严禁 reset、整文件覆盖或顺带清理无关工作;对重叠文件必须逐 hunk 修改。


7. E2 必须新增的测试 ​

IPC 完整性 ​

  • 2~3 MiB 合法 PNG,随机拆成 4 KiB~64 KiB 多段输入,最终 SHA 必须逐字节一致;
  • 连续 100 帧大 payload,不能出现交叉/重排;
  • 任意翻转 1 byte -> SHA mismatch -> failed,不落盘;
  • 截断 payload -> failed / timeout,不落盘;
  • 非法 header/image length -> fail-close,不允许 buffer 无界增长。

Preview 序列 ​

未勾选 Screenshot:

text
capture 10 frames
-> liveScreenshotPreviewItems = 10
-> UI 走 timeline
-> screenshotPersistedCount = 0
-> screenshots/ 不存在

勾选 Screenshot:

text
capture 10 frames
-> liveScreenshotPreviewItems = 10
-> persisted = 10
-> 每帧设备只 capture 1 次

HEIC ​

  • 每个 persisted artifact 能被 ImageIO/NSImage 正常解码;
  • max pixel <= 1800;
  • preview max pixel <= 900;
  • 新 Session 不生成 .png;
  • 历史 PNG Session 继续正常 Review;
  • HEIC encoder 不可用 fixture -> JPEG fallback,Manifest 诚实记录。

内存/背压 ​

  • 1800 个 preview thumbnail 不保存 decoded NSImage;
  • 受 maxCount/maxBytes 双上限保护;
  • slow encode 不产生 burst catch-up / unbounded queue。

8. E4 真机验收矩阵 ​

至少执行:

A. 未勾选 Screenshot · 5 分钟 ​

必须:

text
顶部形成 1 FPS 实时截图序列
不是单图原地替换
observed > 0
persisted = 0
screenshots 磁盘 = 0

B. 勾选 Screenshot · 5 分钟 ​

必须:

text
所有落盘图片可正常打开
0 张黑屏/损坏/CRC异常
0 SHA mismatch
持久化格式 HEIC(或明确 JPEG fallback)

C. 勾选 Screenshot · 30 分钟 ​

记录:

text
截图张数
平均/P50/P95 单张大小
screenshots 总大小
Encode P50/P95
Mac CPU/RSS
Screenshot Worker CPU/RSS
capture success/failure/reconnect
Performance DTX/CoreProfile drop
Session Stop latency

硬门禁:

text
30min screenshot artifact <= 200 MiB
无持续 RSS 失控
无 screenshot encode backlog
Performance CoreProfile / Startup / FrameTime 无回归

目标值基于历史样本约 110 MiB / 30min。


9. 禁止事项 ​

以下方案均禁止:

  1. 回退到每秒启动一个 pymobiledevice3;
  2. 用视频流替代 1 FPS Screenshot;
  3. 未勾选时写临时全量截图到 Session/磁盘;
  4. 为显示序列长期持有 [NSImage] / [CGImage];
  5. 保存原始 PNG + HEIC 两份;
  6. HEIC 失败时 fallback 到原始 PNG;
  7. 只修黑屏显示,不修字节流完整性;
  8. 只检查 PNG magic/header,不做 SHA + 完整 decode;
  9. 每个 pipe chunk 新建独立 Task;
  10. Screenshot 失败影响 Performance Collector / CoreProfile;
  11. 因 Screenshot 选择变化重新 Prepare App;
  12. Screenshot 子任务改动 Unified CoreProfile 生命周期;FrameTime 子任务若需要改变 CoreProfile 配置,只能按本文件 FTI-008 的证据门禁实施,禁止无证据 stop/drain/restart。

10. 开发记录硬门禁 ​

实施 AI 必须持续更新本文件,不允许“代码完成后再一次性补文档”。

至少追加:

text
## Implementation Update
- 实际修改文件
- 最终 reader 架构
- SHA/完整性校验实现
- HEIC/JPEG 实际参数
- 与本方案偏差及原因

## Validation Update · E2
- Python / Swift 测试数量
- 大 payload / corruption / historical compatibility 结果

## Validation Update · E4
- 两轮/三轮真机 Session ID
- observed/persisted/failure/reconnect
- screenshot 总大小
- 平均/P95 图片大小
- encode P50/P95
- CPU/RSS
- PNG corruption 是否归零

同时同步 site/devlog/index.md。代码通过但没有 devlog 实现与验证记录,不能标记完成。


11. Definition of Done ​

全部满足才允许关闭:

text
[ ] 未勾选 Screenshot 时主页面仍显示实时截图序列
[ ] 未勾选时 screenshots 磁盘写入为 0
[ ] Preview 使用轻量 HEIC thumbnail,不长期存 decoded image
[ ] Preview 有 count + byte 双上限

[ ] Worker -> Swift byte stream 严格保序
[ ] 每帧有 byteLength + SHA256 完整性校验
[ ] ImageIO 完整 decode 通过后才标 valid
[ ] 损坏/截断 frame 不落盘且不中断性能 Session
[ ] 真机黑屏/坏 PNG 问题归零

[ ] 不再持久化原始 PNG
[ ] 默认持久化 HEIC maxPixel=1800 / Q75
[ ] 默认 preview HEIC maxPixel=900 / Q60
[ ] HEIC 不可用只 fallback JPEG,不 fallback PNG
[ ] 历史 PNG Session 继续正常 Review

[ ] 5min 未勾选 E4 通过
[ ] 5min 勾选 E4 通过
[ ] 30min 勾选长测通过
[ ] 30min screenshot artifact <= 200 MiB
[ ] 无 encode backlog / RSS 线性失控
[ ] Performance DTX / CoreProfile / Startup / FrameTime 无回归

[ ] Implementation Update 已写入 devlog
[ ] E2/E4 数字证据已写入 devlog
[ ] site/devlog/index.md 已同步

本轮最终目标不是单纯“把图片压小”,而是形成一条可信且轻量的 Screenshot 产品链:

设备只截图一次;传输逐字节可验证;实时序列始终可见;未选则零磁盘;选中后只保存清晰 HEIC,不保存原图。


Part II · Build60 真机 E4 后续:FrameTime 连续性 + GPU Counter 数值链 ​

12. E4 新现场事实与问题重新定性 ​

12.1 FTI-008 · FrameTime:Build60 修复有效,但只解决了“完全断流 + 假旧值”,没有关闭长时连续性 ​

Build60 已确认修复两项旧问题:

text
frame-continuation 顶层 early-return 已移除                  ✅
无新 Presented Frame 时不再无限重复最后一个 FrameTime       ✅
quality = missing-no-new-frame                               ✅

因此本轮不允许回退这两个修复。

第一轮完成 Session 1a768d0b-2e74-470a-9b25-9be85480e215:

text
CoreProfile start/restart = 1 / 0
CoreProfile chunks        = 33
CoreProfile input coverage≈ 26.75 s
Presented Frame           = 87
Frame event coverage      ≈ 3.16 s
后续产品 sample           = missing-no-new-frame

这证明 continuation decoder 已不再直接冻结旧值,但该轮真实 Presented Frame 仍只覆盖前几秒。

第二轮较长实时采集 Session aa26b20d-5d55-49ab-aafd-59655ee56ae5 更有代表性:

text
CoreProfile chunks                 = 848
CoreProfile observed Raw           ≈ 3.37 GiB
CoreProfile host input coverage    ≈ 131.63 s
startup-lifecycle chunks           = 38
frame-continuation chunks          = 810
CoreProfile start/restart          = 1 / 0

Presented Frame                    = 1744
Frame event timestamp coverage     ≈ 80.36 s
frame interval median              ≈ 16.72 ms
largest Presented Frame gap        ≈ 3.53 s
> 100 ms gaps                      = 32
> 500 ms gaps                      = 25
> 1 s gaps                         = 19

product samples                    = 126
FrameTime valid                    = 92
FrameTime missing-no-new-frame     = 34
missing 时 Graphics FPS > 0        = 19
missing 时 Graphics FPS >= 20      = 14

因此不能把所有 missing 解释为“页面静止没有新帧”。在一部分 FPS=20~38 的时间点仍然没有新的 Presented Frame,说明真实逐帧源存在连续性缺口。

当前问题边界已经从旧 FTI-006:

text
continuation 完全不解析

推进到新的 FTI-008:

text
CoreProfile Raw 持续输入
+ continuation 持续解析
+ freshness 正确
BUT
Presented Frame 在长窗中仍存在秒级空洞

另外第二轮联合模式约 132 秒观察到约 3.37 GiB CoreProfile Raw,平均约 25.6 MiB/s。当前 startup-and-frame 在 Startup 完成后仍保留 Startup lifecycle filters + Presented Frame filter 整场运行,这是必须重点验证的高流量候选因素,但当前证据还不足以直接宣判它就是唯一根因。

12.2 FTI-009 · GPU Counter:第一轮是 CapturePlan 语义,第二轮才是真正数值协议失败 ​

必须严格区分两轮。

第一轮 Session 1a768d0b-2e74-470a-9b25-9be85480e215 中,用户是在采集开始后才添加 GPU Counter 卡片。Start 时冻结的 CapturePlan 实际为:

text
gpuUsage                                      selected
GPU Counter / gpuCounter                     NOT selected
ios.device.gpu.counter.source_native          NOT requested
gpu.counter.raw events                        0

所以第一轮 GPU Counter 无值是符合当前 CapturePlan 架构的预期行为,不能作为 decoder 失败证据。

第二轮沿用了第一次结束时的指标选择,Start 时已经包含:

text
gpuCounter                                    selected
ios.device.gpu.counter.source_native          requested
perfowl.gpu-counter.raw                       activated

第二轮真实收到:

text
gpu.counter.raw total        = 1248
device-info                  = 1
schema-ready                 = 1
decoded data row             = 0
unsupported-message          = 1246

设备信息明确返回 A16、min-collection-interval = 10000,并返回动态 Counter schema,其中包含 GRC_TIMESTAMP、GRC_GPU_CYCLES 等 key。也就是说:

text
GPU DTX service connected ✅
device-info              ✅
schema                   ✅
大量后续消息              ✅
真正 counterValues       ❌

当前 1246 条后续消息在 Perfowl 归一化前观察为:

text
pythonType = list
length = 2
itemTypes = [int, int]

而 GPUCounterDecoder.consume() 当前硬编码:

python
if not isinstance(message, (list, tuple)) or len(message) < 5:
    return unsupported-message

所以所有两整数消息都被直接拒绝。

但严禁因此把 [int, int] 直接猜成 timestamp + gpuCycles 或任意 Counter 数值。它们可能是 heartbeat/control,也可能说明真正 data payload 在 DTX payload/aux/selector 的另一层没有被当前 bridge 正确保留下来。

12.3 GPU Counter configure 参数存在高风险疑点,必须先验证再修改 ​

当前 selector 合同是:

text
configureCounters:counterProfile:interval:windowLimit:tracingPID:

本地方法签名也明确为:

text
counters
counter_profile
interval
window_limit
tracing_pid

但当前生产调用为:

python
minimum = device_info["min-collection-interval"]
configure(
    PInt64(minimum),  # 当前传到 counters
    PInt64(3),
    PInt64(1),        # 当前 interval 固定 1
    PInt64(0),
    PInt32(-1),
)

且现有单测把 (minimum, 3, 1, 0, -1) 当成“frozen profile parameters”锁死。

这存在非常强的参数语义/顺序错配风险:设备的 min-collection-interval 从命名上更像 interval 候选,而当前却传入 counters。但仅凭命名仍然不能直接交换参数。

实施 AI 必须先通过至少一种独立证据确认真实调用合同:

  1. 项目中冻结的 py-ios-device / Instruments 参考实现;或
  2. 可复现的真实 DTX wire capture;或
  3. 与当前 pymobiledevice3 版本一致的经过验证参考。

只有证据确认后才能改 configure 参数。禁止“因为看起来合理”直接把 minimum 从第 1 个参数换到第 3 个参数。


13. FTI-008 最终修复方案:先证明瓶颈,再收窄 Unified CoreProfile 长时负载 ​

13.1 架构硬边界 ​

继续保持:

text
Startup + FrameTime
-> 单 CoreProfile/kperf owner
-> Start 时一次启动
-> startCount = 1
-> restartCount = 0
-> 正常 sourceEpoch = 1

禁止回退旧架构:

text
Startup complete
-> stop
-> drain
-> restart frame-only CoreProfile

否则 FTI-001 的首值延迟会重新出现。

同时继续保持 Build60 freshness:

text
没有新 Presented Frame
-> frameTimeMs = nil
-> quality = missing-no-new-frame

禁止使用:

text
1000 / Graphics FPS

伪造 FrameTime。

13.2 Phase F1:增加足够的逐帧源连续性遥测 ​

在修改 CoreProfile 配置前,先增加以下只读遥测,必须能按 Session 导出:

text
coreProfilePostStartupObservedBytes
coreProfilePostStartupChunkCount
coreProfilePostStartupBytesPerSecond
presentedFrameCount
presentedFrameCoverageSeconds
presentedFrameLargestGapMs
presentedFrameGapOver100msCount
presentedFrameGapOver500msCount
presentedFrameGapOver1000msCount
frameTimeMissingSampleCount
frameTimeMissingWhileGraphicsFPSPositiveCount
frameTimeMissingWhileGraphicsFPSAtLeast20Count
deviceLostEventMarkerCount / TRACE_LOST_EVENTS
hostDroppedEventHighWater
activeKdebugFilters
coreProfileConfigGeneration

不得把这些遥测混进指标本身;它们是诊断/验证元数据。

13.3 Phase F2:真机 A/B,先判断是否与联合 Startup filters 强相关 ​

同一设备、同一 App、相同操作脚本,至少各跑 2~5 分钟:

A · FrameTime-only ​

text
CoreProfile mode = frame-only
filters = {PRESENTED_FRAME_FILTER}

B · Startup + FrameTime ​

text
CoreProfile mode = startup-and-frame
filters = LAUNCH_KDEBUG_FILTERS ∪ PRESENTED_FRAME_FILTER

必须对比:

text
CoreProfile MiB/s
Presented Frame count
Presented Frame coverage / Session duration
largest gap
>100ms / >500ms / >1s gap count
FrameTime missing ratio
missing while Graphics FPS > 0 / >=20
TRACE_LOST_EVENTS
host drop
CPU/RSS/Stop latency

判定 ​

若 A 长窗显著连续,而 B 出现当前秒级空洞,同时 B 的 CoreProfile byte rate 明显更高,则将“Startup filters 长时保留导致 CoreProfile 高负载/帧事件连续性恶化”升级为已验证主因。

若 A 同样出现类似空洞,则停止 filter 收窄实现,继续调查 Presented Frame filter / kdebug source 本身,不得为了赶进度执行无依据的 reconfigure。

13.4 Phase F3:只有证据成立时,验证“同 Tap 原位收窄 filter”能力 ​

优先方案不是 restart,而是:

text
Start:
  filters = Startup + Presented Frame

Startup report frozen:
  同一个 CoreProfile Tap
  尝试设备支持的原位 setConfig / filter narrowing
  filters -> Presented Frame only

Session Stop:
  正常 stop 一次

实施前必须先证明当前 CoreProfile service / pymobiledevice3 11.3.1 在 recording 中调用 setConfig: 的真实行为:

text
不 stop recording
不重新 acquire kperf
不产生第二 sourceEpoch
不要求第二 RAW-v2 header
不清空 thread/timebase parser state
Presented Frame timestamp 继续单调

如果设备/服务不支持这种原位 reconfigure,或实测会隐式重启/切换 source,则不得上线该方案。

此时保持现有诚实 missing,并把替代真实逐帧源研究作为后续独立任务;禁止偷偷恢复 stop/drain/restart。

13.5 Phase F4:若原位收窄可用,实施时必须保持一个 decoder 状态机 ​

不要创建第二个 Frame decoder。

推荐:

text
LaunchTraceStreamDecoder
  -> Startup阶段解析 lifecycle + Presented Frame
  -> Startup结果冻结
  -> CoreProfile configGeneration += 1(仅配置代,不是 sourceEpoch)
  -> 后续只保留 Presented Frame filter
  -> 同一个 RAW parser / timebase / previousFrameTimestampNs 继续

配置切换边界必须记录:

text
configGeneration
beforeFilters
afterFilters
reconfigureRequestedAtNs
reconfigureAcknowledgedAtNs
firstFrameAfterReconfigureNs
reconfigureGapMs

不能把 config generation 当 source epoch,也不能重置 _previous_frame_timestamp_ns 造成假的超大/超小第一帧间隔。

13.6 FTI-008 E2 ​

至少新增:

text
[ ] A/B telemetry aggregation 单测
[ ] configGeneration 与 sourceEpoch 独立
[ ] Startup report frozen 后任何 continuation/config change 不修改启动结果
[ ] reconfigure 前后 Presented Frame 不重复
[ ] previousFrameTimestamp 连续
[ ] 无新帧仍 missing-no-new-frame
[ ] reconfigure 失败时不 restart CoreProfile,Session 其余指标继续
[ ] startCount=1 / restartCount=0 contract

若最终未实施原位 reconfigure,也必须保留 F1/F2 诊断能力和证据,并把状态写成 blocked/needs-source-investigation,不能伪称完成。


14. FTI-009 最终修复方案:GPU Counter data-row 协议必须从 DTX envelope 开始取证 ​

14.1 Phase G1:先记录原始 DTX envelope census,不能只看合并后的 list ​

当前 _merge_gpu_notification() 可能把 payload 和 auxiliary 过早拍平成 list,导致无法判断两整数消息真正来自哪里。

增加仅诊断/可关闭的结构遥测,至少记录:

text
deliveryKind = dispatch | notification
selector(若有)
payload python type
payload list length
payload item types
aux count
aux item types
binary payload byte length(若有)
首层整数值(有界,不记录大二进制正文)
message sequence
host receive monotonic ns

对于 binary 数据只记录长度/hash/shape,禁止把整块 GPU Counter binary 复制进日志造成新的磁盘问题。

目标是明确区分:

text
schema message
heartbeat/control message
真正 data notification
flush/tail message

14.2 Phase G2:核对 configure 参数合同 ​

对照可信参考,明确:

text
configureCounters:
counterProfile:
interval:
windowLimit:
tracingPID:

每个参数的业务含义、设备单位和推荐值。

重点验证当前:

text
(minimum, 3, 1, 0, -1)

是否错误。

若确认参数错位,修复生产代码并删除/更新把错误 tuple 冻结为正确行为的现有单测。

新测试必须用参数语义名称断言,而不是只断言一个神秘 tuple,例如:

text
counterSet = ...
counterProfile = ...
interval = deviceMinCollectionInterval
windowLimit = ...
tracingPID = -1

实际数值仍以验证后的参考合同为准,不能在方案阶段猜死。

14.3 Phase G3:保留 DTX 结构直到 decoder 能明确识别 message kind ​

不要在 service bridge 层把所有消息都无条件合并成同一种 list。

推荐内部 envelope:

python
GPUCounterWireEnvelope(
    delivery_kind,
    selector,
    payload,
    auxiliary,
    sequence,
)

Decoder 根据经过真机验证的消息合同解析:

text
schema -> decodeOrder
heartbeat/control -> explicit non-data event
data -> counter bytes -> source-native counterValues
unknown -> unsupported-message + shape evidence

两整数 [int, int] 只有在真实 wire/参考实现证明语义后才能命名;否则继续 unknown-control / unsupported,不能冒充 data。

14.4 Phase G4:用匿名真机 fixture 锁定协议 ​

一旦识别到第一条真实 data row,必须冻结一份去设备身份、去 App 身份的最小 fixture,覆盖:

text
device-info
schema
至少一条 heartbeat/control
至少两条 data row(用于 delta/warmup)
flush/tail(如存在)

测试必须证明:

text
schema -> decoder ready
first data -> warmup/absolute 语义正确
second data -> delta counter 正确
未知消息 fail-closed
counter key 全量保留
不猜 percent/ms/MHz 等工程单位

GPU Counter 在完成设备公式校准前继续标:

text
scope = device
unit = source-native-count
quality = candidate/degraded/missing

14.5 GPU Counter UI 状态必须诚实区分“没选 / 没武装 / 等 schema / 无 data / 有值” ​

专用 GPUCounterPanel 已经保留,新增状态提示:

text
not-selected
next-session-only
activating
schema-ready-no-data
data-ready
source-failed

不能把所有情况都表现成一个空白图表。


15. 采集中添加指标的统一产品语义 ​

这次第一轮 GPU Counter 测试暴露的是交互语义缺口,不是采集 Bug。

15.1 CapturePlan 继续在 Start 时冻结 ​

不得为了“采集中点了添加”动态改写当前 Session 的 CapturePlan。

原因:某些源存在独占资源、启动配置、目标绑定或高成本副作用。动态启动会破坏可复现性,也可能触发 Prepare/CoreProfile/kperf 生命周期变化。

15.2 添加卡片时按“当前 Session 是否已经采集该数据”分类 ​

A. 数据源已经被当前 Session 武装 ​

例如某指标虽然当前没显示,但它对应 source 已经 active 且完整数据一直在采:

text
采集中添加卡片
-> 立即显示当前/历史已采数据

B. 数据源 Start 时未武装 ​

例如第一轮 GPU Counter:

text
Start 时未请求 ios.device.gpu.counter.source_native
-> 采集中添加 GPU Counter 卡片
-> 不动态启动 perfowl.gpu-counter.raw
-> 卡片显示:本次未采集 · 下次测试生效
-> 选择状态保留到下一 Session

FrameTime 等需要专门 CoreProfile source 的指标同理。

C. Artifact Policy ​

Screenshot 特殊:Live Screenshot Preview 整场始终存在,但“是否持久化 Screenshot Sequence”是 Start 时冻结的 ArtifactPolicy。

因此采集中才勾选 Screenshot:

text
实时预览继续存在
本 Session 不从中途开始保存
UI提示:截图保存将在下次测试生效

除非未来另立产品决策支持 artifact policy 动态切换;本轮不做。

15.3 UI 判断依据 ​

不要仅看 selectedMetricIDs。

Controller/Workbench 必须能读取当前 Session 冻结事实:

text
activeCapturePlan.requestedMetricIDs
activeCapturePlan.executionSourceIDs
activeSessionArtifactPolicy

卡片展示状态由:

text
用户当前想显示什么
+
本 Session 实际采了什么

共同决定。

不得出现“卡片加出来了,所以用户自然以为正在采”的误导。


16. 综合推荐修改文件 ​

实施 AI 先按最新代码确认实际路径,不要盲目照列表整文件覆盖。

Screenshot ​

text
MacApp/Sidecar/perfowl_collector_core/screenshot_worker.py
MacApp/Sources/PerfowlCore/PersistentScreenshotWorker.swift
MacApp/Sources/PerfowlCore/PerformanceSessionController.swift
MacApp/Sources/PerfowlCore/PerformanceSessionStore.swift
MacApp/Sources/PerfowlCore/PerformanceModels.swift
MacApp/Sources/PerfowlCore/SessionReview.swift
MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift
MacApp/Sources/PerfowlApp/SessionReviewViews.swift
MacApp/Tests/PerfowlCoreTests/PersistentScreenshotWorkerTests.swift
MacApp/Sidecar/tests/test_screenshot_worker.py

FrameTime ​

text
MacApp/Sidecar/perfowl_collector_core/realtime.py
MacApp/Sidecar/perfowl_collector_core/launch_trace.py
MacApp/Sidecar/perfowl_collector_core/adapters.py
MacApp/Sidecar/perfowl_collector_core/frame_derivation.py(仅确有需要)
MacApp/Sidecar/tests/test_frametime_continuation.py
新增 FrameTime continuity / config narrowing 专项测试

GPU Counter ​

text
MacApp/Sidecar/perfowl_collector_core/pmd3_deep_services.py
MacApp/Sidecar/perfowl_collector_core/gpu_counter.py
MacApp/Sidecar/perfowl_collector_core/adapters.py
MacApp/Sidecar/perfowl_collector_core/realtime.py
MacApp/Sidecar/tests/test_perfowl_gpu_counter_service.py
GPU Counter decoder/fixture 专项测试
MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift
MacApp/Sources/PerfowlCore/PerformanceSessionController.swift

重叠文件 realtime.py、PerformanceSessionController.swift、PerfowlWorkbenchView.swift 必须串行、逐 hunk 合并,不能让三个子任务互相覆盖。


17. 综合实施顺序 ​

严格建议按以下顺序,避免把多个未知因素一次混在一起:

text
Phase 0
读取 AGENTS.md / git status / git diff
确认当前工作树和其他 AI 改动

Phase S1
Screenshot Pipe 严格串行 reader
+ byteLength/SHA/ImageIO 完整性

Phase S2
HEIC 1800/Q75 persistence
+ 900/Q60 transient timeline
+ 历史 PNG compatibility

Phase F1
FrameTime continuity telemetry

Phase F2
frame-only vs startup-and-frame 真机 A/B

Phase F3
只有 A/B 支持假设时,验证同 Tap 原位 filter narrowing

Phase F4
只有设备证明安全时才实现 narrowing
否则记录 blocked,不准回退 rotation

Phase G1
GPU Counter DTX envelope census

Phase G2
核对 configure 参数合同

Phase G3
修 service bridge / decoder
+ 匿名真实 fixture

Phase G4
Workbench 的 next-session-only / schema-ready-no-data 状态

Phase I
全量 E2

Phase E4
Screenshot + FrameTime + GPU Counter 真机联合验收

FrameTime F2 和 GPU G1/G2 属于必须先取证再改代码的阶段。另一个 AI 不得把“方案要求修复”理解成“必须猜一个实现然后提交”。证据否定假设时,正确动作是停止该分支、记录结果并进入下一候选,而不是硬改。


18. 综合 E2 测试门禁 ​

除前文 Screenshot E2 外,新增:

FrameTime ​

text
[ ] continuation early-return 不复发
[ ] freshness 不复发
[ ] continuity telemetry 计算正确
[ ] gap histogram / missing-with-FPS 统计正确
[ ] configGeneration != sourceEpoch
[ ] 若支持原位 narrowing:切换前后 parser/timebase/frame timestamp 连续
[ ] narrowing 失败不会 restart CoreProfile
[ ] Startup report 完成后保持冻结

GPU Counter ​

text
[ ] DTX dispatch/notification envelope 不丢 selector/payload/aux 结构
[ ] configure 参数使用经过证据确认的语义顺序
[ ] schema / heartbeat / data / unknown 明确分类
[ ] unknown 两整数消息不被猜成 counterValues
[ ] 真实匿名 fixture 可稳定解析至少两条 data row
[ ] warmup/delta/reset 逻辑有测试
[ ] source-native unit 保持诚实

采集中添加指标 ​

text
[ ] Start 时未武装 GPU Counter,中途添加 -> source 不启动
[ ] UI 显示“本次未采集 · 下次测试生效”
[ ] 下一 Session Start -> GPU Counter 正式进入 CapturePlan
[ ] 已经 active 的普通指标中途加卡 -> 可立即显示
[ ] Screenshot persistence 中途勾选 -> 本 Session 不从中途落盘,明确提示下次生效

19. 综合 E4 真机验收矩阵 ​

19.1 Screenshot ​

继续执行前文 A/B/C:

text
A 未勾选 5min:实时序列正常,persisted=0
B 勾选 5min:0 损坏图,HEIC/JPEG fallback诚实
C 勾选 30min:截图 <= 200 MiB,无 RSS/encode backlog

19.2 FrameTime ​

D · Baseline A/B ​

text
FrameTime-only 2~5min
Startup+FrameTime 2~5min

必须把 F2 全部数字写入 devlog。

E · 修复后 5min ​

硬门禁:

text
CoreProfile startCount = 1
restartCount = 0
正常 sourceEpoch = 1
FrameTime 不重复旧值
Graphics FPS >=20 时不应持续出现秒级 Presented Frame 空洞
无新的 host drop / device loss 回归
Startup report 不被 continuation 污染

“偶发页面静止导致无 Presented Frame”允许诚实 missing;不能要求人为制造每秒都有 FrameTime。

F · 修复后 15~30min ​

记录:

text
CoreProfile post-startup MiB/s
Presented Frame coverage ratio
largest gap
>100/500/1000ms gaps
FrameTime missing ratio
missing while FPS >0 / >=20
CPU/RSS
Stop latency

若仍有秒级 gap,FTI-008 不得关闭。

19.3 GPU Counter ​

G · 运行中添加语义 ​

text
Start 时不选 GPU Counter
-> Recording 中添加卡片
-> perfowl.gpu-counter.raw 不应中途启动
-> 明确 next-session-only

H · 下一 Session 真正采集 ​

下一轮 Start 前保持 GPU Counter 选中:

text
CapturePlan 包含 ios.device.gpu.counter.source_native
source activated
设备 info/schema ready
decoded data row > 0
产品 sample gpuCounters 非空

至少运行 2 分钟,确认:

text
不是只出一帧后 stale
Counter key schema 稳定/变化处理正确
当前选择曲线能持续更新
Session/Export 保留完整 counter dictionary

如果仍只有 heartbeat/schema,没有 data row,则必须保留 schema-ready-no-data,FTI-009 不得关闭。


20. 综合禁止事项 ​

除 Screenshot 原禁止项外,再增加:

  1. 禁止为了 FrameTime 连续性使用 1000/FPS 合成 FrameTime;
  2. 禁止未经 A/B 证据直接认定 Startup filters 是唯一根因;
  3. 禁止未经真机验证在运行中调用 setConfig: 并假定不会重启 kperf;
  4. 禁止恢复 Startup -> stop/drain -> FrameTime restart rotation;
  5. 禁止把 configGeneration 冒充 sourceEpoch;
  6. 禁止看到 GPU [int,int] 就自行定义为 timestamp/cycles;
  7. 禁止仅凭参数名称猜测并交换 configure 参数,必须有独立参考/wire 证据;
  8. 禁止为了 GPU Counter 数值去执行设备侧 JavaScript 或引入不可审计公式;
  9. 禁止采集中动态修改已经冻结的 CapturePlan 来迎合新加卡片;
  10. 禁止用空图表隐瞒 not-collected / next-session-only / schema-ready-no-data / source-failed 状态;
  11. 禁止 Screenshot/FrameTime/GPU 任一子任务失败拖垮 CPU/Memory/Network 等主 Session;
  12. 禁止 reset/覆盖当前其它未提交工作。

21. 综合开发记录硬门禁 ​

本文件现在是三个子链的唯一后续实施记录入口。

实施 AI 必须在本文件继续追加:

text
## Implementation Update
### Screenshot
- reader / integrity / HEIC / timeline 实际实现

### FrameTime
- A/B 结果
- 是否验证 Startup filters 主因
- 是否支持同 Tap reconfigure
- 最终实现或 blocked 原因

### GPU Counter
- DTX envelope census
- configure 参数最终合同与证据来源
- decoder 最终 message taxonomy
- decoded data row 数量
- 采集中添加指标语义实现

## Validation Update · E2
- Python / Swift 全量数字
- 各专项测试数字
- 失败与修正

## Validation Update · E4
- 全部 Session ID
- Screenshot 大小/损坏数
- FrameTime gap/coverage/raw-rate
- GPU schema/data row/sample 数
- CapturePlan 中途添加行为

同时必须同步:

text
site/devlog/index.md
internal/field-test-issues.md

如果部分任务 blocked / paused,也必须当天写明证据、停止条件和下一步;禁止只记录成功。


22. 综合 Definition of Done ​

原 Section 11 的 Screenshot DoD 全部继续有效,并额外要求:

text
FTI-008 FrameTime
[ ] Build60 continuation/freshness 修复不回归
[ ] 完成 frame-only vs startup-and-frame 真机 A/B
[ ] 已用证据确认或排除 Startup filters 长时负载假设
[ ] 若实施原位 filter narrowing,设备已证明不产生 stop/restart/sourceEpoch变化
[ ] Startup report 继续冻结
[ ] 5min 真机无“FPS持续有值但 FrameTime连续秒级空洞”回归
[ ] 15~30min 长测连续性数据已记录
[ ] startCount=1 / restartCount=0

FTI-009 GPU Counter
[ ] 第一轮“中途添加未武装指标”的语义已在 UI 明确
[ ] 第二轮真实 GPU Counter source 能区分 schema/control/data
[ ] configure 参数合同有独立证据
[ ] 不再把未知消息统一丢成无解释的 unsupported-message
[ ] 至少真实解析出持续 data row,而不是只有 schema/heartbeat
[ ] gpuCounters 产品 sample 持续非空
[ ] source-native 单位诚实,不猜工程单位
[ ] Session/Export 保留完整 counter dictionary

运行中添加指标
[ ] 未武装 source 中途添加只显示 next-session-only
[ ] 不动态修改当前 CapturePlan
[ ] 下一 Session 正确继承用户选择并启动 source
[ ] 已武装 source 的纯显示卡片仍可立即添加

共同
[ ] Sidecar全量测试通过
[ ] Swift全量测试通过
[ ] Screenshot 5/30min E4完成
[ ] FrameTime 5/15~30min E4完成
[ ] GPU Counter 下一 Session >=2min E4完成
[ ] 无 CoreProfile/DTX/Stop/RSS 新回归
[ ] 本文件 Implementation/E2/E4 已更新
[ ] site/devlog/index.md 已同步
[ ] internal/field-test-issues.md 已同步

本综合批最终目标:

Screenshot 要可信且轻量;FrameTime 要真实、连续但不伪造;GPU Counter 要从真实设备协议得到可审计数值;Recording 中临时添加卡片必须明确区分“只是显示”与“本 Session 真正已经采集”。


23. Implementation Update(2026-09-16,续接实现) ​

原实施 AI 在额度耗尽前只留下两处脚手架(screenshot_worker.py 增加 contentSHA256 计算、PersistentScreenshotWorker.swift 增加空的 readerTask 属性),核心工作全部未完成。本轮补齐 Screenshot(S1/S2)、FrameTime 只读遥测(F1)、采集中添加指标语义(Section 15);FrameTime A/B(F2/F3/F4)与 GPU Counter 协议(G1/G2/G3/G4)需要真机才能继续,本轮诚实标记为 blocked,未编码猜测。

Screenshot(S1 + S2) ​

S1:串行 reader + 完整性校验 ​

  • MacApp/Sources/PerfowlCore/PersistentScreenshotWorker.swift:移除“每个 pipe chunk 一个 Task”设计(无法保证进入 actor 的顺序),改为 readabilityHandler 同步 yield 进 AsyncStream,由唯一长期 readerTask 顺序消费并调用 handleIncomingBytes。
  • 新增 framing 长度上限(header ≤ 64 KiB,image ≤ 32 MiB),越界立即 failCloseFraming:关闭连接、清空 buffer、isRunning=false,不等待永远不会到来的字节;PerformanceSessionController 的采集循环感知 isRunning=false 后自动回退单次截图,不影响 Performance Collector。
  • 新增四步完整性校验 verifyFrame:byteLength 匹配 → SHA-256 匹配(Worker 侧 screenshot_worker.py 已算好 contentSHA256)→ ImageIO 完整解码 → 宽高 > 0;不再以“PNG signature 正确”作为 valid 条件。
  • PerformanceSessionStore.validateScreenshotAttachments 原先硬编码 PymobiledeviceScreenshotClient.isPNG,改为格式无关的 isDecodableImage(ImageIO 解码),因为新 Session 落盘 HEIC/JPEG,旧 Session 落盘 PNG。

S2:HEIC 持久化 + 实时预览 + 历史兼容 ​

  • 新文件 MacApp/Sources/PerfowlCore/ScreenshotImageEncoder.swift:ImageIO 降采样 + 有损编码,优先 HEIC,仅在本机无 HEIC 编码器时退化 JPEG,绝不退化 PNG。
  • 新文件 MacApp/Sources/PerfowlCore/LiveScreenshotPreviewItem.swift:与 SessionScreenshot(持久化事件)完全独立的瞬时内存结构,带 thumbnailData,不落盘。
  • PerformanceSessionController:新增 liveScreenshotPreviewItems(previewHistoryMaxCount=3600 / previewHistoryMaxBytes=96MiB 双上限),在 captureScreenshots 循环内对每一次有效截图(不论是否勾选保存)编码 900px/Q60 缩略图并入队——修复 Bug A(未勾选时只有单图原地替换)。编码内联在既有单线程采集循环里,天然与拍摄节奏串行,不产生 backlog。
  • PerformanceSessionStore.appendScreenshot:持久化改为 1800px/Q75 HEIC(或 JPEG),文件名 shot-NNNNNN.heic/.jpg;SessionScreenshot 新增 storageFormat/maxPixelSize/encodeQuality/originalByteCount(decodeIfPresent,历史 PNG Session 全部解码为 nil,不臆断格式);PerformanceSessionManifest 新增 screenshotStorageFormat/StorageMaxPixelSize/StorageQuality/OriginalBytesObserved/PersistedBytes/PreviewThumbnailBytes 审计字段。
  • PerfowlWorkbenchView:screenshotPreview 门禁从 selectedMetricIDs.contains(.screenShot) 改为 performance.isActive(与是否勾选保存完全解耦);screenshotTimelinePreview 改为纯内存渲染(LivePreviewScreenshotCard 直接 NSImage(data:)),不再依赖 currentSessionURL 才能显示;旧的磁盘驱动 LiveScreenshotCard/LiveScreenshotTimelineItem/liveScreenshotItems 已随之移除(liveScreenshotPoints/LiveScreenshotIndex 保留,供持久化历史场景使用,不受影响)。

FrameTime 只读遥测(F1) ​

  • MacApp/Sidecar/perfowl_collector_core/realtime.py:
    • RealtimeMetricProjector 新增 frame_continuity_telemetry():frameTimeMissingSampleCount / WhileGraphicsFPSPositiveCount / WhileGraphicsFPSAtLeast20Count,在 frameTimeMs 判定为 missing-no-new-frame 的同一采样点与已计算好的 framesPerSecond 关联,诊断字段不进入指标本身。
    • _track_frame_telemetry 扩展:presentedFrameCount、presentedFrameLargestGapMs、Over100/500/1000msCount(基于 Presented Frame 时间戳差)。
    • consume() 在 coreProfileStreamRole == "frame-continuation" 分支新增 coreProfilePostStartupObservedBytes/ChunkCount,用于计算 coreProfilePostStartupBytesPerSecond。
    • 会话最终 manifest 新增以上全部字段 + hostDroppedEventHighWater(复用既有 bridge_live_drop_total)+ coreProfileConfigGeneration=1(占位;本轮未实现同 Tap 原位 reconfigure,见下)。
  • 未做(诚实边界):deviceLostEventMarkerCount/TRACE_LOST_EVENTS、activeKdebugFilters 未新增会话级重复字段——前者已在 startupObservability.deviceLostMarkerCount 存在,后者已在 core_profile_configured 事件的 kdebugFilters 存在,不重复造字段。

FrameTime A/B 与原位收窄(F2/F3/F4)—— blocked,需要真机 ​

按方案 §13.3 的规定,必须先用真机数据证明或证伪“Startup filters 长时负载”假设,才允许动 CoreProfile 配置。本环境没有连接的 iOS 设备,无法执行 frame-only vs startup-and-frame 的 2~5 分钟真机 A/B。F1 已经把 F2 需要比对的全部字段准备好(presentedFrameCoverageSeconds 等由 manifest 直接算得,无需额外工具)。F3(同 Tap 原位 filter 收窄)、F4(实现)严格依赖 F2 证据,未动。

GPU Counter 协议(G1/G2/G3/G4)—— G1 已有基线诊断,G2/G3/G4 blocked ​

  • 复核代码发现 Build 60 基线(非本轮新增)在 gpu_counter.py::_message_shape 已经对不满足 len(message)>=5 的消息记录 pythonType/length/itemTypes/nestedLengths,G1 诉求的“结构级遥测”已有部分覆盖。
  • 未做且必须诚实说明原因:
    • G2(configure 参数合同):pmd3_deep_services.py::GPUCounterControlService.configure_counters_counter_profile_interval_window_limit_tracing_pid_ 当前把设备 min-collection-interval 传给第 1 个参数 counters、interval 固定为 1,方案明确要求“只有证据确认后才能改”。本环境搜索了仓库全部 vendored 源码(含冻结的 pymobiledevice3-11.3.1 runtime)与 MacApp/dist,没有找到任何独立的 py-ios-device/Instruments 参考实现或真实 DTX wire capture可以证实或证伪当前参数顺序。没有证据,按方案红线不能改,也不能是“看起来更合理就换”。
    • G3(decoder message taxonomy):依赖 G2 的参数合同才能正确解释后续两整数消息的语义,G2 未解,G3 未动。
    • G4(匿名真机 fixture):需要真实设备的 GPU Counter data row,本环境无设备,未做。
  • 进一步动 G1(在 _merge_gpu_notification 之前保留 payload/aux/selector 分离)需要改造 DTX 桥接层的消息队列形状,且只有连着真机才能验证没有破坏实际收发——在没有设备验证能力的前提下对这段代码做结构改动风险大于收益,本轮未做。

采集中添加指标统一语义(Section 15) ​

  • PerformanceSessionController 新增 @Published activeCapturePlan: CapturePlan?(Start 时冻结的真实 CapturePlan 快照)与 activeScreenshotPersistenceEnabled: Bool(Start 时冻结的 ArtifactPolicy),以及 isArmedThisSession(_:) 判定某 WorkbenchMetricID 对应的 registry metric ID 集合是否已被当前 Session 的冻结 CapturePlan 完整覆盖。
  • PerfowlWorkbenchView 的“添加指标”弹层:勾选了但本 Session 未武装的数据源型指标(如中途勾 GPU Counter)显示“本次未采集 · 下次测试生效”;ScreenShot 单独判定 activeScreenshotPersistenceEnabled,未冻结时显示“截图保存将在下次测试生效”;screenshotTimelinePreview 底部“已保存/未保存”也改用冻结状态而非实时勾选状态,避免中途勾选却谎称“已保存”。
  • 未动 selectedMetricIDs 本身的可编辑性(Build54 已保证录制中仍可自由增删卡片,只影响显示),也未让任何选择变化重新触发 Prepare/CapturePlan 重算——完全符合“CapturePlan 只在 Start 冻结”的既有红线。

24. Validation Update · E2 ​

Swift:  167/167 通过(3 项显式基准跳过),swift build 与 swift test 均无警告新增(除既有 1 处不相关的 nil-coalescing 提示)
Python: 430/430 通过(含 48 项 subtests),pytest via `uv run --python 3.12 --with pytest --with-requirements requirements-core-candidate.txt`

新增专项测试:

Screenshot IPC(真实子进程 fixture,非 mock):
  testFragmentedDeliveryReassemblesByteExact  —— 2MiB+ payload 拆成 4KiB 片段乱序风险下仍逐字节一致
  testShaMismatchIsRejected / testByteLengthMismatchIsRejected / testCorruptedImageFailsDecodeVerification
  testOversizedHeaderFailsClosesConnection —— 越界 framing 声明触发 fail-close 而非无界 buffer
  testWorkerCrashMidFrameDoesNotHangOrCorruptNextCapture

ScreenshotImageEncoderTests:HEIC/JPEG 二选一不退化 PNG、编码产物自解码、拒绝不可解码输入、maxPixelSize 生效

Screenshot 持久化契约:
  testMockWorkerPersistedScreenshotsWhenEnabled 扩展断言 .heic 扩展名、manifest 审计字段、附件二次解码
  testHistoricalScreenshotWithoutStorageFormatFieldsDecodesCleanly —— 历史 PNG Session 缺字段按 nil 解码

Bug A 回归:
  testControllerPopulatesLivePreviewEvenWhenNotPersisting —— 未勾选 ScreenShot 时仍形成实时序列,且不落盘任何 screenshot 事件

FrameTime F1:
  test_frame_continuity_telemetry_distinguishes_idle_from_source_gap —— FPS 健康时的 missing-no-new-frame 计入"疑似源缺口",FPS=0 时不计入

Section 15:
  testMidSessionCardAddDoesNotArmUnrequestedSource —— 中途选中未武装的数据源,CapturePlan/Session 落盘均确认未被动态改写

失败与修正:两处历史测试 fixture(SessionArtifactsTests.swift、PerformanceFoundationTests.swift)用的是“仅 PNG signature + 1 字节”的假图片,S1/S2 上线后这类数据会被 ImageIO 正确拒绝(这正是本轮要修的问题本身,不是回归)——已替换为真实可解码的 1×1 PNG fixture。另发现 SequencedScreenshotCapturer 里同一份假数据因缩进不同只被替换了一半,导致一个测试短暂失败,已定位并修正两处。


25. Validation Update · E4 ​

未执行。原因:Screenshot 的 5min/30min 真机验收(§8 A/B/C)、FrameTime A/B 与修复后验收(§19.2 D/E/F)、GPU Counter 运行中添加与下一 Session 真实采集验收(§19.3 G/H)均要求连接真实 iOS 设备,本环境无可用设备。代码与判据均已就绪;建议按以下顺序执行:

1. Screenshot A(未勾选 5min)→ B(勾选 5min)→ C(勾选 30min)
2. FrameTime D(frame-only vs startup-and-frame 2~5min,写满 presentedFrame*/coreProfilePostStartup* 字段对照表)
3. 若 D 支持"Startup filters 是主因"假设,再进入 F3 原位收窄验证;若不支持,FTI-008 保持 blocked/needs-source-investigation,不得回退 stop/drain/restart
4. GPU Counter G/H(中途添加语义 + 下一 Session 真实采集,需先有 G2 证据才能指望 data row > 0)

26. 当前状态小结 ​

[x] Screenshot Bug A(未勾选也有实时序列)
[x] Screenshot Bug B(IPC 严格保序 + 四步完整性校验)
[x] Screenshot Optimization C(HEIC 1800/Q75 持久化 + 900/Q60 预览,历史 PNG 兼容)
[x] FrameTime F1(只读连续性遥测)
[x] Section 15(采集中添加指标 next-session-only 语义)
[x] Sidecar 全量测试 430/430、Swift 全量测试 167/167

[ ] FrameTime F2 真机 A/B(blocked:无设备)
[ ] FrameTime F3/F4(blocked:依赖 F2 证据)
[ ] GPU Counter G2 参数合同独立证据(blocked:仓库内无可用参考实现,需要真机 wire capture 或外部可信参考)
[ ] GPU Counter G3/G4(blocked:依赖 G2)
[ ] E4 全部真机验收(blocked:无设备)

FTI-008、FTI-009 在 internal/field-test-issues.md 中的状态相应更新为“部分已实现(诊断遥测/UI 语义),核心协议问题仍待真机”。

Perfowl · Performance Observer