fix: stabilize public demo and align sorter twins
Document arhipovdan.ru DNS/TLS blocker, add production smoke and GPU perf tooling, unify twin layout, and expand visual regression coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
302
scripts/headed-gpu-baseline.mjs
Normal file
302
scripts/headed-gpu-baseline.mjs
Normal file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Headed / hardware-preferring GPU baseline.
|
||||
* Samples window.__PERF_SNAPSHOT__ for quality modes low|medium|high|demo.
|
||||
*
|
||||
* Usage: npm run perf:gpu
|
||||
* Env:
|
||||
* PLAYWRIGHT_BASE_URL default http://127.0.0.1:3100
|
||||
* PERF_USE_XVFB=0 skip auto xvfb when DISPLAY unset
|
||||
*/
|
||||
import { mkdirSync, writeFileSync, existsSync } 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 GPU_ARGS = [
|
||||
'--use-gl=angle',
|
||||
'--use-angle=gl-egl',
|
||||
'--enable-webgl',
|
||||
'--enable-webgl2',
|
||||
'--ignore-gpu-blocklist',
|
||||
'--enable-gpu-rasterization',
|
||||
'--enable-precise-memory-info',
|
||||
'--enable-unsafe-swiftshader', // fallback if EGL/NVIDIA path unavailable in CI/coder
|
||||
];
|
||||
|
||||
function resolveBaseUrl() {
|
||||
const raw = process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:3100';
|
||||
return new URL(raw.includes('://') ? raw : `http://${raw}`);
|
||||
}
|
||||
|
||||
function urlForMode(mode) {
|
||||
const u = resolveBaseUrl();
|
||||
u.searchParams.set('perf', '1');
|
||||
u.searchParams.set('quality', mode);
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
function isSoftwareRenderer(renderer = '') {
|
||||
return /swiftshader|llvmpipe|softpipe|software|mesa offscreen|microsoft basic render/i.test(
|
||||
renderer,
|
||||
);
|
||||
}
|
||||
|
||||
function emptySnap(mode = 'demo') {
|
||||
return {
|
||||
mode,
|
||||
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: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function sampleSnapshot(page) {
|
||||
const snap = await page.evaluate(() => {
|
||||
const s = window.__PERF_SNAPSHOT__;
|
||||
return s ? { ...s } : null;
|
||||
});
|
||||
return snap ?? emptySnap();
|
||||
}
|
||||
|
||||
async function measureMode(browser, mode, notes) {
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
const url = urlForMode(mode);
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.waitForSelector('canvas', { timeout: 45_000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => window.__PERF_RESET__?.());
|
||||
await page.waitForTimeout(2500);
|
||||
const idle = await sampleSnapshot(page);
|
||||
|
||||
await page.evaluate(() => window.__PERF_RESET__?.());
|
||||
await page
|
||||
.getByTestId('demo-play')
|
||||
.click({ timeout: 8_000 })
|
||||
.catch(() => notes.push(`${mode}: play click skipped`));
|
||||
await page.waitForTimeout(4500);
|
||||
const play = await sampleSnapshot(page);
|
||||
|
||||
// memory stability probe: seek + replay light
|
||||
const heapBefore = play.heapMb;
|
||||
const geoBefore = play.geometries;
|
||||
const texBefore = play.textures;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await page.getByTestId('demo-case-0').click().catch(() => undefined);
|
||||
await page.waitForTimeout(400);
|
||||
await page.getByTestId('demo-play').click().catch(() => undefined);
|
||||
await page.waitForTimeout(1200);
|
||||
}
|
||||
const afterReplay = await sampleSnapshot(page);
|
||||
|
||||
return {
|
||||
mode,
|
||||
url,
|
||||
idle,
|
||||
play,
|
||||
afterReplay,
|
||||
heapDeltaMb: Number((afterReplay.heapMb - heapBefore).toFixed(2)),
|
||||
geometriesDelta: afterReplay.geometries - geoBefore,
|
||||
texturesDelta: afterReplay.textures - texBefore,
|
||||
software: isSoftwareRenderer(play.renderer || idle.renderer),
|
||||
};
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
function renderMarkdown(report) {
|
||||
const soft = report.softwareRendererDetected;
|
||||
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 demo = report.modes.find((m) => m.mode === 'demo')?.play ?? emptySnap();
|
||||
let verdict = 'PARTIAL — software/headless diagnostic only';
|
||||
if (!soft && demo.averageFps >= 55 && demo.minimumFps >= 40 && demo.p95FrameTimeMs <= 20) {
|
||||
verdict = 'PASS — Excellent Demo GPU';
|
||||
} else if (!soft && demo.averageFps >= 30 && demo.minimumFps >= 24 && demo.p95FrameTimeMs <= 33) {
|
||||
verdict = 'PASS — Acceptable Demo GPU';
|
||||
} else if (!soft && (demo.averageFps < 30 || demo.p95FrameTimeMs > 40)) {
|
||||
verdict = 'FAIL — Demo GPU below live demo bar';
|
||||
} else if (soft) {
|
||||
verdict = 'NOT A GPU BASELINE — software renderer (SwiftShader/llvmpipe/etc.)';
|
||||
}
|
||||
|
||||
return `# HEADED_GPU_PERFORMANCE_REPORT
|
||||
|
||||
Measured: ${report.measuredAt}
|
||||
Commit: \`${report.commit}\`
|
||||
Base URL: ${report.baseUrl}
|
||||
Display: ${report.displayNote}
|
||||
Software renderer detected: **${soft ? 'YES' : 'no'}**
|
||||
Verdict: **${verdict}**
|
||||
|
||||
## Notes
|
||||
|
||||
${report.notes.map((n) => `- ${n}`).join('\n')}
|
||||
|
||||
## Mode table (after Play)
|
||||
|
||||
| Mode | Renderer | Avg FPS | Min FPS | p95 | Calls | Triangles | Heap |
|
||||
| ---- | -------- | ------: | ------: | --: | ----: | --------: | ---: |
|
||||
${rows}
|
||||
|
||||
## Memory after 3 seek+replay (demo mode)
|
||||
|
||||
\`\`\`
|
||||
${JSON.stringify(
|
||||
report.modes.find((m) => m.mode === 'demo')
|
||||
? {
|
||||
heapDeltaMb: report.modes.find((m) => m.mode === 'demo').heapDeltaMb,
|
||||
geometriesDelta: report.modes.find((m) => m.mode === 'demo').geometriesDelta,
|
||||
texturesDelta: report.modes.find((m) => m.mode === 'demo').texturesDelta,
|
||||
}
|
||||
: {},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
\`\`\`
|
||||
|
||||
## Prior software/headless WebGL diagnostic baseline (reference only)
|
||||
|
||||
\`\`\`
|
||||
Average FPS: 9.6
|
||||
Minimum FPS: 3.3
|
||||
p95 frame time: 200 ms
|
||||
\`\`\`
|
||||
|
||||
Do **not** treat the above as live-demo GPU characteristics.
|
||||
|
||||
## Chromium launch args
|
||||
|
||||
\`\`\`
|
||||
${report.launchArgs.join(' ')}
|
||||
\`\`\`
|
||||
|
||||
Raw JSON: \`agent/reports/gpu-baseline.json\`
|
||||
`;
|
||||
}
|
||||
|
||||
async function runMeasurement() {
|
||||
const notes = [];
|
||||
const hasDisplay = Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
||||
let displayNote = hasDisplay
|
||||
? `headed (${process.env.DISPLAY || process.env.WAYLAND_DISPLAY})`
|
||||
: 'no DISPLAY — Chromium headless';
|
||||
|
||||
if (!hasDisplay) {
|
||||
notes.push(
|
||||
'No DISPLAY/WAYLAND_DISPLAY. Prefer xvfb-run wrapper (auto) for a virtual headed session.',
|
||||
);
|
||||
} else {
|
||||
notes.push('Headed Chromium with GPU-preferring flags.');
|
||||
}
|
||||
notes.push('PerfCollector only with ?perf=1; quality forced via ?quality=.');
|
||||
notes.push('Prior headless ~9.6 FPS is labeled software/headless WebGL diagnostic baseline.');
|
||||
|
||||
const launchArgs = [...GPU_ARGS];
|
||||
const browser = await chromium.launch({
|
||||
headless: !hasDisplay,
|
||||
args: launchArgs,
|
||||
});
|
||||
|
||||
const modes = [];
|
||||
try {
|
||||
for (const mode of MODES) {
|
||||
modes.push(await measureMode(browser, mode, notes));
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
const softwareRendererDetected = modes.some((m) => m.software);
|
||||
const commit = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
}).stdout.trim();
|
||||
|
||||
const report = {
|
||||
measuredAt: new Date().toISOString(),
|
||||
commit,
|
||||
baseUrl: resolveBaseUrl().toString(),
|
||||
headed: hasDisplay,
|
||||
displayNote,
|
||||
launchArgs,
|
||||
softwareRendererDetected,
|
||||
modes,
|
||||
notes,
|
||||
};
|
||||
|
||||
mkdirSync(join(ROOT, 'agent', 'reports'), { recursive: true });
|
||||
writeFileSync(join(ROOT, 'agent', 'reports', 'gpu-baseline.json'), JSON.stringify(report, null, 2));
|
||||
writeFileSync(join(ROOT, 'docs', 'HEADED_GPU_PERFORMANCE_REPORT.md'), renderMarkdown(report));
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
headed: hasDisplay,
|
||||
softwareRendererDetected,
|
||||
demoFps: modes.find((m) => m.mode === 'demo')?.play.averageFps,
|
||||
renderer: modes.find((m) => m.mode === 'demo')?.play.renderer,
|
||||
reports: ['docs/HEADED_GPU_PERFORMANCE_REPORT.md', 'agent/reports/gpu-baseline.json'],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const hasDisplay = Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
||||
const allowXvfb = process.env.PERF_USE_XVFB !== '0';
|
||||
const xvfb = existsSync('/usr/bin/xvfb-run');
|
||||
|
||||
// Re-exec under xvfb so Chromium gets a virtual DISPLAY (better chance at real GL).
|
||||
if (!hasDisplay && allowXvfb && xvfb && !process.env.PERF_UNDER_XVFB) {
|
||||
console.log('Re-launching under xvfb-run for virtual headed session…');
|
||||
const env = { ...process.env, PERF_UNDER_XVFB: '1' };
|
||||
delete env.DISPLAY; // let xvfb-run assign the real display
|
||||
const r = spawnSync(
|
||||
'xvfb-run',
|
||||
['-a', '-s', '-screen 0 1440x900x24', 'node', fileURLToPath(import.meta.url)],
|
||||
{
|
||||
cwd: ROOT,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
process.exit(r.status ?? 1);
|
||||
}
|
||||
|
||||
await runMeasurement();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user