ops: finalize production domain and hardware benchmark tooling
Add /version.json build identity, deploy 16e7930 release markers,
DNS/Named Tunnel prep docs, portable browser benchmark, and replay
stability probe. Keep Quick Tunnel until permanent domain cutover.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
233
scripts/portable-browser-benchmark.mjs
Normal file
233
scripts/portable-browser-benchmark.mjs
Normal file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Portable headed browser benchmark for the presentation laptop.
|
||||
* Uses the system Chrome/Chromium channel when available — no forced GPU flags.
|
||||
*
|
||||
* npm run perf:browser
|
||||
* npm run perf:browser:export
|
||||
*
|
||||
* Env: PLAYWRIGHT_BASE_URL (default http://127.0.0.1:3100)
|
||||
*/
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = resolve(__dirname, '..');
|
||||
const MODES = ['low', 'medium', 'high', 'demo'];
|
||||
const exportOnly = process.argv.includes('--export') || process.env.PERF_EXPORT === '1';
|
||||
|
||||
function baseUrl() {
|
||||
const raw = process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:3100';
|
||||
return new URL(raw.includes('://') ? raw : `http://${raw}`);
|
||||
}
|
||||
|
||||
function urlFor(mode) {
|
||||
const u = baseUrl();
|
||||
u.searchParams.set('perf', '1');
|
||||
u.searchParams.set('quality', mode);
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
function isSoft(renderer = '') {
|
||||
return /swiftshader|llvmpipe|softpipe|software|mesa offscreen|microsoft basic render/i.test(
|
||||
renderer,
|
||||
);
|
||||
}
|
||||
|
||||
async function sample(page) {
|
||||
return (
|
||||
(await page.evaluate(() => {
|
||||
const s = window.__PERF_SNAPSHOT__;
|
||||
return s ? { ...s } : null;
|
||||
})) || {
|
||||
mode: 'demo',
|
||||
renderer: 'unknown',
|
||||
averageFps: 0,
|
||||
minimumFps: 0,
|
||||
p95FrameTimeMs: 0,
|
||||
p99FrameTimeMs: 0,
|
||||
longFramesOver33: 0,
|
||||
longFramesOver50: 0,
|
||||
drawCalls: 0,
|
||||
triangles: 0,
|
||||
geometries: 0,
|
||||
textures: 0,
|
||||
programs: 0,
|
||||
heapMb: 0,
|
||||
dpr: 1,
|
||||
shadows: false,
|
||||
antialias: false,
|
||||
hardwareAccelerated: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function measureMode(browser, mode, notes) {
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
try {
|
||||
await page.goto(urlFor(mode), { waitUntil: 'domcontentloaded', timeout: 90_000 });
|
||||
await page.waitForSelector('canvas', { timeout: 60_000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => window.__PERF_RESET__?.());
|
||||
await page.waitForTimeout(2500);
|
||||
const idle = await sample(page);
|
||||
|
||||
await page.evaluate(() => window.__PERF_RESET__?.());
|
||||
await page.getByTestId('demo-play').click({ timeout: 8_000 }).catch(() => notes.push(`${mode}: play skipped`));
|
||||
await page.waitForTimeout(5000);
|
||||
const play = await sample(page);
|
||||
|
||||
// Full-ish playlist: jump through cases quickly
|
||||
for (const idx of [0, 2, 4, 8, 9]) {
|
||||
await page.getByTestId(`demo-case-${idx}`).click().catch(() => undefined);
|
||||
await page.waitForTimeout(900);
|
||||
}
|
||||
const afterPlaylist = await sample(page);
|
||||
|
||||
// Three replays (Replay Demo overlay or seek case 0 + play)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const finished = page.getByTestId('demo-finished');
|
||||
if (await finished.isVisible().catch(() => false)) {
|
||||
await finished.getByRole('button', { name: /Replay/i }).click().catch(() => undefined);
|
||||
} else {
|
||||
await page.getByTestId('demo-case-0').click().catch(() => undefined);
|
||||
await page.getByTestId('demo-play').click().catch(() => undefined);
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
const afterReplay = await sample(page);
|
||||
|
||||
const meta = await page.evaluate(() => ({
|
||||
userAgent: navigator.userAgent,
|
||||
viewport: { width: window.innerWidth, height: window.innerHeight },
|
||||
webgl: (() => {
|
||||
const c = document.createElement('canvas');
|
||||
const gl = c.getContext('webgl2') || c.getContext('webgl');
|
||||
if (!gl) return { ok: false };
|
||||
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
return {
|
||||
ok: true,
|
||||
vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : null,
|
||||
renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),
|
||||
};
|
||||
})(),
|
||||
}));
|
||||
|
||||
return {
|
||||
mode,
|
||||
idle,
|
||||
play,
|
||||
afterPlaylist,
|
||||
afterReplay,
|
||||
meta,
|
||||
software: isSoft(play.renderer || meta.webgl?.renderer || ''),
|
||||
};
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
function toMarkdown(report) {
|
||||
const rows = report.modes
|
||||
.map((m) => {
|
||||
const s = m.play;
|
||||
return `| ${m.mode} | ${s.renderer} | ${s.averageFps} | ${s.minimumFps} | ${s.p95FrameTimeMs} | ${s.drawCalls} | ${s.triangles} | ${s.heapMb} |`;
|
||||
})
|
||||
.join('\n');
|
||||
const soft = report.softwareRendererDetected;
|
||||
return `# HARDWARE_BROWSER_BENCHMARK
|
||||
|
||||
Measured: ${report.measuredAt}
|
||||
Commit probe URL: ${report.baseUrl}
|
||||
Browser: ${report.browserVersion}
|
||||
Headed: yes (no forced GPU flags)
|
||||
Software renderer: **${soft ? 'YES' : 'no'}**
|
||||
|
||||
## Mode table (after Play)
|
||||
|
||||
| Mode | Renderer | Avg FPS | Min FPS | p95 | Calls | Triangles | Heap |
|
||||
| ---- | -------- | ------: | ------: | --: | ----: | --------: | ---: |
|
||||
${rows}
|
||||
|
||||
## Notes
|
||||
|
||||
${report.notes.map((n) => `- ${n}`).join('\n')}
|
||||
|
||||
Raw JSON: \`agent/reports/browser-benchmark.json\`
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const notes = [];
|
||||
if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
||||
notes.push(
|
||||
'No DISPLAY — attempting headed launch may fail. Run on the presentation laptop with a real display.',
|
||||
);
|
||||
}
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: false,
|
||||
channel: process.env.PERF_CHROME_CHANNEL || undefined,
|
||||
// Intentionally NO GPU override flags — measure the machine as the jury sees it.
|
||||
});
|
||||
} catch (err) {
|
||||
notes.push(`channel launch failed (${err.message}); falling back to bundled Chromium headed`);
|
||||
browser = await chromium.launch({ headless: false });
|
||||
}
|
||||
|
||||
const modes = [];
|
||||
try {
|
||||
for (const mode of MODES) {
|
||||
modes.push(await measureMode(browser, mode, notes));
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
const commit = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
}).stdout.trim();
|
||||
|
||||
const report = {
|
||||
measuredAt: new Date().toISOString(),
|
||||
commit,
|
||||
baseUrl: baseUrl().toString(),
|
||||
browserVersion: modes[0]?.meta?.userAgent ?? 'unknown',
|
||||
softwareRendererDetected: modes.some((m) => m.software),
|
||||
modes,
|
||||
notes,
|
||||
};
|
||||
|
||||
mkdirSync(join(ROOT, 'agent', 'reports'), { recursive: true });
|
||||
const jsonPath = join(ROOT, 'agent', 'reports', 'browser-benchmark.json');
|
||||
const mdPath = join(ROOT, 'docs', 'HARDWARE_BROWSER_BENCHMARK_LATEST.md');
|
||||
writeFileSync(jsonPath, JSON.stringify(report, null, 2));
|
||||
writeFileSync(mdPath, toMarkdown(report));
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
exportOnly,
|
||||
softwareRendererDetected: report.softwareRendererDetected,
|
||||
demoFps: modes.find((m) => m.mode === 'demo')?.play.averageFps,
|
||||
renderer: modes.find((m) => m.mode === 'demo')?.play.renderer,
|
||||
jsonPath,
|
||||
mdPath,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user