planP4E1approved2026-09-20

Perfowl 工作台 UI 信息架构精简与设备交互修复方案

> 执行定位:本方案可直接交给后续 AI 实施。用户已经逐项确认本轮 UI 需求。本批目标是减少工作台中的内部实现噪音、统一指标卡片布局、重构截图与启动时间表达、修正设备提示位置,并专项排查 Wi‑Fi 设备列表偶现/消失。 > > 边界:本批优先修改 SwiftUI 展示层和必要的展示模型/设备发现状态管理。除“条件指标可用性”和“Wi‑Fi 设备稳定性”确有需要外,不改变 Collector 原始采集、指标公式、Session 数据格式、报告统计算法和既有原始字段。P95/Jitter/Refresh Rate 等数据可以继续采集、保存和进入报告,但不再作为实时工作台曲线展示。 > > 禁止:不得为了 UI 简化删除底层诊断数据;不得把缺失值填 0;不得用 UI 猜测替代 Collector 的真实状态;不得破坏已有 Session 回放与报告兼容。

1. 现状与本轮目标

当前工作台存在三类问题:

  1. 展示层泄露内部实现:如 ActivityTrace FrameLifetime、candidate、profile、valid、阶段覆盖数、丢事件标记、source/scope/quality 等,大量属于研发/诊断信息,不应长期占据用户主界面。
  2. 信息重复和卡片过重:截图每张一张深色卡片;启动总时间重复展示;Display Performance 同时展示 5 个实时项;Memory 等多指标使用大块 readout,和 CPU Usage 的紧凑布局不一致。
  3. 设备交互不稳定/位置不合理:连接/断开提示嵌在截图区域;Wi‑Fi 设备记录偶尔出现、偶尔消失。

本轮最终 UI 原则:

2. 已确认产品需求

2.1 截图区域:从“大卡片列表”改成紧凑时间轴截图带

当前代码位置:

当前问题:

2.1.1 新布局

改为“水平截图带 + 共用时间刻度轴”:

建议结构:

ScreenShot

0s        1s        2s        3s        4s
|---------|---------|---------|---------|
[图]      [图]                [图]      [图]

真实间隔缺口应能从时间轴看出来,但实现不能通过大量空白 View 堆积造成性能问题。推荐以统一坐标映射/Canvas/overlay 或有界 spacer 计算实现。

2.1.2 删除的常驻信息

从单张截图 UI 删除:

底层 quality、sequence、bytes 继续保留在数据对象/Session/日志中,不得删除数据字段。

2.1.3 异常截图

如果缩略图无法解码或截图失败:

2.2 移除 Scene 01

当前:

要求:

2.3 移除默认“常规测试”模式卡片/Tab

当前:

要求:

直接目标:

3. 启动时间区域重构

涉及:

3.1 删除 Running / Foreground Ready 顶部指标

当前 StartupTiming panel 额外声明:

要求:

3.2 启动总时间只出现一次

当前同一结果会出现在:

新规则:

3.3 启动阶段只展示“阶段名称 + 耗时 + 时间轴”

阶段主 UI 只需要:

删除阶段行中的:

需要保留多 occurrence 数据时,可以在内部数据/诊断详情中保留,不在常规视图重复展开。

3.4 正常不存在的阶段:完全不显示

规则必须严格区分:

3.5 关键起点/终点缺失:红叉表达,不写长说明

用户确认的异常表达:

实现要求:

3.6 丢事件标记仍保留在诊断层

deviceLostEventMarkerCount、deviceLossDisplay、profile reason、totalRepairDisplay 等:

4. 全部性能卡片统一为 CPU Usage 风格

用户明确:当前 CPU Usage 的布局就是目标布局。

目标样式:

4.1 通用交互

多指标卡片:

单指标卡片:

4.2 建议抽取共享组件

不要让 Memory / Display / GPU / CPU Core 每个各写一套大卡片 UI。

建议抽取:

或者同等职责的共享组件。

特殊指标只负责:

这样后续新指标不会再次产生不同布局。

4.3 清理现有 Memory 大 readout

当前 MemoryUsagePanel 的 5 个指标使用两排大块 Button。

要求:

5. Display Performance 专项调整

涉及:

5.1 删除内部实现描述

删除标题旁:

不要在常规 UI 显示 Collector source。

5.2 实时卡片只保留 FPS + Frame Time

当前 live panel 有:

新规则:

5.3 P95 / Jitter 移到报告

P95 与 Jitter 是基于 Frame Time 序列派生的统计值:

本批必须检查:UI 删除 live series 后,报告仍能读取数据,不能误删 Registry/Collector/JSON/CSV 合同。

5.4 Refresh Rate 不画曲线

Refresh Rate 作为刷新环境/设备能力值:

5.5 删除“FPS / 帧耗时 / 全部”模式切换器

当前 DisplayChartMode 是为 5 series 设计的。

新布局只剩两个 series,应删除模式切换器:

6. “添加指标”列表精简

涉及:

6.1 每项只显示“名称 + 含义”

删除:

每个条目只保留:

  1. 指标名称;
  2. 一句用户能理解的指标含义。

建议统一中文含义,示例:

如 .fps / .frameTime 已经只是向 .displayPerformance 迁移的兼容 ID,实施 AI 必须检查它们是否仍需要单独出现在 picker。若没有独立产品语义,则 picker 只保留 Display Performance,但 enum/历史偏好迁移兼容不可删除。

6.2 条件指标:条件不满足直接置灰

当前 WorkbenchMetricSupportState.conditional.selectionEnabled 实际仍返回 true,不符合新要求。

新规则:

实现要求:

等转换成 enabled/disabled。

7. 设备连接/断开提示改成顶部 Banner

当前:

新规则:

建议:

.overlay(alignment: .top) {
    if let notice = activeUSBStatusNotice {
        USBStatusBanner(notice: notice)
            .transition(.move(edge: .top).combined(with: .opacity))
            .zIndex(...)
    }
}

注意 macOS 标题栏区域和自定义 titleBar 的视觉层级,Banner 必须看起来是“从软件顶部下拉”,而不是出现在内容中部。

8. Wi‑Fi 设备列表稳定性专项修复

涉及:

8.1 当前观察到的风险点

当前 refreshDevices() 同时整合 USB discovery 和 listNetworkDeviceIdentifiers(),并在 applyDiscovery 最终用当轮结果替换 discoveredDevicesByID/devices。

代码已经有两项保护:

但仍需验证以下情况是否造成“Wi‑Fi 有时有、有时没有”:

  1. listNetworkDeviceIdentifiers() 单轮瞬时返回空/超时,最终 merge 是否仍会删掉上一轮 Wi‑Fi;
  2. USB attach/detach 触发 refreshDevices() 时,Network discovery 尚未稳定,是否先发布后又覆盖;
  3. networkHydrationTask?.cancel() 是否会因为单设备 retry/新一轮 refresh 取消其他 Wi‑Fi 设备 hydration;
  4. 同一物理设备 USB/Network route 切换时,是否因 identifier/route 合并时机导致闪烁;
  5. generation 检查是否会把较早但有效的 Network 结果丢弃;
  6. 手动刷新和 monitor 事件并发时是否出现旧结果覆盖新结果。

8.2 修复原则

建议采用轻量“missing streak / grace”机制:

8.3 不增加高频轮询

本修复不能为了“稳定”增加高频 Wi‑Fi scan:

9. 当前主要代码修改清单

预计涉及:

必改

可能需要联动

仅当报告显示或 availability 需要时修改;不得为 UI 清理无理由改变采集协议。

10. 实施顺序

Phase 0:基线与测试保护

先补/更新测试,确认以下现状合同:

Phase 1:静态 UI 清理

先做无数据合同风险的项目:

Phase 2:统一指标卡片布局

Phase 3:Display Performance 精简

Phase 4:启动时间表达重构

Phase 5:截图时间轴

Phase 6:Wi‑Fi 稳定性

Phase 7:整体验收与真机验证

11. 自动化测试要求

至少新增/更新以下测试:

UI/模型合同

  1. WorkbenchDisplayContractTests
  1. WorkbenchModelsTests
  1. StartupTracePresentationTests
  1. Screenshot 测试

Wi‑Fi 设备稳定性

增加 DeviceFoundation 场景:

  1. 第一次发现 Wi‑Fi A;
  2. 下一次 network list 瞬时空;
  3. UI 仍保留 Wi‑Fi A;
  4. 后续 network list 恢复 A,不产生删除+新增闪烁;
  5. 连续达到移除阈值后,A 才从列表消失;
  6. USB A 拔线后如果 Network A 可用,selected device 不应短暂清空;
  7. hydration 失败只标 offline,不移除 row;
  8. 两台 Network 设备同时 hydration,retry 一台不能取消另一台的有效结果。

12. 真机验收清单(E4)

必须至少执行:

A. 截图

再做 30 分钟长测,确认:

B. 启动

至少覆盖:

  1. 阶段较完整的一轮;
  2. App 天生不触发某可选阶段;
  3. 起点缺失样本;
  4. 终点缺失样本。

验收:

C. 卡片

D. 添加指标

E. 设备提示

F. Wi‑Fi

至少连续执行 20 次:

要求:

13. 性能门禁

本批属于 UI 重构,但仍需遵守轻量化要求。

截图时间轴

卡片

Wi‑Fi

实施后记录变更前后:

若出现明显退化,本批不得标记 validated。

14. 停止条件与风险

出现以下情况应停止扩大修改范围,优先修正:

  1. 为隐藏 UI 文本而误删 Collector/Session 原始字段;
  2. P95/Jitter 从报告数据中消失;
  3. startup endpoint 红叉只能靠阶段数量猜测;
  4. 指标 toggle 导致采集 source 重启;
  5. Wi‑Fi 修复引入永久幽灵设备;
  6. screenshot 时间轴导致长测 UI 明显卡顿;
  7. 当前脏工作区中的其他 R8/Build73 改动被混入本批。

当前工作区已有大量未提交修改和 untracked 文件,本批执行时必须严格限定 diff,不得覆盖或清理其他并行批次内容。

15. 完成定义

只有同时满足以下条件才可称为本轮 UI 改造完成:

16. Implementation Update(2026-09-20)

范围:§2~§8 全部落地(Phase 1~6)。Phase 7 的真机 E4 未执行——实现 AI 只交付 E2,真机验收(§12 A–F 清单、§13 性能门禁的真机测量)需由用户执行。

基线与隔离:基线 c9a133d(Build 74 已推送 commit)。本批在独立 worktree (分支 workbench-ui,/private/tmp/perfowl-ui)中执行,未触碰共享脏工作区 的任何文件;已核对 c9a133d 与共享工作区 HEAD 之间 UI 层代码完全一致(仅 PerformanceModels.swift 6 行与并行批次相关)。

改动清单(tracked 9 文件 +1217/−620;新增 3 文件 474 行)

文件变更
MacApp/Sources/PerfowlApp/PerfowlWorkbenchView.swift+996/−620:删 modeTabs / Scene 01、USB 提示改顶部 Banner、新增 CompactMetricPanel + MetricSeriesToggleReadout、Memory/Display/通用卡片统一、启动时间表达重构、LivePreviewScreenshotCard → ScreenshotTimelineFrame、picker 改运行时可用性
MacApp/Sources/PerfowlCore/WorkbenchModels.swift+185:删 modeNote、删恒真的 selectionEnabled、新增 WorkbenchMetricAvailability / MetricAvailabilityContext / availability(for:in:)、startupTiming series 清空、displayPerformance 收敛为 FPS + Frame Time、新增 displayPerformanceReportOnlySeries
MacApp/Sources/PerfowlCore/DeviceFoundation.swift+150:单 hydration 任务 → 每设备任务 + 有界 sweep、retainingMissedNetworkRows(in:) 宽限窗口、networkMissStreak
MacApp/Sources/PerfowlCore/StartupTracePresentation.swift+52:startBoundaryMissing / endBoundaryMissing 与私有 isRequiredGap
MacApp/Sources/PerfowlCore/PerformanceSessionController.swift+7:发布 startupSourceUnavailable(逐字取自 app_ready)
MacApp/Sources/PerfowlCore/ScreenshotTimelineLayout.swift新增 106 行纯布局模型
MacApp/Tests/.../ScreenshotTimelineLayoutTests.swift新增 130 行 / 6 项
MacApp/Tests/.../DeviceFoundationNetworkStabilityTests.swift新增 238 行 / 6 项
另 4 个既有测试文件+497/−…(详见 §17)

16.1 §2.1 截图:从大卡片列表改成紧凑时间轴截图带

新增 ScreenshotTimelineLayout(纯布局模型,不依赖 SwiftUI 体,因此 “位置由 elapsedSeconds 线性决定”“刻度开销不随 Session 长度增长”这两条可以被 单测钉死):

第 t 秒捕获的帧左沿落在 t × pointsPerSecond,真实掉帧读出真实缺口, 而不是按固定步长把 1Hz 与掉线排成同一种样子。

2 h 步长 300s),标签数上限 41,不随 Session 长度增长。

报告 omittedSeconds**(UI 显示 +12s 之类的压缩提示),不假装这段是连续的。

UI 侧:LivePreviewScreenshotCard(160 宽、带容器背景/边框、重复打印 quality.rawValue / #sequence / bytes)→ ScreenshotTimelineFrame(96 宽、 无容器),无法解码的帧保留它在时间轴上的槽位并画 camera.slash。仍用 LazyHStack,间隔由有界 padding(.leading, gap.points) 给出,不每刷新重排 整段、不重复解码历史缩略图、不为每一帧建占位 View。实时带与空闲预览统一 .frame(height: 156),两者切换不再 reflow。

数据未删:LiveScreenshotPreviewItem 的 sequence / quality / thumbnailData 原样保留(ScreenshotTimelineLayoutTests 钉死)。

16.2 §2.2 / §2.3:Scene 01 与测试模式 Tab

“Scene 01” 用户可见文字(内部 scene 概念未用于展示)。

Text("网络测试") / Scene 01 / func modeTab(。

16.3 §3:启动时间表达

[]——这两个“进程观察值”是对同一次启动的第二份、且不完整的复述。 底层数据未删:ios.process.startup.observed_running_ms / foreground_ready_ms 仍在 metricIDsByGroup[.startupTiming],测试钉死它们仍 可被选中并请求。

头不再重复 Total · N/M phases · status rawValue。

Profile <version> 与 profile 选择理由;阶段行只留名称 + 耗时 + 时间轴, 删 status.rawValue、reason、#occurrence。

endBoundaryMissing,只从 Collector 自身端点证据推导—— totalSource/totalRepairReason 含 start-substituted / frame-end-repaired、 launchStartTimestampNs == nil、或必需阶段的 gap;notApplicable 阶段由 私有 isRequiredGap 显式排除,因此“App 没触发这个可选回调”永不触发红叉。 不得由“捕获到 6/9”推断端点缺失(测试用“9 个阶段只捕获 2 个但末端已测量 ⇒ 终点不报缺失”钉死)。

deviceLossDisplay / 9 阶段 ledger / decoderProfileVersion / algorithmVersion / profileSelectionReason 全部仍在(测试钉死),只是不在 常规工作台常驻展示。

16.4 §4:统一为 CPU Usage 风格

新增两个共享组件,之后所有指标面板同一套骨架:

accent,未选中弱化但数值仍可读)。

迁移:Memory(5 个两排大块 → 右上 5 个紧凑 toggle,删 APP/整机 · quality)、 Display、以及通用卡片(CPU / CPU Core / Network / Disk / GPU Usage / Energy / Thread / CSwitch / Wakeups / Battery / Thermal)。GPU Counter 保留单线选择器 (100+ counter 不适用逐条 toggle),沿用同一 readout 组件。

曲线开关只改本地 @State:不重新 prepare、不重启 source、允许全部隐藏 (删除旧的“至少保留一条”约束)。

16.5 §5:Display Performance

Frame Time)。

意义;两个 toggle 各自独立。

displayPerformanceReportOnlySeries(定义保留),且它们仍在 metricIDsByGroup[.displayPerformance]——Collector 合同、JSON/CSV schema、 报告统计完全未变;测试钉死三个 product metric ID 仍在请求集合内,且报告层 仍从 PerformanceSample.frameTimeP95Ms 等读值。

16.6 §6:添加指标列表

source-native、采样频率等实现词汇);每行 = 名称 + 一句中文含义。

WorkbenchMetricAvailability + MetricAvailabilityContext + WorkbenchMetricCatalog.availability(for:in:)。判据全部来自真实状态:无设备 → “未选择设备”;未 prepared → “准备完成后可用”;startupTiming → Collector app_ready 的 startupSourceUnavailable(为此在 PerformanceSessionController 新增发布属性,invalidateCollectorReadiness() 时清空);battery → route == .wirelessUserspace。未报告不等于不支持: 只有显式的 true 才置灰。

true,使条件行永远可选、选了又静默什么都不出),其 4 处 no-op 门禁随之删除。

16.7 §7:设备提示改成顶部 Banner

截图区两处 activeUSBStatusNotice overlay 全部删除;根 View 用 .overlay(alignment: .top) 承载新的 USBStatusBanner(胶囊、allowsHitTesting(false)、 zIndex(20)、从自定义标题栏下方 56pt 进入),沿用既有 1s 自动消失机制, 不推挤下方卡片。

16.8 §8:Wi‑Fi 设备列表稳定性

探测)→ networkHydrationTasks: [String: Task](每设备)+ networkHydrationSweep (有界批量,并发宽度 maxConcurrentNetworkProbes = 4)。

折叠成空列表,单一样本无法区分“本轮没查到”与“设备没了”。现在单轮缺失保留 上一轮 Network row,连续 discoveryMissStreakLimit = 2 轮缺失才移除; 只对 Network 生效,USB 拔线仍即时。

17. Validation Update(2026-09-20)

证据等级 E2 completed / E4 pending。

17.1 测试

Swift 全量:204 tests / 3 skipped / 0 failures(同口径基线 c9a133d 为 182 / 3 skipped / 0,+22 全为本批新增,无一项既有测试被删或放宽)。 新增与改写:

套件新增项钉住什么
ScreenshotTimelineLayoutTests6轴线性(t × pointsPerSecond)、一拍漂移不产生间隔而掉帧产生、刻度开销有界(30 min → 31 个 / 2 h → 25 个,上限 41)、超大间隔被压缩并如实报告省略秒数、ruler 刻度判定、item 元数据未删
DeviceFoundationNetworkStabilityTests6单轮空枚举保留 Wi‑Fi 行;连续 2 轮才移除;USB 拔线仍即时(宽限只给 Network);hydration 失败只标 offline 不删行;retry 一台不取消另一台的在途探测(旧代码会);并发探测 ≤ maxConcurrentNetworkProbes = 4
StartupTracePresentationTests+4起/止边界互相独立且不由阶段数推断;notApplicable 末端阶段永不触发终点红叉;审计数据(coverage / repair / device-loss / 9 阶段 ledger / decoder profile / algorithm version)未丢
WorkbenchDisplayContractTests+4截图带不含 quality.rawValue / #sequence / bytes 且异常用 camera.slash;USB 提示是顶层 Banner 而非截图 overlay;picker 无 modeNote 且用运行时可用性;启动卡 total 只出现一次、边界用红叉且不含 N/M phases
WorkbenchModelsTests+1条件指标按真实事实置灰(无设备 / 未 prepared / startupSourceUnavailable / route);picker 无实现词汇;.fps/.frameTime 不再有独立行;defaultSelection 改用 .displayPerformance 后 product metric ID 集合与旧 .fps 默认经 migratePreferences 后完全相等
DisplayPerformanceSwiftTests+1live 仅 FPS + Frame Time;P95/Jitter/Refresh Rate 定义与 product metric ID 仍在请求集合内

17.2 门禁

metric 合同)。

17.3 如实披露的偏差

  1. 通用卡片的 scope chip(APP/整机)一并移除。方案只对 Memory 明确要求

(§4.3),本批为“一套骨架”把它从所有通用卡片移除。scope 未删除:仍在 PerformanceSample.metricScopes 与 readout 的 hover tooltip 中(unit 同)。

  1. 启动轴标签 “Raw Span 审计轴 / Collector 启动轴” 删除。方案未逐条列出,

属 §1 “减少内部实现噪音”目标;axisUsesAuditFallback 数据字段保留。

  1. defaultSelection 的 .fps 改为 .displayPerformance。这不是采集合同

变化:scopedCapturePlan 恒先 migratePreferences,测试用集合相等钉死 “新旧默认经迁移后请求的 product metric ID 完全一致”。

  1. 删除恒真的 selectionEnabled 及其 4 处门禁。该属性返回常量 true,

那些门禁从未真正拦住任何东西;正确的门禁换成 WorkbenchMetricCatalog.availability(for:in:)。

  1. §13 性能门禁的真机测量未执行(Workbench 首屏响应、30 min 截图滚动、

CPU/内存峰值、Wi‑Fi refresh 耗时)。实现 AI 只交付 E2,真机由用户执行。 有界性已由单测钉死:刻度 ≤ 41、间隔 ≤ 64pt、并发 ≤ 4、LazyHStack 惰性、 不重复解码。

  1. 一条既有负载敏感 flake(非本批回归):

PerformanceFoundationTests.testSessionControllerPassesNestedCoreSessionDirectory 的等待预算是 100 × 20ms = 2.0s,空载单跑实测 1.75–2.05s(基线 c9a133d 单独跑 3/3 通过,1.751 / 1.869 / 1.904s),已经紧邻预算。本机 被多个 AI 共用、负载均值曾在 19 → 79 摆动,负载 79 时全量套件中该用例 超时失败(本批树同一时间也是全量失败);负载回落到 20 后本批树全量 204 / 3 skipped / 0 failures、基线全量 182 / 3 skipped / 0 failures。 未放宽任何阈值,也未改该用例。

17.4 未做与下一步

与 §13 真机性能测量;未构建测试包;未声明 validated。

(§13,含 30 分钟截图长测与 Wi‑Fi 反复刷新)**,通过后本批才能关门。