gpu-perf-agent
v0.1.0
Published
Agent-ready WebGPU/WebGL2 performance profiler: Node-callable benchmark, trace, and memory reports with verdicts for automated optimization loops. Ships an agent skill.
Maintainers
Readme
gpu-perf-agent
An agent-ready WebGPU/WebGL2 performance profiler. It ships as an npm CLI plus a drop-in agent skill, so AI coding assistants (Claude, Gemini, Codex, Copilot, Cursor, ...) can profile a page, read a one-word verdict, apply an optimization, and prove the win with a regression-checked compare — autonomously.
Humans and CI get the same thing: repeatable performance/memory artifacts for tuning browser GPU workloads.
The default runner is a fast direct Chrome DevTools Protocol harness. It launches Chrome/Chromium itself and talks CDP over WebSocket, avoiding Playwright's context and driver layers. A Playwright runner remains exported as runPlaywrightReport() for compatibility.
Give the Skill to Your Agent
The skill definition lives in skill/SKILL.md and is included in the npm tarball (node_modules/gpu-perf-agent/skill/SKILL.md). Install the package, then drop the skill where your agent discovers skills:
npm install -D gpu-perf-agentClaude Code — project-scoped (recommended, travels with the repo):
mkdir -p .claude/skills/webgpu-performance-profiling
cp node_modules/gpu-perf-agent/skill/SKILL.md .claude/skills/webgpu-performance-profiling/SKILL.mdOr user-wide with ~/.claude/skills/webgpu-performance-profiling/.
OpenAI Codex CLI:
mkdir -p ~/.codex/skills/webgpu-performance-profiling
cp node_modules/gpu-perf-agent/skill/SKILL.md ~/.codex/skills/webgpu-performance-profiling/SKILL.mdOr reference the skill from your AGENTS.md: "For GPU performance profiling, follow node_modules/gpu-perf-agent/skill/SKILL.md".
Gemini CLI / Antigravity:
mkdir -p ~/.gemini/skills/webgpu-performance-profiling
cp node_modules/gpu-perf-agent/skill/SKILL.md ~/.gemini/skills/webgpu-performance-profiling/SKILL.mdOr reference it from GEMINI.md the same way as AGENTS.md.
GitHub Copilot (VS Code) — project-scoped:
mkdir -p .github/skills/webgpu-performance-profiling
cp node_modules/gpu-perf-agent/skill/SKILL.md .github/skills/webgpu-performance-profiling/SKILL.mdCline / Roo Code / Cursor — paste the contents of skill/SKILL.md into your rules file (.clinerules, .cursorrules, or custom instructions).
Once installed, ask your agent things like "profile http://localhost:5173 and fix the biggest GPU bottleneck" — the skill teaches it the full baseline → optimize → compare loop.
What It Captures
- Fast direct-CDP benchmark samples through Chrome/Chromium.
- Optional Playwright runner for compatibility.
- Optional GPU timestamp helpers for precise WebGPU/WebGL2 command timing.
- WebGPU/WebGL2 capability metadata.
- JS heap and browser CDP metrics.
- Optional Chrome trace summaries for GPU, frame, and memory-infra events.
- Optional declared GPU allocation tracking for buffers/textures created by your app.
- Optional macOS
xctracecapture using Xcode Instruments templates such asMetal System Trace.
Important limitation: the web platform does not expose exact VRAM residency for WebGPU/WebGL2. For deep memory analysis, use all three layers together: declared allocation tracking in the app, Chrome trace memory-infra summaries, and native Instruments captures on macOS.
Install
From npm:
npm install -D gpu-perf-agent
npx gpu-perf-agent doctorOr run one-off without installing:
npx gpu-perf-agent doctorFrom a checkout of this repository:
npm install
npm run doctorThe fast runner prefers an installed Chrome/Chromium. If it cannot find one, it falls back to Playwright's bundled Chromium path when available. Playwright itself is an optional peer dependency — only needed for the compatibility runner or its bundled browser:
npm install playwright
npx playwright install chromiumCLI
Run a report against a local file:
node ./src/cli.js run --file examples/webgl2-draw.html --samples 5 --duration-ms 1000 --out reports/webgl2.json--file targets are served through a temporary localhost server, so module imports and secure-context APIs behave like a normal local app. Use --file-root DIR when the benchmark imports files outside the benchmark directory.
Run a report against an app URL and include a Chrome trace summary:
node ./src/cli.js run --url http://localhost:5173/bench.html --samples 10 --duration-ms 2000 --trace --out reports/candidate.jsonSave the raw Chrome trace too:
node ./src/cli.js run --url http://localhost:5173/bench.html --trace --raw-trace --out reports/candidate.jsonFor agent loops, launch Chrome once and send repeated jobs to the local server:
node ./src/cli.js serve --port 9099Then call it from any Node tool:
const response = await fetch("http://127.0.0.1:9099/run", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
url: "http://localhost:5173/bench.html",
samples: 5,
durationMs: 1000
})
});
const report = await response.json();Compare two reports and fail with exit code 1 when a metric regresses beyond the threshold:
node ./src/cli.js compare --base reports/base.json --candidate reports/candidate.json --threshold 5Both run and compare accept --json for machine-readable stdout — ideal for agents and CI scripts.
Record an Xcode Instruments trace on macOS:
node ./src/cli.js xctrace --url http://localhost:5173/bench.html --template "Metal System Trace" --time-limit 15s --out reports/metal.traceReport Structure
Every report (file, --json stdout, serve responses, and Node API results) contains a top-level agent-friendly digest:
{
"summary": {
"verdict": "excellent", // excellent | good | needs-work | poor
"fps": 60,
"frameTimeMsMean": 16.4,
"frameTimeMsP95": 17.1,
"gpuFrameMsMean": 4.2, // when GPU timing hooks are active
"jsHeapGrowthMBPerSec": 0.02,
"trackedVram": { "totalMiB": 128.5, "textureMiB": 96.2, "bufferMiB": 32.3, "textureCount": 14, "bufferCount": 42, "leakedResources": 0 },
"slowFrameCount": 0,
"warnings": [ /* actionable bottleneck descriptions */ ]
},
"diagnostics": { "warnings": [], "highlights": [], "resources": {} },
"inPage": { /* per-sample measurements, slow-frame op counts */ },
"trace": { /* Chrome trace summary when --trace */ },
Record an Xcode Instruments trace on macOS:
```bash
node ./src/cli.js xctrace --url http://localhost:5173/bench.html --template "Metal System Trace" --time-limit 15s --out reports/metal.traceNode API
import { runReport, compareReports } from "gpu-perf-agent";
const report = await runReport({
url: "http://localhost:5173/bench.html",
samples: 10,
durationMs: 1000,
trace: true
});For repeated runs inside one Node process, keep Chrome alive:
import { FastCDPHarness } from "gpu-perf-agent";
const harness = await FastCDPHarness.launch({ channel: "chrome" });
try {
const before = await harness.run({ url: "http://localhost:5173/bench.html" });
const after = await harness.run({ url: "http://localhost:5173/bench.html" });
} finally {
await harness.close();
}Page Hook
For meaningful GPU work, expose a hook in the page. The runner will call it for warmup and sample phases.
globalThis.__gpuReportBench = async ({ phase, sampleIndex, durationMs, api }) => {
// Run your workload for roughly durationMs.
return {
api,
fps: 60,
gpuMs: 4.2,
allocatedBytes: globalThis.__gpuMemoryTracker?.snapshot().totalBytes
};
};Without the hook, the runner still records capability, memory, and requestAnimationFrame timing, but it cannot know which GPU workload you intended to measure.
Declared GPU Allocation Tracking
Use the browser helper to track resources your app creates. This is not exact VRAM residency; it is a deterministic estimate from API descriptors, which is ideal for regression checks.
import { trackWebGPUDevice } from "gpu-perf-agent/browser/allocation-tracker";
const device = await adapter.requestDevice();
globalThis.__gpuMemoryTracker = trackWebGPUDevice(device);For WebGL2:
import { trackWebGL2Context } from "gpu-perf-agent/browser/allocation-tracker";
const gl = canvas.getContext("webgl2");
globalThis.__gpuMemoryTracker = trackWebGL2Context(gl);Precise GPU Timing
For precise GPU command timing, instrument the benchmark page. WebGPU timestamp query results are nanoseconds, but the exact timestamp source is implementation-defined by the browser/GPU stack. The runner records these as custom metrics, so compare can gate on them.
import { requestTimedWebGPUDevice } from "gpu-perf-agent/browser/timing";
const adapter = await navigator.gpu.requestAdapter();
const { device, timer } = await requestTimedWebGPUDevice(adapter);
const gpuTimeNs = await timer.measure((encoder, gpuTimer) => {
const pass = gpuTimer.beginComputePass(encoder);
pass.dispatchWorkgroups(128);
pass.end();
});For WebGL2, use EXT_disjoint_timer_query_webgl2 when available:
import { createWebGL2Timer } from "gpu-perf-agent/browser/timing";
const timer = createWebGL2Timer(gl);
const gpuTimeNs = timer ? await timer.measure(() => draw()) : null;Suggested Stack
- Use the allocation tracker in benchmarks to catch declared buffer/texture growth.
- Use
gpu-perf-agent run --tracefor repeatable browser traces and CDP metrics. - Use
gpu-perf-agent comparein CI or agent loops to catch regressions. - Use
gpu-perf-agent xctracewhen Chrome-level signals are not enough and you need Metal/GPU-driver level evidence on macOS.
License
MIT
