#!/usr/bin/env node /** * tools/adaptive-smoke.mjs — empirically trigger the adaptive-quality downstep. * * Loads a heavy sample in headless Chrome (software GL = slow) + applies a CDP * CPU throttle so the REAL measured fps sustains < 60. Captures the genuine * `[hmwebviewer] adaptive quality -> tier N (pr=..)` console logs emitted by * src/viewer/adaptiveQuality.ts, plus the live #.fps overlay text. Proves the * <60fps → optimize path fires on real frame measurement (no logic patched). * * USAGE: node tools/adaptive-smoke.mjs [--url URL] [--asset PATH] [--throttle N] [--seconds S] */ 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 a = argv.slice(2); let url = 'http://127.0.0.1:4173', asset = '/samples/ABeautifulGame.ktx2.glb', throttle = 6, seconds = 25; for (let i = 0; i < a.length; i++) { if (a[i] === '--url') url = a[++i]; else if (a[i] === '--asset') asset = a[++i]; else if (a[i] === '--throttle') throttle = Number(a[++i]); else if (a[i] === '--seconds') seconds = Number(a[++i]); } return { url, asset, throttle, seconds }; } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function main() { const o = parseArgs(process.argv); const exe = findBrowser(); if (!exe) { console.error('[adaptive-smoke] No Chrome/Edge found.'); process.exit(1); } const browser = await puppeteer.launch({ executablePath: exe, headless: 'new', args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'], }); const page = await browser.newPage(); const tierLogs = []; page.on('console', (m) => { const t = m.text(); if (t.includes('adaptive quality')) { tierLogs.push(t); console.log(' LOG ' + t); } }); await page.evaluateOnNewDocument(() => { window.__hmwReady = null; window.addEventListener('hmw:ready', (e) => { window.__hmwReady = e.detail; }); }); const target = o.url + '?model=' + o.asset; console.log('[adaptive-smoke] ' + target + ' throttle=' + o.throttle + 'x watch=' + o.seconds + 's browser=' + exe); await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForFunction('window.__hmwReady != null', { timeout: 60000 }); console.log('[adaptive-smoke] model loaded; applying CPU throttle + watching fps...'); const client = await page.target().createCDPSession(); await client.send('Emulation.setCPUThrottlingRate', { rate: o.throttle }); const start = Date.now(); while (Date.now() - start < o.seconds * 1000) { await sleep(2000); const state = await page.evaluate(() => { const el = document.querySelector('.fps'); return { fps: el ? el.textContent : '(no overlay)', pr: window.__viewer ? undefined : undefined }; }); console.log(' t+' + Math.round((Date.now() - start) / 1000) + 's overlay=' + state.fps + ' tierSteps=' + tierLogs.length); if (tierLogs.length >= 3) break; // enough proof } await browser.close(); console.log('\n[adaptive-smoke] tier-change log lines captured: ' + tierLogs.length); tierLogs.forEach((l) => console.log(' ' + l)); if (tierLogs.length === 0) { console.error('[adaptive-smoke] NO downstep observed — fps stayed >=60 (raise --throttle or use a heavier asset).'); process.exit(2); } console.log('[adaptive-smoke] PASS — adaptive quality degraded on sustained <60fps.'); } main().catch((e) => { console.error('[adaptive-smoke] Fatal: ' + (e && e.stack ? e.stack : e)); process.exit(1); });