tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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 }