browser.mjs (4559B)
1 import assert from 'node:assert/strict'; 2 import {spawn} from 'node:child_process'; 3 import {createServer} from 'node:http'; 4 import {readFile,mkdtemp,rm,writeFile} from 'node:fs/promises'; 5 import {tmpdir} from 'node:os'; 6 import {resolve,extname,sep} from 'node:path'; 7 const root=resolve('.'); 8 const server=createServer(async(req,res)=>{ 9 try { 10 const path=new URL(req.url,'http://localhost').pathname; 11 if(path==='/'){res.setHeader('Content-Type','text/html');res.end('<!doctype html><title>WASM experiment</title>');return;} 12 const file=path==='/arboricx.wasm'?process.argv[2]:resolve(root,'.'+path); 13 if(path!=='/arboricx.wasm'&&!file.startsWith(root+sep))throw new Error('outside root'); 14 res.setHeader('Content-Type',({'.mjs':'text/javascript','.js':'text/javascript','.wasm':'application/wasm','.json':'application/json'})[extname(file)]??'application/octet-stream'); 15 res.end(await readFile(file)); 16 } catch {res.writeHead(404);res.end();} 17 }); 18 await new Promise(r=>server.listen(0,'127.0.0.1',r)); 19 const url=`http://127.0.0.1:${server.address().port}/`; 20 const profile = await mkdtemp(resolve(tmpdir(), 'tricu-demo-chromium-')); 21 let browser, socket; 22 let next = 0; const pending = new Map(); 23 try { 24 browser = spawn(process.argv[3] || 'chromium', ['--headless', '--no-sandbox', '--disable-dev-shm-usage', 25 '--js-flags=--max-old-space-size=768', '--remote-debugging-port=0', `--user-data-dir=${profile}`, 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] }); 26 const debuggerURL = await new Promise((resolve, reject) => { 27 let stderr = ''; 28 const timer = setTimeout(() => reject(new Error('Chromium startup timed out: ' + stderr)), 15_000); 29 browser.on('error', error => { clearTimeout(timer); reject(error); }); 30 browser.on('exit', code => { clearTimeout(timer); reject(new Error('Chromium exited: ' + code + '\n' + stderr)); }); 31 browser.stderr.on('data', chunk => { 32 stderr += chunk; 33 const match = stderr.match(/DevTools listening on (ws:\/\/\S+)/); 34 if (match) { clearTimeout(timer); resolve(match[1]); } 35 }); 36 }); 37 const endpoint = new URL(debuggerURL); 38 const tab = await (await fetch(`http://${endpoint.host}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' })).json(); 39 socket = new WebSocket(tab.webSocketDebuggerUrl); 40 await new Promise((resolve, reject) => { socket.onopen = resolve; socket.onerror = reject; }); 41 const exceptions = []; 42 socket.onmessage = ({ data }) => { 43 const event = JSON.parse(data); 44 if (event.id) { 45 const waiter = pending.get(event.id); pending.delete(event.id); 46 if (!waiter) return; 47 clearTimeout(waiter.timer); 48 if (event.error) waiter.reject(new Error(JSON.stringify(event.error))); 49 else waiter.resolve(event.result); 50 } else if (event.method === 'Runtime.exceptionThrown') exceptions.push(event.params); 51 }; 52 const send = (method, params = {}) => new Promise((resolve, reject) => { 53 const id = ++next; 54 const timer = setTimeout(() => { pending.delete(id); reject(new Error('CDP timed out: ' + method)); }, 180_000); 55 pending.set(id, { resolve, reject, timer }); 56 socket.send(JSON.stringify({ id, method, params })); 57 }); 58 await send('Runtime.enable'); await send('Page.enable'); 59 const evaluate = async expression => { 60 const result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); 61 if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails)); 62 return result.result.value; 63 }; 64 const report=await evaluate("import('/ext/wasm/browser-page.mjs').then(m=>m.run())"); 65 assert.equal(report.samples.length,24); 66 assert.deepEqual(exceptions,[]); 67 await writeFile(process.argv[4],JSON.stringify(report,null,2)+'\n'); 68 for(const index of [0,1,2,3]) { 69 const median=host=>report.samples.filter(s=>s.host===host&&s.case===index).map(s=>s.compileMs).sort((a,b)=>a-b)[1]; 70 console.log(`browser compiler-${index}: JS ${median('js').toFixed(1)} ms; WASM ${median('wasm').toFixed(1)} ms`); 71 } 72 console.log(report.controls.join('\n')); 73 } finally { 74 for (const waiter of pending.values()) { clearTimeout(waiter.timer); waiter.reject(new Error('Browser test closed')); } 75 pending.clear(); socket?.close(); 76 if (browser && browser.exitCode === null) { 77 const exited = new Promise(resolve => browser.once('exit', resolve)); 78 browser.kill('SIGKILL'); await exited; 79 } 80 await new Promise(resolve => server.close(resolve)); 81 await rm(profile, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); 82 }