#!/usr/bin/env node /** * tools/perf-smoke.mjs — Perceived-load smoke test for hmwebviewer (PLAN P5-3). * * Launches headless Chrome via puppeteer-core (uses the system Chrome; no * Chromium download). For each sample asset it loads `?model=` and * measures wall time from navigation-start to the viewer's `hmw:ready` event * (dispatched in ThreeDViewer.onLoaded). Asserts < 3000ms perceived. * * USAGE * node tools/perf-smoke.mjs [--url http://127.0.0.1:4173] [--asset /samples/Box.glb ...] * * Defaults: url = http://127.0.0.1:4173 (vite preview); assets = the sample set. * Run `npm run build && npm run preview` first (or `npm run dev` on its port). * * Browser auto-detected: Chrome, then Edge, then PUPPETEER_EXECUTABLE_PATH. */ import puppeteer from 'puppeteer-core'; import { existsSync } from 'node:fs'; const CHROME_CANDIDATES = [ 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', ]; function findBrowser() { if (process.env.PUPPETEER_EXECUTABLE_PATH && existsSync(process.env.PUPPETEER_EXECUTABLE_PATH)) { return process.env.PUPPETEER_EXECUTABLE_PATH; } for (const p of CHROME_CANDIDATES) if (existsSync(p)) return p; return null; } function parseArgs(argv) { const args = argv.slice(2); let url = 'http://127.0.0.1:4173'; const assets = []; for (let i = 0; i < args.length; i++) { if (args[i] === '--url') url = args[++i]; else if (args[i] === '--asset') assets.push(args[++i]); else if (args[i] === '-h' || args[i] === '--help') return { help: true }; } if (assets.length === 0) { assets.push('/samples/Box.glb', '/samples/Duck.glb', '/samples/Duck.optimized.glb', '/samples/Avocado.glb'); } return { url, assets }; } const THRESHOLD_MS = 3000; async function measureAsset(browser, baseUrl, asset) { const page = await browser.newPage(); await page.evaluateOnNewDocument(() => { window.__hmwReady = null; window.addEventListener('hmw:ready', (e) => { window.__hmwReady = e.detail; }); }); const target = baseUrl + '?model=' + asset; const t0 = Date.now(); try { await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForFunction('window.__hmwReady != null', { timeout: 30000 }); const data = await page.evaluate(() => ({ readyPerf: window.__hmwReady, origin: performance.timeOrigin })); const ms = (data.origin + data.readyPerf) - t0; return { asset, ms: Math.round(ms), ok: ms < THRESHOLD_MS }; } catch (e) { return { asset, ms: null, ok: false, error: (e && e.message) ? e.message : String(e) }; } finally { await page.close(); } } async function main() { const opts = parseArgs(process.argv); if (opts.help) { console.error('Usage: node tools/perf-smoke.mjs [--url URL] [--asset PATH ...]'); process.exit(0); } const exe = findBrowser(); if (!exe) { console.error('[perf-smoke] No Chrome/Edge found. Set PUPPETEER_EXECUTABLE_PATH.'); process.exit(1); } const browser = await puppeteer.launch({ executablePath: exe, headless: 'new', args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'], }); console.log('[perf-smoke] base=' + opts.url + ' browser=' + exe); console.log('[perf-smoke] threshold=' + THRESHOLD_MS + 'ms perceived'); const results = []; for (const asset of opts.assets) { process.stdout.write(' ' + asset + ' ... '); const r = await measureAsset(browser, opts.url, asset); results.push(r); console.log(r.ms === null ? 'FAIL (' + (r.error || 'timeout') + ')' : (r.ms + 'ms ' + (r.ok ? 'OK' : 'OVER'))); } await browser.close(); console.log('\n[perf-smoke] summary'); for (const r of results) { console.log(' ' + r.asset.padEnd(34) + (r.ms === null ? 'FAIL' : (r.ms + 'ms').padEnd(8)) + (r.ok ? 'PASS' : 'FAIL')); } const failed = results.filter((r) => !r.ok); if (failed.length) { console.error('[perf-smoke] ' + failed.length + ' asset(s) over threshold/failed.'); process.exit(1); } console.log('[perf-smoke] all within ' + THRESHOLD_MS + 'ms.'); } main().catch((e) => { console.error('[perf-smoke] Fatal: ' + (e && e.stack ? e.stack : e)); process.exit(1); });