【免费下载链接】pierre
pierre’s open source code
导读
本文是一份可复用的性能基准测试 runbook(操作手册),面向 Pierre 仓库中 DiffsHub 生产模式下的滚动性能回归测试。核心方法是:锁定两个 git SHA(baseline 与 test),分别在两个独立 worktree 中构建并启动 DiffsHub 生产服务器,用统一的滚动驱动脚本滚动一个大型渲染 diff,同时用 Chrome performance trace 记录渲染主线程上的样式计算、布局、绘制等事件,最终量化对比两个 SHA 之间的 CSS 性能差异。读完本文,你将掌握一套端到端、可复现、可归档的基准测试流程,能够独立完成 CSS 选择器、布局、containment、绘制与滚动条相关的性能改动验证。
本文对应的完整官方文档为 CSS_PERFORMANCE_BENCHMARK.md,其中所有命令与脚本均基于当前仓库实测整理,可直接复制运行。
基准测试的定位与适用场景
DiffsHub 是一个在浏览器中渲染大型代码 diff 的应用,滚动长 diff 时浏览器主线程的工作量直接决定用户观感。该基准测试专门用于回答一个精确的问题:两个 git SHA 之间,滚动渲染大型 diff 的 CSS/渲染性能是否发生了显著变化。
它记录的并不是 FPS 或帧时间,而是 Chrome performance trace 中 renderer 主线程上可归类的事件耗时,重点覆盖:
- CSS 选择器匹配与样式计算(
UpdateLayoutTree) - 布局(
Layout) - 预绘制与绘制(
PrePaint、Paint、PaintImage) - 图层管理(
Layerize、UpdateLayer、ScrollLayer、Commit) - 任务调度与事件分发(
RunTask、FunctionCall、EventDispatch、FireAnimationFrame) - HTML 解析(
ParseHTML)
适合验证的改动类型包括 CSS 选择器、布局、containment、paint 与滚动条相关的变更。从 DiffsHubViewer.tsx 可以看到,实际滚动容器使用了cv-scrollbar类,并显式应用了[contain:strict]、[overflow-anchor:none]、[will-change:scroll-position]等高成本 CSS 属性——这类属性正是该基准测试关注的核心对象。
一、前置要求:Chrome Trace 捕获
录制 Chrome performance trace 是整条流水线的前提。文档给出的工具优先级如下:
- Chrome DevTools MCP(首选):用于浏览器导航、稳定页检查、performance trace 录制、页面脚本执行与 trace 导出;
- 直接 Chrome CDP 或 Playwright CDP(备选):当 DevTools MCP 不可用或无法可靠导出 trace 时使用。
整个基准测试期间必须遵守严格的单一一致性约束:
- 只使用一种 trace 工具;
- 只使用一种浏览器模式(headed 或 headless,两个 SHA 必须一致);
- 两个 SHA 之间不得混用浏览器版本、trace categories、profile/cache 处理方式。
任何违反一致性的做法都会让对比失去意义,因为 trace 数据的可比性是本基准的立身之本。
二、选择测试模式:两种模式及取舍
整个基准测试期间,两个 SHA 必须使用同一种模式。
模式一:隔离纯文本模式(Isolated Plain-Text Mode,CSS 专项推荐)
这是 CSS-only 调查场景下的推荐模式。做法是在两个 worktree 中分别打同一个临时补丁:在 WorkerPoolManager.ts 中,于以下四个方法的开头立即插入return;:
highlightFileASTprimeFileHighlightCachehighlightDiffASTprimeDiffHighlightCache
以highlightDiffAST为例,补丁后的形态为:
public highlightDiffAST( instance: DiffRendererInstance, diff: FileDiffMetadata ): void { return; const cachedResult = this.getDiffResultCache(diff); // existing code continues... }为什么要打这个补丁?从源码看,这四个方法负责把文件/diff 提交给 worker 池做语法高亮与高亮缓存预热(见 WorkerPoolManager.ts)。在它们开头直接return,相当于临时禁用异步高亮:trace 里不再出现 syntax-highlight token span 与 worker 高亮结果的干扰,样式/布局/绘制成本被"隔离"出来,信噪比最高。
注意:该补丁不要 commit,清理阶段必须还原。
模式二:高亮生产模式(Highlighted Production Mode)
仅当你有意测量完整生产行为时才使用。它更贴近真实用户,但 CSS 小差异通常更难检测,因为高亮引入的 DOM 与 worker 噪音会淹没细微变化。该模式不要 stub 高亮——保持真实高亮行为。
三、定义输入:锁定 SHA、slug 与 worktree 路径
选定精确 commit
export BASE_SHA=<baseline-sha> export TEST_SHA=<test-sha> git rev-parse --short "$BASE_SHA" git rev-parse --short "$TEST_SHA"指定唯一 worktree slug
如果基准测试需要重跑,使用唯一的 slug 避免冲突:
export BASE_SLUG=css-perf-base export TEST_SLUG=css-perf-testworktree 根目录
关键约定:基准测试 worktree 必须放在全局临时目录下,而不是 home 目录下的 Pierre 主 worktree 根目录:
export BENCHMARK_WORKTREE_ROOT=/tmp/pierre-css-benchmark-worktrees export BASE_WORKTREE=$BENCHMARK_WORKTREE_ROOT/$BASE_SLUG export TEST_WORKTREE=$BENCHMARK_WORKTREE_ROOT/$TEST_SLUG这种放置方式适合短生命周期基准运行。不要用/tmp存放长期 worktree,因为操作系统或清理任务可能删除临时文件。
四、创建 worktree 并初始化 Pierre worktree 元数据
从仓库根目录创建两个临时 worktree(detached 模式),并在每个 worktree 内初始化 Pierre 的 worktree 元数据:
export AGENT=1 mkdir -p "$BENCHMARK_WORKTREE_ROOT" git worktree add --detach "$BASE_WORKTREE" "$BASE_SHA" git worktree add --detach "$TEST_WORKTREE" "$TEST_SHA"cd "$BASE_WORKTREE" export AGENT=1 moonx root:wt -- setupcd "$TEST_WORKTREE" export AGENT=1 moonx root:wt -- setupmoonx root:wt -- setup底层调用的是 scripts/wt.ts 中的setupWorktree:它会写入每个 worktree 的.env.worktree、安装依赖,并打印每个 worktree 的端口偏移量(port offset)。
端口计算规则:DiffsHub 运行在
3692 + PIERRE_PORT_OFFSET例如:
offset 20 -> http://localhost:3712 offset 30 -> http://localhost:3722这与 wt.ts 中定义的端口基准表一致——docsDiffshub: 3692正是 DiffsHub 的端口基址,worktree 通过偏移量获得独立端口,互不冲突。
记录两个 DiffsHub 端口:
export BASE_PORT=<base-port> export TEST_PORT=<test-port>五、隔离纯文本模式的临时补丁
若测量高亮生产模式,可跳过本节。
在两个 worktree 中分别对 packages/diffs/src/worker/WorkerPoolManager.ts 应用同一临时编辑——在以下方法开头立即return;:
packages/diffs/src/worker/WorkerPoolManager.ts方法列表:
highlightFileAST primeFileHighlightCache highlightDiffAST primeDiffHighlightCache示例(highlightDiffAST):
public highlightDiffAST( instance: DiffRendererInstance, diff: FileDiffMetadata ): void { return; const cachedResult = this.getDiffResultCache(diff); // existing code continues... }从源码实现看,这四个方法本身带有缓存命中/纯文本短路逻辑(如highlightDiffAST中isDiffPlainText(diff)与areDiffRenderOptionsEqual的检查),说明高亮任务天然存在去重机制;而补丁是在更上游的位置直接切断任务提交,使 trace 中完全不含高亮渲染路径的事件。不要 commit 此补丁,清理阶段必须还原。
六、构建两个 worktree
从各自根目录构建:
cd "$BASE_WORKTREE" export AGENT=1 moonx diffshub:buildcd "$TEST_WORKTREE" export AGENT=1 moonx diffshub:build七、启动生产服务器
从各自的 DiffsHub app 目录启动生产服务器(注意指定端口):
cd "$BASE_WORKTREE/apps/diffshub" nohup env AGENT=1 PORT="$BASE_PORT" moonx diffshub:start > /tmp/diffshub-base.log 2>&1 & export BASE_SERVER_PID=$!cd "$TEST_WORKTREE/apps/diffshub" nohup env AGENT=1 PORT="$TEST_PORT" moonx diffshub:start > /tmp/diffshub-test.log 2>&1 & export TEST_SERVER_PID=$!等待两个服务器就绪:
curl -fsS --retry 30 --retry-delay 1 "http://localhost:$BASE_PORT" > /dev/null curl -fsS --retry 30 --retry-delay 1 "http://localhost:$TEST_PORT" > /dev/null八、确定测试页面与稳定条件
两个 SHA 必须使用同一个路由。路由要足够大,能触发虚拟化滚动(virtualized scrolling),并且在开始录制 trace 前保持稳定。
默认路由:
/oven-sh/bun/pull/30412录制前必须等待以下条件全部为真:
document.querySelector('.cv-scrollbar') instanceof HTMLElement; document.querySelector('button[aria-label="Start autoscroll"]') instanceof HTMLElement; !document.body.innerText.includes('STREAMING');这些条件分别对应:滚动容器已挂载、自动滚动控制按钮已出现、diff 内容流式加载已完成。.cv-scrollbar滚动容器与Start autoscroll按钮可以在 DiffsHubViewer.tsx 与 WorkerPoolStatus.tsx 中找到对应实现(按钮由AutoScrollTester驱动)。
高亮生产模式额外要求:等待 worker/highlight 统计信息变为空闲,或已足够长时间不再变化,确保高亮结果不太可能在 trace 录制期间落地。
九、录制 Trace:warmup 与 kept runs
使用 Chrome DevTools MCP、直接 Chrome CDP 或 Playwright CDP 录制。trace 文件必须包含 renderer-main 事件,如UpdateLayoutTree与Layout。
关键参数:
- 固定 viewport:每次运行相同,例如
1440x1000; - 统一浏览器模式:每次运行相同,并记录是 headed 还是 headless;
- 每个 SHA 先跑一次不录制的 warmup,然后录制至少 3 次有效 run(kept runs);
- 所有 warmup 与 kept runs 使用同一个滚动驱动。
推荐的交替执行顺序(避免两个 SHA 并发录制):
base-warmup test-warmup base-1 test-1 base-2 test-2 base-3 test-3不要同时录制两个 SHA。交替运行有助于降低缓存状态、JIT 预热、机器温度与后台负载带来的漂移。Warmup 结果必须满足与 kept runs 相同的滚动校验,但不计入平均值。
每次 warmup 流程:
- 导航到
http://localhost:<PORT><ROUTE>; - 等待上述稳定页条件;
- 不录制 trace,执行一次下方页面脚本;
- 校验返回的滚动结果,然后从指标汇总中丢弃。
每次 kept run 流程:
- 导航到
http://localhost:<PORT><ROUTE>; - 等待上述稳定页条件;
- 不刷新页面,开始录制 performance trace;
- 执行一次下方页面脚本;
- 停止并保存 trace;
- 将返回的滚动结果与 trace 文件名一并保存。
如果 trace 启动、停止、导出或浏览器连接失败,丢弃该 trace 并重跑,不要保留失败导出的部分 trace 文件。若失败导致你更换了 trace 工具或浏览器模式,则必须用新工具/新模式重跑全部 run,确保所有 kept traces 可比。
直接使用 Chrome CDP 时,至少包含以下 trace categories(逗号分隔):
devtools.timeline disabled-by-default-devtools.timeline disabled-by-default-devtools.timeline.frame toplevel blink cc v8建议使用专用 Chrome profile 目录,并在 headless 或后台运行时禁用后台节流,例如启动 Chrome 时携带:
--user-data-dir=<temp-dir> --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding十、滚动驱动脚本(核心)
驱动一:确定性固定距离滚动(CSS 专项推荐)
该脚本通过每次 trace 施加相同的scrollTop序列,避开 autoscroll 定时器/帧率漂移。它驱动真实的.cv-scrollbar元素,与用户滚动喂入相同的 renderer 状态。不要派发合成的scroll事件——它们不会移动浏览器滚动状态,也不代表真实渲染工作。
targetScrollTop必须安全低于两个 SHA 的scrollHeight - clientHeight。默认路由目前支持1_000_000。
async () => { const scroller = document.querySelector('.cv-scrollbar'); if (!(scroller instanceof HTMLElement)) throw new Error('missing scroller'); const pause = document.querySelector('button[aria-label="Pause autoscroll"]'); if (pause instanceof HTMLElement) pause.click(); const targetScrollTop = 1_000_000; const durationMs = 5_000; const steps = 300; const maxScrollTop = scroller.scrollHeight - scroller.clientHeight; if (targetScrollTop > maxScrollTop) { throw new Error( `targetScrollTop ${targetScrollTop} exceeds maxScrollTop ${maxScrollTop}` ); } const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve)); const previousScrollBehavior = scroller.style.scrollBehavior; scroller.style.scrollBehavior = 'auto'; try { scroller.scrollTop = 0; await nextFrame(); await nextFrame(); const startTime = performance.now(); let positionChecksum = 0; for (let step = 1; step <= steps; step++) { const targetTime = startTime + (durationMs * step) / steps; while (performance.now() < targetTime) { await nextFrame(); } const expectedScrollTop = Math.round((targetScrollTop * step) / steps); scroller.scrollTop = expectedScrollTop; await nextFrame(); const actualScrollTop = Math.round(scroller.scrollTop); if (actualScrollTop !== expectedScrollTop) { throw new Error( `scrollTop mismatch at step ${step}: expected ${expectedScrollTop}, got ${scroller.scrollTop}` ); } positionChecksum += actualScrollTop * step; } await nextFrame(); return { scrollTop: Math.round(scroller.scrollTop), targetScrollTop, durationMs: performance.now() - startTime, steps, positionChecksum, scrollHeight: scroller.scrollHeight, clientHeight: scroller.clientHeight, }; } finally { scroller.style.scrollBehavior = previousScrollBehavior; } };校验规则:每个 kept trace 的scrollTop、targetScrollTop、steps、positionChecksum必须在两个 SHA 之间一致。任何报告 mismatch 或抛错的 trace 都要重跑。
驱动二:产品自动滚动(仅在有意测量产品 autoscroll 行为时使用)
async () => { const scroller = document.querySelector('.cv-scrollbar'); if (!(scroller instanceof HTMLElement)) throw new Error('missing scroller'); const pause = document.querySelector('button[aria-label="Pause autoscroll"]'); if (pause instanceof HTMLElement) pause.click(); scroller.scrollTop = 0; await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)) ); const start = document.querySelector('button[aria-label="Start autoscroll"]'); if (!(start instanceof HTMLElement)) { throw new Error('missing autoscroll start'); } start.click(); await new Promise((resolve) => setTimeout(resolve, 5000)); const stop = document.querySelector('button[aria-label="Pause autoscroll"]'); if (stop instanceof HTMLElement) stop.click(); return { scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, clientHeight: scroller.clientHeight, }; };推荐的 trace 文件名:
/tmp/diffshub-traces/base-1.json /tmp/diffshub-traces/base-2.json /tmp/diffshub-traces/base-3.json /tmp/diffshub-traces/test-1.json /tmp/diffshub-traces/test-2.json /tmp/diffshub-traces/test-3.json十一、分析 Trace:Bun 聚合脚本
录制完成后,用以下 bun 脚本聚合指标。按需修改dir与文件名:
bun -e ' const fs = require("fs"); const dir = "/tmp/diffshub-traces"; const groups = { base: ["base-1.json", "base-2.json", "base-3.json"], test: ["test-1.json", "test-2.json", "test-3.json"], }; const metrics = [ "UpdateLayoutTree", "Layout", "PrePaint", "Paint", "PaintImage", "Layerize", "UpdateLayer", "ScrollLayer", "Commit", "RunTask", "FunctionCall", "EventDispatch", "FireAnimationFrame", "ParseHTML", ]; function average(values) { return values.reduce((sum, value) => sum + value, 0) / values.length; } function median(values) { const sorted = [...values].sort((a, b) => a - b); const middle = Math.floor(sorted.length / 2); return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle]; } function round(value) { return +value.toFixed(2); } function summarizeValues(values) { return { average: round(average(values)), median: round(median(values)), min: round(Math.min(...values)), max: round(Math.max(...values)), }; } function summarizeTrace(file) { const data = JSON.parse(fs.readFileSync(`${dir}/${file}`, "utf8")); const events = data.traceEvents || data; const threads = new Map(); for (const event of events) { if (event.ph === "M" && event.name === "thread_name") { threads.set(`${event.pid}:${event.tid}`, event.args?.name); } } const totals = Object.fromEntries(metrics.map((metric) => [metric, 0])); for (const event of events) { if (event.ph !== "X" || !metrics.includes(event.name)) continue; if (threads.get(`${event.pid}:${event.tid}`) !== "CrRendererMain") continue; totals[event.name] += (event.dur || 0) / 1000; } return totals; } function summarizeRun(file) { const row = summarizeTrace(file); const paintComposite = row.PrePaint + row.Paint + row.PaintImage + row.Layerize + row.UpdateLayer + row.ScrollLayer + row.Commit; return { file, updateLayoutTree: row.UpdateLayoutTree, layout: row.Layout, styleLayout: row.UpdateLayoutTree + row.Layout, paintComposite, }; } function summarizeGroup(files) { const runs = files.map(summarizeRun); return { runs: runs.map((run) => ({ file: run.file, updateLayoutTree: round(run.updateLayoutTree), layout: round(run.layout), styleLayout: round(run.styleLayout), paintComposite: round(run.paintComposite), })), summary: { updateLayoutTree: summarizeValues( runs.map((run) => run.updateLayoutTree) ), layout: summarizeValues(runs.map((run) => run.layout)), styleLayout: summarizeValues(runs.map((run) => run.styleLayout)), paintComposite: summarizeValues(runs.map((run) => run.paintComposite)), }, }; } for (const [label, files] of Object.entries(groups)) { console.log(label, JSON.stringify(summarizeGroup(files), null, 2)); } '脚本逻辑说明:
- 通过
ph === "M"的thread_name元事件建立pid:tid -> 线程名映射; - 只统计
CrRendererMain(渲染主线程)上ph === "X"(完整事件)且命中指标名的事件; - 事件耗时
dur以微秒计,除以 1000 转为毫秒后累加; - 输出每个 run 的原始值,以及每个组的 average / median / min / max;
- 聚合出
styleLayout(样式+布局)与paintComposite(绘制+合成)两组综合指标。
滚动距离归一化
使用确定性固定距离滚动驱动时,kept runs 应具有相同的scrollTop、targetScrollTop、steps、positionChecksum。若使用产品 autoscroll 或任何滚动距离不同的驱动,需在求平均值前对每个 run 归一化:
metric_ms_per_million_px = metric_ms / (scrollTop / 1_000_000)十二、报告结果:必含清单
一份完整的基准报告应包含:
BASE_SHA与TEST_SHA- route
- viewport
- trace 工具:Chrome DevTools MCP、直接 Chrome CDP 或 Playwright CDP
- 浏览器模式:headed 或 headless
- 测试模式:isolated plain-text 或 highlighted production
- 滚动驱动:deterministic fixed-distance 或 product autoscroll
- 运行顺序与 warmup 策略
- run 数量与每 run 秒数,或固定距离 run 返回的
durationMs - 固定距离 run 的
targetScrollTop、steps、positionChecksum - 每个 SHA 的平均滚动距离
- 每 run 的原始指标
- 原始指标的 average、median、min、max
- 滚动距离不同时的归一化指标平均值
- 丢弃的 trace 或 trace 收集问题
结论要谨慎。浏览器 trace 本身噪声很大。对于高亮生产模式,低于 1% 的差异通常无法定论,除非在更多 run 上稳定复现。
十三、清理:还原环境
停止服务器:
kill "$BASE_SERVER_PID" "$TEST_SERVER_PID"移除 worktree:
cd <main-repo-root> moonx root:wt -- rm "$BASE_SLUG" --force moonx root:wt -- rm "$TEST_SLUG" --force rmdir "$BENCHMARK_WORKTREE_ROOT" 2>/dev/null || true如果使用了隔离纯文本模式,确保任何剩余 working tree 中的临时高亮 stub 都已移除。确认最终状态:
git status --short moonx root:wt -- psmoonx root:wt -- rm与-- ps同样由 scripts/wt.ts 实现,rm负责拆除 worktree 并释放端口偏移,ps用于列出当前全部 worktree 及其状态——这两条命令配合git status --short即可验证环境已完全还原。
小结
整套流程可以概括为一条可复用的链路:锁定 SHA → 临时 worktree +wt setup→ 按模式打/不打高亮补丁 → 构建并启动双服务器 → 稳定页校验 → 固定距离滚动驱动 + Chrome trace 录制 → bun 聚合 CrRendererMain 指标 → 谨慎解读与归档报告 → 清理还原。其中两个最关键的纪律是:全程保持工具/浏览器模式/驱动一致,以及只对 kept runs 取平均并警惕亚 1% 的噪声差异。遵循本文流程,你就能为 DiffsHub 的任何 CSS 渲染改动建立可信的量化对比基线。
【免费下载链接】pierre
pierre’s open source code
相关推荐
uftrace 实战教程:5个高效性能分析技巧与案例
uftrace 实战教程:5个高效性能分析技巧与案例 uftrace 是一款强大的 C/C++/Rust/Python 函数调用图追踪工具,能够帮助开发者快速定
开发工具AppFlowy-Cloud性能基准:基准测试与性能对比
AppFlowy Cloud性能基准:基准测试与性能对比 概述 AppFlowy Cloud作为开源Notion替代方案的核心后端服务,其性能表现直接影响用户体
后端云原生yuzu Switch 模拟器快速上手:从放密钥到跑通第一局
yuzu Switch 模拟器快速上手:从放密钥到跑通第一局 yuzu 是一款开源的任天堂 Switch 模拟器,用 C++ 编写,官方维护 Windows、L
虚拟化桌面应用图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考