外观
Build61 综合 E4 后续修复方案
唯一交接文档。 本文件在原 Build59 Screenshot E4 后续方案基础上继续扩展,合并 2026-09-16 Build60 真机复测暴露的 FrameTime 长时连续性与 GPU Counter 数值链问题。交给其他 AI 时只需要读取并执行本文件;不要再把 Screenshot、FrameTime、GPU Counter 拆成互相独立且可能冲突的方案。
0. 本轮范围与最终目标
本轮同时处理五个目标,但必须保持三个子链相互隔离、分别验证:
- Screenshot Bug A:未勾选 Screenshot 时,顶部只显示一张图片原地替换,没有实时截图序列;
- Screenshot Bug B:勾选 Screenshot 后,落盘截图显示黑屏;真机证据表明实际是 Worker -> Swift PNG 二进制传输损坏,不是设备真的返回黑屏;
- Screenshot Optimization C:不保存原始全分辨率 PNG,保持清晰前提下统一压缩为 HEIC;
- FTI-008 · FrameTime 长时连续性:Build60 已修复 continuation 顶层 early-return 和旧值无限重复,但真机长窗仍存在秒级 Presented Frame 空洞,必须定位并修复真实逐帧源连续性;
- 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 activationScreenshot 子链不得改动 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 = 25screenshotPersistedCount = 0screenshotPersistenceEnabled = 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
-> 永远显示 screenshotTimelinePreviewScreenshot 是否勾选只改变文案和持久化状态:
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 后必须依次验证:
pngData.count == byteLength- SHA-256 ==
contentSHA256 - ImageIO 能完整创建 image source / image
- 图像宽高 > 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 deadline4.5 HEIC 不可用时的降级
首次编码时检测 HEIC destination 是否可用。
优先级:
text
HEIC Q75
-> 若本机系统编码器明确不可用:JPEG Q80禁止 fallback 到原始 PNG 持久化,因为会重新引入 GB 级空间占用。
Manifest 必须记录最终实际格式和参数。
5. Session / Review / Export 兼容
新 Session
建议文件:
text
screenshots/shot-000001.heicSessionScreenshot.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 磁盘 = 0B. 勾选 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. 禁止事项
以下方案均禁止:
- 回退到每秒启动一个 pymobiledevice3;
- 用视频流替代 1 FPS Screenshot;
- 未勾选时写临时全量截图到 Session/磁盘;
- 为显示序列长期持有
[NSImage]/[CGImage]; - 保存原始 PNG + HEIC 两份;
- HEIC 失败时 fallback 到原始 PNG;
- 只修黑屏显示,不修字节流完整性;
- 只检查 PNG magic/header,不做 SHA + 完整 decode;
- 每个 pipe chunk 新建独立 Task;
- Screenshot 失败影响 Performance Collector / CoreProfile;
- 因 Screenshot 选择变化重新 Prepare App;
- 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 必须先通过至少一种独立证据确认真实调用合同:
- 项目中冻结的 py-ios-device / Instruments 参考实现;或
- 可复现的真实 DTX wire capture;或
- 与当前 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 message14.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/missing14.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
-> 卡片显示:本次未采集 · 下次测试生效
-> 选择状态保留到下一 SessionFrameTime 等需要专门 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.pyFrameTime
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 backlog19.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-onlyH · 下一 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 原禁止项外,再增加:
- 禁止为了 FrameTime 连续性使用
1000/FPS合成 FrameTime; - 禁止未经 A/B 证据直接认定 Startup filters 是唯一根因;
- 禁止未经真机验证在运行中调用
setConfig:并假定不会重启 kperf; - 禁止恢复 Startup -> stop/drain -> FrameTime restart rotation;
- 禁止把 configGeneration 冒充 sourceEpoch;
- 禁止看到 GPU
[int,int]就自行定义为 timestamp/cycles; - 禁止仅凭参数名称猜测并交换 configure 参数,必须有独立参考/wire 证据;
- 禁止为了 GPU Counter 数值去执行设备侧 JavaScript 或引入不可审计公式;
- 禁止采集中动态修改已经冻结的 CapturePlan 来迎合新加卡片;
- 禁止用空图表隐瞒
not-collected / next-session-only / schema-ready-no-data / source-failed状态; - 禁止 Screenshot/FrameTime/GPU 任一子任务失败拖垮 CPU/Memory/Network 等主 Session;
- 禁止 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.1runtime)与MacApp/dist,没有找到任何独立的 py-ios-device/Instruments 参考实现或真实 DTX wire capture可以证实或证伪当前参数顺序。没有证据,按方案红线不能改,也不能是“看起来更合理就换”。 - G3(decoder message taxonomy):依赖 G2 的参数合同才能正确解释后续两整数消息的语义,G2 未解,G3 未动。
- G4(匿名真机 fixture):需要真实设备的 GPU Counter data row,本环境无设备,未做。
- G2(configure 参数合同):
- 进一步动 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 语义),核心协议问题仍待真机”。