// Drive the bench_fluidity.html harness in a headed Chrome via the DevTools // Protocol (Node 22 global WebSocket + fetch — no npm deps). Chrome must be // running with --remote-debugging-port=9333 and the no-throttle flags so the // page keeps rendering at full GPU speed even while occluded. const PORT = process.env.CDP_PORT || 9333; const URLMATCH = 'bench_fluidity.html'; const DURATION = +(process.env.DUR || 2500); const REPS = +(process.env.REPS || 3); const sleep = (ms) => new Promise(r => setTimeout(r, ms)); async function findTarget() { const wantAny = !!process.env.NAV || process.env.ANYPAGE === '1'; for (let i = 0; i < 60; i++) { try { const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json(); const t = wantAny ? list.find(x => x.type === 'page' && /^https?:|^about:|chrome:\/\/newtab/.test(x.url)) : list.find(x => x.type === 'page' && x.url.includes(URLMATCH)); if (t && t.webSocketDebuggerUrl) return t; } catch { /* port not up yet */ } await sleep(500); } throw new Error('no page target found (wantAny=' + wantAny + ')'); } function connect(wsUrl) { return new Promise((resolve, reject) => { const ws = new WebSocket(wsUrl); let id = 0; const pending = new Map(); ws.addEventListener('message', (ev) => { const msg = JSON.parse(ev.data); if (msg.id && pending.has(msg.id)) { const { resolve: r, reject: j } = pending.get(msg.id); pending.delete(msg.id); if (msg.error) j(new Error(JSON.stringify(msg.error))); else r(msg.result); } }); ws.addEventListener('error', (e) => reject(new Error('ws error ' + (e.message || '')))); ws.addEventListener('open', () => { const send = (method, params = {}) => new Promise((r, j) => { const mid = ++id; pending.set(mid, { resolve: r, reject: j }); ws.send(JSON.stringify({ id: mid, method, params })); }); const evalJs = async (expr, awaitPromise = false) => { const res = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise }); if (res.exceptionDetails) throw new Error('eval: ' + JSON.stringify(res.exceptionDetails)); return res.result.value; }; resolve({ send, evalJs, ws }); }); }); } const EXPR = process.env.EXPR || `window.__runSuite(${DURATION}, ${REPS})`; const { evalJs, send } = await connect((await findTarget()).webSocketDebuggerUrl); await send('Runtime.enable'); await send('Page.enable'); if (process.env.NAV) { await send('Page.navigate', { url: process.env.NAV }); await sleep(2500); } else if (process.env.RELOAD === '1') { await send('Page.reload', { ignoreCache: true }); await sleep(1500); } // Wait for harness readiness. for (let i = 0; i < 60 && process.env.SKIPREADY !== '1'; i++) { const ready = await evalJs(`(typeof window.__runSuite === 'function' && typeof window.__runExtra === 'function' && document.getElementById('out') && document.getElementById('out').textContent.startsWith('ready'))`); if (ready) break; await sleep(500); } const env = await evalJs(`(()=>{return {visibility:document.visibilityState, dpr:window.devicePixelRatio, url:location.href};})()`); console.error('ENV', JSON.stringify(env)); if (env.visibility !== 'visible') console.error('WARNING: visibility=' + env.visibility + ' — rAF may be throttled. Anti-throttle flags should keep it running anyway.'); const result = await evalJs(EXPR, true); console.log(JSON.stringify(result)); process.exit(0);