首页
/ opencode 手动性能测试套件:基于 Playwright 与 Chrome Trace 的 App 性能诊断实践

opencode 手动性能测试套件:基于 Playwright 与 Chrome Trace 的 App 性能诊断实践

2026-09-06 09:20:21作者:昌雅子Ethen

本文围绕 packages/app/e2e/performance 目录下的手动性能测试套件(manual performance suite)展开:它是 opencode 桌面应用(packages/app,Solid + Vite 构建的渲染进程)用来量化高负载交互性能的诊断体系,被刻意排除在常规本地与 CI 的 Playwright 发现流程之外。读完后你将掌握:如何构建并运行生产包下的基准场景、如何理解 benchmark fixture 的指标上报契约(BENCHMARK / BENCHMARK_PAGE JSON 行)、如何用 Chrome DevTools Trace 做渲染进程级剖析,以及套件遵循的"只断言场景完成与指标采集、不设定机器相关性能预算"的设计原则。

一、套件定位与运行机制

应用的高负载性能诊断集中在 packages/app/e2e/performance,与常规回归测试物理隔离。它被排除在正常本地和 CI 的 Playwright 发现之外,原因是基准测试运行时间长、且依赖生产构建,不适合混入快速反馈回路。

基准配置的核心行为定义在 playwright.config.ts:它继承主 Playwright 配置,将 testDir 锁定到 performance 目录本身(unit/** 被排除在 Playwright 发现外),并在 webServer 中强制使用生产构建:

// packages/app/e2e/performance/playwright.config.ts(节选)
webServer: {
  ...config.webServer,
  command: `bun run build && bun run serve -- --host 0.0.0.0 --port ${port} --strictPort`,
  reuseExistingServer: false,
}

即:bun run build 构建应用、再以 vite preview 托管生产包,然后串行执行场景(fullyParallel: falseworkers: 1)。这与 AGENTS.md 中"对生产构建运行基准""串行运行基准以避免跨测试竞争"的原则一一对应。

1.1 运行方式

packages/app 目录显式运行:

bun run test:bench

packages/app/package.json 中,该脚本展开为两步:

"test:bench": "bun test ./e2e/performance/unit && playwright test --config e2e/performance/playwright.config.ts"

先跑 e2e/performance/unit 下的纯单元测试(指标计算、探针、trace 写出等模块的确定性验证),再跑 Playwright 浏览器基准。

Windows PowerShell 下需要显式设置单 worker:

$env:PLAYWRIGHT_WORKERS = "1"
bun run test:bench

此外还有一组配套的显式入口:

# 运行指定场景(可配合 trace 目录,见下文)
bunx playwright test --config e2e/performance/playwright.config.ts \
  timeline/session-tab-switch-benchmark.spec.ts

1.2 场景覆盖范围

套件当前的基准场景覆盖五类高价值交互(可在各 spec 文件中逐一核对):

  • 冷/热会话标签页切换计时session-tab-switch-benchmark.spec.ts 对 cold/hot 各跑 5 轮(可用 SESSION_TAB_SWITCH_RUNS 覆盖),v2 布局下再按 review 面板开/关分拆,统计 firstDestinationObservedMsfirstCorrectObservedMsstableObservedMs 的 min/median/max;
  • 首页会话点击计时home-tab-navigation-benchmark.spec.ts 将"打开首页会话并绘制标题栏 tab"拆分为 content 与 titlebar-tab paint 两段测量;
  • 单会话标签关闭计时:关闭唯一会话 tab 后经由稳定的 home 恢复路径绘制首页;
  • 缓存会话重绘与变更追踪session-tab-flash.spec.ts 采样点击后缓存会话的重绘,并验证所有已打开会话 tab 的预取行为;
  • 流式时间线吞吐诊断session-timeline-benchmark.spec.ts 度量流式渲染的吞吐、RAF 回调间隔分布、长任务、几何稳定性与重挂载诊断,并按"新布局开关 × review 面板开/关 × 是否携带 diff"组合出多个场景。

已提交的 smoke 与 regression 测试则继续负责分页、tab 绘制、上下文调整、折叠状态、composer 间距等正确性覆盖——基准套件不重复承担正确性断言。

二、benchmark fixture:统一的上报契约

所有基准共享同一个 benchmark fixture,定义在 benchmark.ts。一个新基准应当"看起来像一个普通 Playwright 测试":

import { benchmark, expect } from "../benchmark"

benchmark("measures one interaction", async ({ page, report }) => {
  // Only scenario-specific setup and interaction belong here.
  report({ durationMs: 42 })
})

fixture 的行为约束(对照 benchmark.ts 源码):

  • 强制 report()report 只允许调用一次,重复调用直接抛错(Benchmark reported metrics more than once);benchmarkResultauto: true 的钩子,测试结束时若没有上报且测试状态与预期一致,会以 Benchmark did not report metrics 让该测试失败——这是"断言指标采集完成"而非断言数值的关键实现;
  • 自动命名与关闭 trace:通过覆写 page fixture,为每个页面安装 observePerformancePage 诊断,结束时调用 reportPerformancePage 停止 trace 并输出诊断行;
  • 导航历史捕获与失败附载framenavigated 事件记录主框架导航 URL 序列;当 testInfo.status !== testInfo.expectedStatus 时,把导航历史以 performance-navigations 附件挂到测试报告上,便于回放失败场景的页面路径;
  • 一致的指标输出:每个基准结束时输出一行 BENCHMARK JSON。

2.1 BENCHMARK / BENCHMARK_PAGE 两行诊断协议

源码 benchmark.ts#L29-L46 输出的 BENCHMARK 行比 README 简写版更完整:

BENCHMARK {"schemaVersion":2,"runID":"...","name":"...","status":"passed","expectedStatus":"expected","retry":0,
           "repeatEachIndex":0,
           "context":{"project":"chromium","platform":"darwin", ...场景 context},
           "metrics":{...},
           "error":undefined}

每个被观察的页面在最终带状态的 BENCHMARK 记录之前,先输出一条 BENCHMARK_PAGEbenchmark.ts#L120-L138),携带同一 runID、导航历史与可选的 trace 文件路径:

BENCHMARK_PAGE {"schemaVersion":2,"runID":"...","name":"...","context":{"platform":"...","trace":".../xxx.json",
             "selectorTrace":false},
             "navigations":["https://..."]}

runID 由 playwright.config.ts#L5 生成(ISO 时间戳 + 进程 PID 的 OPENCODE_PERFORMANCE_RUN_ID),用于把一次运行中所有页面级诊断与基准结果关联起来。

2.2 隔离上下文:withBenchmarkPage

需要隔离浏览器上下文(例如冷/热切换的每轮试验都要干净的环境)时,使用 withBenchmarkPagebenchmark.ts#L100-L118):它自行 browser.newContext()、创建页面、安装同一套诊断生命周期,并在结束时关闭 context。会话 tab 切换基准正是用它包裹每一轮 cold/hot 试验,保证 5 轮结果互不污染:

results[mode].push(
  await withBenchmarkPage(browser, `session-tab-switch-${mode}-${run}`, (page) => trial(page, mode), testInfo),
)

三、Chrome Trace:页面全生命周期的标准轨迹采集

设置 OPENCODE_PERFORMANCE_TRACE_DIR 后,每个基准页面都会自动产出标准 Chrome DevTools trace(无需在测试内写任何 trace 代码),可直接加载进 Chrome DevTools 的 Performance 面板:

OPENCODE_PERFORMANCE_TRACE_DIR=/tmp/opencode-performance-traces \
bunx playwright test --config e2e/performance/playwright.config.ts \
  timeline/session-tab-switch-benchmark.spec.ts

3.1 采集实现:CDP Tracing + ReturnAsStream

chrome-trace.tsstartChromeTrace 通过 CDP 会话采集轨迹,要点(chrome-trace.ts#L21-L67):

  • 类别配置:默认排除 -* 前缀的类别,包含 devtools.timelinev8.executelatencyInfodisabled-by-default-devtools.timeline.stack 等;当 OPENCODE_PERFORMANCE_SELECTOR_TRACE=1 时,额外开启 disabled-by-default-blink.debugdisabled-by-default-devtools.timeline.invalidationTracking——这是 selector 统计(INP 类分析的前置数据)所需的增强采集
  • 传输模式Tracing.start 使用 ReturnAsStream,停止后经 Tracing.tracingComplete 拿到流句柄,再用 IO.read 分块落盘(chrome-trace.ts#L84-L95);
  • 完整性保证:先写 ${file}.partial,若 Chromium 报告 dataLossOccurred 则保留 partial 文件并抛错 Chrome trace lost data,成功才 rename 为正式文件——对应 Puppeteer 官方 tracing 的默认生命周期与失败语义;
  • 文件命名${runID}-${sanitized-name}-${sha256(name)[0:8]}-${nonce}[-selectors].json,trace 路径随后出现在 BENCHMARK_PAGE 行中,供命令行工具消费:
bunx devtools-tracing stats <trace-path-from-BENCHMARK_PAGE>

README 明确边界:Chrome trace 是浏览器级、页面全生命周期的诊断数据;而场景指标(如首帧可见时间、稳定时间)使用的是更窄的显式命名观察窗口。二者的分工写进了 AGENTS.md:"不要跨 harness、probe 和 trace 重复测量;自定义探针只用于产品特有度量"。

INP 分析需要包含受支持的导航/交互 insight 的 trace;selector 统计则需要以 OPENCODE_PERFORMANCE_SELECTOR_TRACE=1 捕获。

3.2 去帧率限制的 uncapped 配置

playwright.uncapped.config.ts 在默认配置上追加启动参数 --disable-frame-rate-limit--disable-gpu-vsync,用于显式的去上限诊断(例如观察 RAF 间隔分布时排除合成器节流干扰)。README 同时声明:原生产品基准应使用默认 Playwright 配置。

四、剖析开关、环境变量与指标语义

4.1 剖析开关(默认关闭)

CPU 与高负载视觉剖析默认禁用,以保持基准负载的确定性:

环境变量 作用
TIMELINE_CPU_PROFILE=1 同时开启 CPU profile 与视觉剖析
TIMELINE_VISUAL_PROFILE=0 在已开启 CPU profile 时仅保留 CPU 剖析
OPENCODE_PERFORMANCE_TRACE_DIR 开启全页面 Chrome trace 采集
OPENCODE_PERFORMANCE_SELECTOR_TRACE=1 trace 中追加 selector/失效跟踪类别(INP、selector 统计所需)
OPENCODE_PERFORMANCE_RUN_ID 诊断行 runID,默认由配置自动生成

这与 AGENTS.md 的原则"当详细剖析会改变负载行为时保持其 opt-in"一致——视觉剖析的截图读取会扰动计时,因此默认关闭。

4.2 流式场景的负载参数

流式时间线场景的默认值与可调项(对照 session-timeline-benchmark.spec.ts#L102-L110 的解析代码):

环境变量 默认值 含义
TIMELINE_DELTA_COUNT 160 流式增量事件数量;覆盖时会随结果 context 上报
TIMELINE_HISTORY_TURNS 320 会话历史轮数(DOM 规模)
TIMELINE_EVENT_BATCH 1 事件投递批大小;覆盖时同样进入 context
TIMELINE_CPU_THROTTLE 30 30x CPU 节流系数
TIMELINE_COMPLETION_TIMEOUT_MS 420000 流式完成等待超时(测试超时在其上再加 60s)
TIMELINE_MINIMAL =1 时使用最小化负载布局
SESSION_TAB_SWITCH_RUNS 5 tab 切换基准每种模式轮次

4.3 指标语义的三条重要边界

README 对流式基准的指标给出了明确的解释边界,值得在引用数据时牢记:

  1. 30x CPU 节流是确定性压力画像,不是模拟真实低端设备;同样,stability 套件中的 4x CPU 压力也如此声明;
  2. 这些指标是主线程回调诊断:吞吐、RAF 回调间隔分布、帧预算等价值、长任务,都度量到"渲染器观察到的完成时间"与"最终几何稳定"为止——不是合成器呈现或掉帧测量;探针禁用的视觉/几何指标会以 null 呈现;
  3. 基准不断言机器相关的性能预算。断言只验证"场景完成 + 指标采集完成"。重绘状态做游程分组(run-length grouping)用于压缩,但每条原始观察时间戳与原始 mutation 批次、布局偏移都无损保留——对应 AGENTS.md 的"保留原始诊断数据或采用无损表示"。

五、与 timeline-stability 套件的分工

e2e/performance 下还有第二个独立配置目录 timeline-stability,通过 bun run test:stability 运行(见 packages/app/package.json#L30):它对时间线布局连续性做可判定的合约测试(锚点保持、行序、披露状态在虚拟化中的保持、键盘/滚轮所有权一致等),并对失败场景保留 video.webmtrace.zip、失败截图、采样 DOM/布局 trace JSON 等证据。

两者分工清晰:stability 套件负责"布局连续性合约是否被违反"的 pass/fail 判定;performance 套件负责"交互到底花了多久、主线程回调分布如何"的诊断量化。前者的判明边界(不检查每个合成器呈现像素、不覆盖特定刷新率/设备)在其 README 中有专门声明。

六、方法论依据与扩展边界

套件的方法论直接对齐浏览器与框架生态的官方建议:Electron 官方教程推荐用重复的 Chrome DevTools / Chrome Tracing 测量做性能回归;Chrome DevTools 官方文档推荐用 Performance 录制刻画运行时工作;Playwright 官方将 trace 定位为测试调试工具而非渲染进程剖析工具——因此本套件选择"Playwright 管场景编排与完成检查、Chrome trace 管通用浏览器剖析、自定义探针只做产品特有测量"的三层结构。

对于未来的打包版(Electron)基准,README 给出了明确路线:若需要主进程与多进程归因,应使用 Electron 官方 contentTracing API,而不是往这套渲染器 harness 里加自制的进程级插桩。当前套件剖析的对象始终是 Chromium 中的共享 app 渲染进程。

参考路径汇总

登录后查看全文
热门项目推荐
相关项目推荐