外观
FTI-005 FrameTime Continuation 与 FTI-006 GPU Counter UI 修复方案
0. 执行摘要
本批只修两个独立问题:
- FTI-005 · Unified CoreProfile 进入
frame-continuation后 FrameTime 停止更新; - FTI-006 · GPU Counter 动态 Counter 全量套用普通卡片布局,导致 Header/曲线严重挤压、名称逐字符竖排。
两项必须分别验证,禁止把 GPU Counter UI 问题误判为采集失败,也禁止为了修 FrameTime 重新引入 Startup → Frame CoreProfile stop/drain/restart。
本批核心边界:
text
FTI-005:
保持 Build57/58 Unified CoreProfile
单 Tap / 单 kperf owner / start=1 / restart=0 / sourceEpoch=1
只修 continuation 阶段的 Presented Frame 解码与 FrameTime freshness
FTI-006:
保持 GPU Counter 原始采集、wire、Session 持久化、导出不变
只重构 GPU Counter 的 Workbench 展示和选择模型代码实现、测试、真机结果与任何方案偏差必须同步写回本 devlog。代码完成但开发记录未更新,视为未完成。
1. FTI-005 · FrameTime 在 Startup 完成后冻结
1.1 现场现象
2026-09-16 Build58 真机 Session 约 4 分 40 秒:
text
CoreProfile observed bytes = 3,753,607,010 bytes ≈ 3.50 GiB
CoreProfile observed chunks = 850
CoreProfile persisted bytes = 0(derivedOnly 正常)
droppedEventHighWater = 0
CoreProfile startCount = 1
CoreProfile restartCount = 0但 frame-events-live.jsonl 只有:
text
Presented Frame = 57
覆盖时间 ≈ 1.146 秒并且 57 帧全部来自:
text
coreProfileStreamRole = startup-lifecycleStartup semantic complete 后:
text
frame-continuation CoreProfile chunks = 815
Presented Frame = 0最终工作台持续重复最后一次:
text
FrameTime = 32.915208 ms形成“FrameTime 一直不变化”的假象。
同样问题在 Build57 Unified 真机会话已经存在,所以 不是 Build58 Raw Retention 引入的回归。
1.2 已确认根因 A:decoder 主动跳过 frame-continuation
文件:
text
MacApp/Sidecar/perfowl_collector_core/launch_trace.py当前 LaunchTraceStreamDecoder.consume():
python
def consume(...):
self._pending_frame_events = []
if payload.get("coreProfileStreamRole") == "frame-continuation":
return None这条逻辑与旧 Startup → Frame rotation 架构相容,但与 Build57 之后的 Unified Continuous Tap 冲突。
Build57 新架构是:
text
Startup + FrameTime
↓
同一 CoreProfile Tap
↓
同一 RAW-v2 stream
↓
Startup semantic complete
↓
CoreProfile 不停止
↓
后续 chunk 仅 role 标记为 frame-continuation因此 continuation 仍然是同一 RAW-v2 连续字节流的一部分,必须继续经过同一个 RAW parser 状态机才能得到 Presented Frame。
当前提前 return None 导致后续 chunk 根本没有进入:
text
PRESENTED_FRAME_DEBUG_ID
→ timestampNs
→ frameTimeMs
→ _pending_frame_events这是 FTI-005 的主根因。
1.3 已确认根因 B:FrameTime 被当作无限有效 gauge 重复发布
文件:
text
MacApp/Sidecar/perfowl_collector_core/realtime.py当前 RealtimeMetricProjector 保存:
python
self._latest_frame_time_ms
self._latest_frame_quality每个 sysmontap product sample 都直接:
python
metrics["frameTimeMs"] = latest_frame_time_ms只要目标仍在前台,最后一次 FrameTime 会无限重复。
所以即使上游 Presented Frame 已经中断,UI 仍会不断收到旧值:
text
32.915208
32.915208
32.915208
...这是第二个独立缺陷。
FrameTime 是事件派生值,不是可以无限保鲜的 gauge。
2. FTI-005 最终修复设计
2.1 保持 Unified CoreProfile,不允许回退 rotation
以下 Build57/58 架构是硬边界:
text
Startup + FrameTime
→ 一个 CoreProfile Tap
→ 联合 filters
→ startCount = 1
→ restartCount = 0
→ sourceEpoch = 1
→ Startup semantic complete 不关闭 CoreProfile严禁为了让 continuation FrameTime 恢复而重新:
text
stop
→ drain
→ restart frame-only tap否则 FTI-001 会复发。
2.2 continuation 必须继续使用同一 RAW-v2 parser state
不要为 continuation 新建第二个 RealtimeFrameDecoder 去从 chunk 中重新找 RAW v2 header。
原因:continuation chunk 是同一流的后续字节,可能没有新的 RAW-v2 header/thread map。
正确设计:
text
LaunchTraceStreamDecoder
│
├─ 一直维护 RAW-v2 stream parser state
│
├─ startup-lifecycle
│ ├─ thread/PID attribution
│ ├─ lifecycle semantics
│ └─ Presented Frame
│
└─ frame-continuation
├─ 保持 parser state
├─ 必要 thread/PID mapping 继续更新
├─ Presented Frame 继续提取
└─ Startup lifecycle report 不再重新打开/污染推荐实现方式:
python
role = payload.get("coreProfileStreamRole")
frame_continuation = role == "frame-continuation"删除当前 continuation 的顶层 early return。
但不能只是机械删除一行然后结束;还必须保证 startup semantics 在 complete 后保持冻结。
2.3 continuation 阶段只继续“流解析 + Frame 投影”,不重新修改 Startup 结果
当前 decoder 在 Startup 完成时已有:
python
self._emitted = True
self._emitted_report = published
self._records.clear()
self._reset_semantics()并且后续 lifecycle retention 已由 not self._emitted 门禁控制。
修复必须锁定以下行为:
text
Startup report 一旦 liveComplete
→ 已发布结果保持冻结
→ continuation 到来的后续 UIKit/scene 事件不得重新改变本次 App Launch 报告但:
text
Presented Frame
thread map / target attribution continuity
stream byte offset
Mach timebase
loss marker仍必须继续维护。
如果实现时发现当前 _emitted 已足以冻结 lifecycle semantics,则只增加针对 continuation 的回归测试,不额外造状态机;如果不能证明,则显式增加:
text
startupSemanticFrozen或等价状态。
禁止为了“安全”直接跳过整个 chunk。
2.4 take_frame_events() 在 continuation 下必须正常返回
修复后行为:
text
consume(frame-continuation chunk)
↓
解析 Presented Frame
↓
_pending_frame_events
↓
take_frame_events()
↓
projector.consume_decoded_core_profile_frames()
↓
frame-events-live.jsonl
↓
FrameTime / Jank / Smooth必须沿用现有单次 RAW scan,不允许同一个 chunk 被两个全量 decoder 重扫。
2.5 FrameTime freshness:禁止无限重复最后值
推荐不要简单增加“2 秒以后清空”这种时间 magic number。
更可靠的产品语义是:
一个 product sample 只有在自上一个 product sample 后收到过新的有效 Presented Frame 时,才发布新的 FrameTime。
建议 RealtimeMetricProjector 增加:
python
_latest_frame_timestamp_ns
_last_published_frame_timestamp_ns或等价 sequence/version。
每次 _consume_decoded_frames() 收到新的有效 FrameTime:
text
更新:
_latest_frame_time_ms
_latest_frame_quality
_latest_frame_timestamp_ns在构造下一个 sysmontap sample 时:
text
如果 target not foreground
→ frameTimeMs = None
→ quality = not-foreground
如果没有任何有效 FrameTime
→ frameTimeMs = None
→ quality = missing
如果 latest_frame_timestamp_ns
<= last_published_frame_timestamp_ns
→ frameTimeMs = None
→ quality = stale/no-new-frame
如果存在新的 FrameTime
→ 发布最新 frameTimeMs
→ quality = candidate
→ last_published_frame_timestamp_ns = latest_frame_timestamp_ns推荐质量值使用:
text
stale如果项目现有 quality 枚举/契约不接受 stale,必须先统一定义为:
text
missing-no-new-frame并同步知识库,不允许私自造一个 UI-only 字符串。
关键目标:
text
没有新帧 ≠ 上一帧继续有效2.6 不要用 Graphics FPS 推算 FrameTime 作为兜底
禁止:
text
FrameTime = 1000 / CoreAnimationFramesPerSecond用来补 CoreProfile Presented Frame 缺失。
原因:
text
Graphics FPS = 每秒 gauge
FrameTime = Presented Frame 逐帧间隔两者语义不同。
FrameTime 缺源时必须诚实 missing/stale。
3. FTI-005 E2 测试
至少新增以下测试。
A. continuation 仍能提取 Presented Frame
构造:
text
startup-lifecycle chunk
→ RAW v2 header
→ thread map
→ lifecycle
→ Presented Frame A/B
标记 Startup complete
frame-continuation chunk
→ Presented Frame C/D/E断言:
text
Startup阶段 frame events > 0
Continuation阶段 frame events > 0
C/D/E timestamp > B
FrameTime连续计算B. continuation 没有第二个 RAW-v2 header 也能解析
这项必须单独锁定。
第二段 continuation fixture:
text
只包含后续 KD_BUF records
没有 RAW_V2_MAGIC
没有第二份 thread table同一个 decoder 必须继续成功解析。
防止未来有人错误地换成新 decoder。
C. Startup report continuation 后保持不变
Startup complete 时保存:
text
reportA随后喂 continuation chunk,包含 Presented Frame,并可混入后续 lifecycle namespace record。
断言:
text
report after continuation == reportA
Total 不变化
phaseResults 不变化
liveRevision 不重新增长D. 不重复 Presented Frame
同一个 Raw chunk 只能产生一次 frame event。
断言:
text
frame timestamp sequence unique
frame count 与 fixture 真实 Presented Frame 数一致E. freshness / stale
流程:
text
新Frame A/B
→ 下一 product sample 发布 FrameTime
没有新Frame
→ 再来 sysmontap sample
→ frameTimeMs = nil
→ quality = stale/no-new-frame
再来新Frame C
→ 下一个 sample 恢复新 FrameTime禁止第二个 sample 重复上一值。
F. foreground 切换
保持:
text
foreground → background
FrameTime = nil / not-foreground重新 foreground 后必须等待新 Presented Frame,不能立即复活旧 FrameTime。
G. Unified 主路径
集成测试至少模拟 30 秒以上 continuation:
text
mode=startup-and-frame
startCount=1
restartCount=0
sourceEpoch=1
Startup complete
后续每秒持续输入 Presented Frame断言:
text
frame-events span > 20s
而不是只覆盖启动约1s4. FTI-005 E4 真机验收
至少执行:
text
A. FrameTime-only 5 min
B. Startup + FrameTime 5 min
C. Startup + FrameTime 15~30 min(推荐)每轮记录:
text
CoreProfile startCount / restartCount
sourceEpoch
observed chunk count / bytes
droppedEventHighWater
TRACE_LOST_EVENTS
Presented Frame count
firstPresentedFrame timestamp
lastPresentedFrame timestamp
Presented Frame coverage duration
FrameTime non-null sample count
FrameTime stale/missing sample count
FPS
Startup Total / phase coverage硬门禁:
text
Startup + FrameTime:
startCount = 1
restartCount = 0
正常 sourceEpoch = 1
Presented Frame coverage 应接近整个有绘制活动的 Session,而非仅约1秒
FrameTime 不得长期复制同一个旧值
Startup Total/阶段不得回归对于静止页面长期没有新 Presented Frame:
text
FrameTime 显示 missing/stale是正确结果,不得为追求“曲线一直有线”而重复旧值。
5. FTI-006 · GPU Counter 卡片显示异常
5.1 现场现象
GPU Counter 卡片真机截图出现:
text
GPU Counter 标题逐字符换行
Counter 名称逐字符竖排
大量彩色 legend/readout 横向挤压
图表主体几乎无法阅读这不是 GPU Usage 的 Renderer/Tiler/Device 三项,也不是 GPU Counter 上游采集断流。
问题是 GPU Counter 动态 schema 直接套用了普通少量 series 卡片布局。
5.2 已确认根因
文件:
text
MacApp/Sources/PerfowlCore/WorkbenchModels.swift当前:
swift
let uniqueKeys = Array(Set(keys)).sorted().prefix(24)也就是最多动态创建 24 条 series。
文件:
text
MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift普通卡片 Header:
swift
HStack {
Text(definition.displayName)
Spacer()
ForEach(definition.series) { series in
MetricSeriesReadout(...)
}
}24 个长 Counter name 被强行塞进一个 HStack。
SwiftUI 为满足布局压缩文本,最终形成逐字符竖排。
同时 24 条 source-native Counter 共用一个 Y 轴,本身也没有良好的可读性:不同 Counter 数值规模可能差几个数量级。
6. FTI-006 最终产品设计
6.1 GPU Counter 必须使用专用 Panel
仿照当前:
text
MemoryUsagePanel新增:
text
GPUCounterPanel在通用 MetricPanel 中 special-case:
swift
if definition.id == .gpuCounter {
GPUCounterPanel(...)
} else if definition.id == .memoryUsage {
...
} else {
generic panel
}禁止继续让动态 GPU Counter 走:
text
ForEach(definition.series)
→ 全部塞进 Header6.2 Header 固定,不显示全部 Counter readout
Header 推荐:
text
GPU Counter 24 counters · 整机只展示:
text
卡片名
可用 counter 数
scope = device不在 Header 放 24 个动态 readout。
6.3 第一版采用“单 Counter 选择”,不要同时画 24 条
本批推荐采用单选而不是多选。
原因:
text
GPU Counter = device-specific source-native count
不同 key 的数量级和物理语义可能完全不同
当前只有一个 Y 轴如果同时画 3~24 条,即使布局修好,也可能因为最大值差异把其它曲线压成直线。
第一版正确产品模型:
text
GPU Counter
↓
设备返回 N 个动态 key
↓
全部采集 / 全部保存
↓
UI 用户从 selector 选择 1 个 key
↓
图表只画该 key未来如果明确有同量纲可比较 counter,再单独设计多选/多轴/归一化模式。
本轮不做。
6.4 初始状态不要随意猜“最重要 Counter”
GPU Counter schema 是 device-specific。
不要基于名字猜测:
text
GRC_*
TA_*
GPU_*哪个一定最重要。
推荐首次打开:
text
selectedCounterKey = nil卡片主体显示:
text
已发现 24 个 GPU Counter
选择一个 Counter 查看实时曲线
[选择 Counter]如果用户已经在本次 App 生命周期选择过一个仍存在的 key,可以保持选择。
可以后续使用 @AppStorage 保存最后选择,但必须以当前 schema 是否包含该 key 为门禁;本轮不是必须项。
6.5 Selector
推荐使用:
text
Menu / Popover
+ 搜索框
+ 单选列表Counter 数量可能大于 24,UI 不应再把 24 当成产品上限。
gpuCounterPanel(keys:) 可以继续为 chart definition 生成需要的 definition,但推荐重构为:
text
全部可用 key 独立作为 availableCounterKeys
当前 selected key 单独生成 1 条 chart series不要为了 UI 布局而丢掉原始 schema。
Selector 列表:
text
Counter name 单行
lineLimit(1)
truncationMode(.tail)
完整名字通过 help/tooltip 查看6.6 Chart
只画当前 selected counter:
text
series count = 1Y 轴:
text
使用该 Counter 当前 viewport 的 observed max
niceMaximum()单位诚实保持:
text
source-native-count或 UI 简写:
text
count但 tooltip/help 必须说明:
text
source-native · device scope禁止未经知识库契约证明把任意 Counter 标成:
text
%
ms
MHz
bytes/s6.7 Current value readout
选中一个 Counter 后,Header 或 selector 下方可以显示:
text
<CounterName>
当前值 123456
整机 · source-nativeCounter 名称限制单行并 tail truncate。
完整名称用 tooltip。
6.8 底层采集、Session、Export 不变
本批只改 Workbench presentation。
禁止为了 UI 简化而改成只采一个 Counter。
仍然:
text
设备返回的全部 counterValues
→ PerformanceSample.gpuCounters
→ Session metrics.csv / JSON
→ ExportUI selector 只决定:
text
当前图表画哪一个不决定:
text
设备采哪个
Session存哪个这样用户后续回看/导出仍可得到完整数据。
6.9 不要和 GPU Usage 混淆
产品继续保留两个概念:
text
GPU Usage
= Graphics service
= Renderer / Tiler / Device utilization %
= foreground-device
GPU Counter
= 动态硬件 counter schema
= source-native count
= deviceGPU Counter 专用卡片标题、scope 与帮助文案必须明确是:
text
整机 · source-native不要让用户误以为它是目标 App 的 GPU 百分比。
7. FTI-006 E2/UI 测试
至少覆盖:
A. 24 counter schema 不再撑爆 Header
构造 24 个长名称:
text
GRC_FAKE_COUNTER_WITH_A_VERY_LONG_NAME_00...断言:
text
GPU Counter title 仍为单行
Header 不创建 24 个 MetricSeriesReadout如果项目有 snapshot/pixel test,则增加固定宽度截图测试。
B. 无选择状态
text
24 counters available
selected = nil应显示:
text
选择一个 Counter 查看实时曲线图表不绘制错误的 24 条线。
C. 单选
选择 key A:
text
chart series = 1
value = sample.gpuCounters[A]再选择 key B:
text
chart series切换到B
A不再参与Y轴缩放D. schema 更新
当前选择 key A,下一批 schema 不再包含 A:
text
selectedCounterKey → nil显示“请选择 Counter”,禁止继续展示过期 A。
E. 长名字
Counter name 100+ chars:
text
UI单行
尾部省略
help/tooltip完整不得再逐字符竖排。
F. 底层数据保持全量
即使 UI 只选择 key A:
text
PerformanceSample.gpuCounters仍应包含 A/B/C/... 全部源返回 key。
Session/Export 不得只剩 A。
8. FTI-006 真机验收
选择 GPU Counter 后至少 5 分钟。
验收:
text
GPU Counter 卡片布局稳定
标题不换成逐字符竖排
Selector能查看设备返回的全部 key
一次只显示一条曲线
切换不同 key 曲线和值同步变化
Session/Export仍包含完整 counter dictionary
Mac UI CPU/RSS无明显异常需要保留一张修复前截图和一张修复后截图作为 devlog 证据。
9. 推荐实施顺序
Phase 0 · 登记问题
在:
text
internal/field-test-issues.md新增:
text
FTI-005 · Unified CoreProfile continuation 后 FrameTime 冻结
FTI-006 · GPU Counter 动态卡片布局失控初始状态:
text
已定位实施开始后改:
text
修复中Phase 1 · FTI-005 decoder continuation
先只做:
text
移除 frame-continuation 整块 early return
保持同一 RAW parser state
continuation 继续解析 Presented Frame
Startup semantic freeze 不回归先跑专项 Python tests。
Phase 2 · FTI-005 freshness
再做:
text
FrameTime 新鲜度/一次性发布
无新 Presented Frame → missing/stale不要和 Phase 1 混成一个大改,方便失败时定位。
Phase 3 · FTI-006 专用 GPUCounterPanel
实现:
text
固定Header
available key selector
单Counter chart
长名字截断
schema失效自动清选择不动采集端。
Phase 4 · 全量 E2
必须运行:
text
Sidecar 全量 pytest
Swift 全量 tests
Unified CoreProfile专项
Frame pipeline专项
GPU Counter专项
Session/Export专项不能只跑新增测试。
Phase 5 · E4
先验证 FTI-005:
text
Startup + FrameTime 5min确认连续后,再验证 GPU Counter UI。
如果时间允许补:
text
Startup + FrameTime 15~30min10. 禁止事项
以下实现直接判定不合格:
- 为修 FrameTime 重新恢复 Startup → Frame CoreProfile restart;
- continuation 新建一个失去 RAW-v2 header state 的独立 decoder;
- 同一个 CoreProfile chunk 被两个完整 RAW scanner 重复解析;
- FrameTime 没有新帧时继续重复上一值;
- 用
1000/FPS冒充 FrameTime; - continuation 后重新修改已经冻结的 Startup Total/phase;
- GPU Counter 只加
.lineLimit(1)就宣称完成; - 仍然把 24 个 Counter readout 塞进一个 Header HStack;
- 24 条不同量级 Counter 继续共用一张默认曲线图;
- 为 UI 单选而减少设备实际采集或 Session 保存的 Counter 数量;
- 根据 Counter 名称猜百分比/ms/MHz 等单位;
- 把 GPU Counter 和 GPU Usage 合并成一个指标;
- 覆盖/回退当前正在开发的 Screenshot Worker 或其它未提交工作;
- 未更新 devlog 就标记修复完成。
11. 开发记录硬门禁
实施 AI 必须持续更新本文件:
text
site/devlog/p3-p4-frametime-continuation-and-gpu-counter-ui-repair-plan.md至少追加:
text
## Implementation Update
- 实际修改文件
- 实际实现
- 与本方案偏差
- 偏差原因
- 当前状态
## Validation Update · E2
- Python测试数
- Swift测试数
- 专项测试
- 失败/修复记录
## Validation Update · E4
- Session时长
- Frame count / coverage
- CoreProfile start/restart/sourceEpoch
- drop/loss
- FrameTime freshness
- GPU Counter UI截图结果
- Session/Export完整性状态变化必须同步:
text
site/devlog/index.md
internal/field-test-issues.md如果实施 partial / blocked / paused,也必须即时写入。
12. Definition of Done
FTI-005
text
[ ] frame-continuation 不再被 LaunchTraceStreamDecoder 顶层跳过
[ ] continuation Presented Frame 能继续产生
[ ] continuation 无第二 RAW-v2 header 仍可解析
[ ] Startup report complete 后保持冻结
[ ] Presented Frame 无重复
[ ] FrameTime 不再无限复制最后值
[ ] 无新帧时输出 missing/stale
[ ] 新帧恢复后 FrameTime 自动恢复
[ ] foreground/background 语义正确
[ ] startup-and-frame startCount=1
[ ] restartCount=0
[ ] 正常 sourceEpoch=1
[ ] Startup Total/phase 不回归
[ ] 5min 真机 Presented Frame coverage 不再只有约1秒
[ ] 推荐完成 15~30min 长测FTI-006
text
[ ] GPU Counter 使用专用 Panel
[ ] Header 不再渲染全部动态 Counter readout
[ ] 24+长名称不会挤压标题
[ ] Counter selector 可查看全部动态 key
[ ] 第一版单选一个 Counter
[ ] 图表只绘制当前选择 key
[ ] Y轴只受当前选择 key 影响
[ ] schema失效时旧选择自动清除
[ ] 长名字单行省略并可查看完整 tooltip
[ ] scope 明确显示 device/整机
[ ] unit 保持 source-native
[ ] GPU Counter 与 GPU Usage 语义继续分离
[ ] 底层采集仍保存完整 counter dictionary
[ ] Session/Export 不因 UI 单选丢字段
[ ] 真机 UI 修复前后截图归档共同门禁
text
[ ] Sidecar全量测试通过
[ ] Swift全量测试通过
[ ] 不覆盖其它未提交工作
[ ] internal/field-test-issues.md 已更新
[ ] 本 devlog 已写 Implementation/E2/E4 记录
[ ] site/devlog/index.md 已同步
[ ] 记录未完成前不得标记已解决13. 最终期望
FTI-005 修复后:
text
Startup + FrameTime
↓
启动阶段立即有 FrameTime
↓
Startup semantic complete
↓
同一 CoreProfile Tap 继续
↓
FrameTime 持续整个 Session
↓
没有新绘制时诚实 missing/staleFTI-006 修复后:
text
GPU Counter
↓
完整采集设备动态 schema
↓
完整保存所有 counter
↓
Workbench只选择一个 Counter查看
↓
布局稳定、量纲诚实、曲线可读本批的目标不是增加更多数据,而是:
让已经采到的数据在正确的生命周期持续使用,并让动态 GPU Counter 以可读、诚实、不会破坏布局的方式展示。
Implementation and E2 Verification Update (2026-09-16 · Build 60)
1. 实际修改范围与代码落地
- 关联问题与方案编号:FTI-006(Unified CoreProfile 进入 continuation 阶段后 FrameTime 停止更新且静止时无限重复旧值)、FTI-007(GPU Counter 动态 key 导致通用卡片 Header 逐字符竖排与量级挤压)。方案内部初稿代称 FTI-005/FTI-006 已正式对齐为台账编号 FTI-006/FTI-007。
- 架构原则兑现:
- 严格保持 Unified CoreProfile 单 Tap(start=1, restart=0, sourceEpoch=1),未回退为 stop/drain/restart rotation;
- 启动报告在
liveComplete后保持完全冻结且不可变,continuation 事件绝不修改或重新触发启动报告; - FrameTime 增加严格新鲜度门禁,无新帧时置空(
quality = "missing-no-new-frame"),坚决杜绝静止重复旧值伪造匀速卡顿,坚决不使用1000 / FPS冒充 FrameTime; - GPU Counter 前端移除截断,完整保留全部动态 key;提供专属
GPUCounterPanel,固定 Header、Popover 实时搜索单选、单曲线自适应 Y 轴,底层全量采集、存储与导出完整无损。
- 核心文件变更:
MacApp/Sidecar/perfowl_collector_core/launch_trace.py:解锁coreProfileStreamRole == "frame-continuation"的早退拦截,使同一 decoder 保持 RAW-v2 状态机继续解析后继 presented frame 记录;对 lifecycle/debug_id/report trigger 增加not is_frame_continuation保护,确保 startup 报告在liveComplete后保持完全冻结,take_frame_events()持续供给呈现帧。MacApp/Sidecar/perfowl_collector_core/realtime.py:增加 FrameTime 新鲜度门禁_latest_frame_timestamp_ns与_last_published_frame_timestamp_ns。仅在收到严格晚于上次发布时间戳的有效帧时发布frameTimeMs;无新帧到达时发布None,quality="missing-no-new-frame";前后台切换或 generation 变更时重置时间戳。MacApp/Sidecar/tests/test_frametime_continuation.py(新增):6 项专项单测,覆盖跨阶段流解析、无 raw header 连续流解析、启动报告冻结性、帧去重、新鲜度门禁与前后台切换。MacApp/Sidecar/tests/test_startup_bootstrap.py:补充断言锁定 continuation 解码流记录但不作为启动端点。MacApp/Sources/PerfowlCore/WorkbenchModels.swift:移除 GPU Counter 动态 schema 的.prefix(24)截断,保留底层返回的全部动态 key;底层全量采集、原始存储与导出完整保留。MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift:实现专属GPUCounterPanel,固定 Header(展示GPU Counter标题与N counters · 整机);提供带搜索框、清除选择、单行尾部截断(带完整名称 Tooltip)、单位标签与当前值预览的 Popover 列表;未选择时提供引导态,选中时仅绘制该 counter 的独立趋势线,Y 轴自适应缩放;schema 变更时自动清除失效选中。MacApp/Tests/PerfowlCoreTests/WorkbenchModelsTests.swift:新增单测testGPUCounterPanelRetainsAllKeysWithoutTruncation,覆盖 30+ 动态 key 的全量保留与属性契约。
2. 验证结果与数字证据 (E2)
- Python 测试:
417/417 PASSED(10.10s,0 失败); - Swift 测试:
154/154 PASSED(15.93s,3 项显式基准跳过,0 失败); - VitePress 站点验证:
npm run build通过,63 项 Registry 契约全过,0 报错 0 死链; - 安装包生成与签名 (Build 60):
- App 产物:
MacApp/dist/Perfowl.app(Developer ID Application: Jiacheng Li (76J692XB5C) 签名,Hardened Runtime 开启,codesign --verify --deep --strict通过); - 主程序 SHA-256:
16cd10b9a0b81bfa400c461f030a5e087e26188d165790179f95c93e2f1a02ea; - ZIP 归档:
MacApp/dist/Perfowl-internal-test-Build60-x86_64.zip(109,353,018 bytes,SHA-256:ebf9fcef82b1c7c42aca1f41e9fa0c3d319903420279f6461e73d8193c6b0e70,独立解包验签通过); - DMG 镜像:
MacApp/dist/Perfowl-internal-test-Build60-x86_64.dmg(140,816,085 bytes,SHA-256:f3f25a62f36c71f15da19c378d6c656dc4be406128000d4c1b5b147b36a372fe,hdiutil verify及 Developer ID 容器验签通过)。
- App 产物:
- 静态审计与报告:
- 方案 HTML:
docs/architecture/perfowl-frametime-continuation-and-gpu-counter-ui-repair-plan-v1-2026-09-16.html - 匿名证据:
docs/validation/evidence/perfowl-build60-frametime-continuation-and-gpu-counter-ui-e2-anonymous-2026-09-16.json - 验收 HTML:
docs/validation/perfowl-build60-frametime-continuation-and-gpu-counter-ui-2026-09-16.html
- 方案 HTML:
3. 遗留项与未关门门禁
- 本机缺少 Apple Notary 凭据,公证与 ticket stapling 门禁仍开放;
- GPL-3.0 法务评估门禁仍开放;
- 交付状态标记为
partial,等待真机 E4(iPhone 15)在真实 App 上进行长会话连续帧时序与 GPU Counter 选择交互验证。
Validation Update · E4 2026-09-16 · Build60 真机复测
本轮 E4 证明 Build60 的两个实现修复本身生效,但又暴露了新的、范围更窄的后续问题,因此本 devlog 继续保持 implemented · partial:
- FrameTime:continuation early-return 已解除,新鲜度门禁也正确,无新帧时输出
missing-no-new-frame,不再无限复制旧值;但长窗仍存在真实逐帧连续性缺口。较长 Sessionaa26b20d-5d55-49ab-aafd-59655ee56ae5中 CoreProfile 848 chunks / 约 3.37 GiB / 131.63 s 持续输入,Presented Frame 1744 个、时间戳覆盖约 80.36 s,最大 gap 约 3.53 s;126 个产品 sample 中 34 个 FrameTime missing,其中 19 个时刻 Graphics FPS > 0、14 个 FPS >= 20。该后续问题新记为 FTI-008 · FrameTime 长时连续性。 - GPU Counter UI:专用 Panel 布局问题已解决;第一轮 Session 是 Recording 中途才添加 GPU Counter,Start 时未进入 CapturePlan,因此
gpu.counter.raw=0属正常语义。第二轮 Start 时已包含ios.device.gpu.counter.source_native,真实收到 1248 个 GPU raw event:device-info 1、schema-ready 1、其余 1246 全为当前 decoder 不支持的两整数 message shape,decoded data row=0。该数值链问题新记为 FTI-009 · GPU Counter data-row protocol。 - 后续统一实施方案已并入
site/devlog/p4-build59-screenshot-e4-followup-repair-plan.md,该文件现在同时覆盖 Screenshot E4 后续、FTI-008、FTI-009 与“采集中添加未武装指标下次 Session 生效”的交互语义。