/ proof-of-concept / fmm3dbie-webgpu-poc
Sign in
proof-of-concept / fmm3dbie-webgpu-poc
fmm3dbie-webgpu-poc / scripts / smoke.mjs
95 lines · 4.4 KBCodeBlameHistory
2 * Does the app work in a browser? Serves dist/, opens index.html in headless
3 * Chrome (hardware WebGPU if there is any, SwiftShader otherwise), solves
4 * the quick case, and waits for the status line to report
5 * success. Any console error, page error or failed request fails the run. A
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 6 * screenshot goes to tmp/smoke-<problem>.png.
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 8 * Run after `vite build`: node scripts/smoke.mjs [case] [problem] (default quick room; problem room or cymbal)
10import { createServer } from 'node:http';
11import { mkdir, readFile } from 'node:fs/promises';
12import { extname, join } from 'node:path';
13import puppeteer from 'puppeteer-core';
15const DIST = new URL('../dist/', import.meta.url).pathname;
16const TMP = new URL('../tmp/', import.meta.url).pathname;
17const CHROME = process.env.CHROME_PATH ?? '/usr/bin/google-chrome';
18const CASE = process.argv[2] ?? 'quick';
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 19const PROBLEM = process.argv[3] ?? 'room';
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 20const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml' };
22const server = createServer(async (req, res) => {
23 try {
24 const path = req.url === '/' ? '/index.html' : req.url.split('?')[0];
25 const data = await readFile(join(DIST, path));
26 res.writeHead(200, { 'content-type': MIME[extname(path)] ?? 'application/octet-stream' });
27 res.end(data);
28 } catch {
29 res.writeHead(404);
30 res.end('not found');
31 }
32});
33await new Promise((r) => server.listen(0, '127.0.0.1', r));
34const port = server.address().port;
35await mkdir(TMP, { recursive: true });
37const flagSets = [
38 ['--headless=new', '--no-sandbox', '--enable-unsafe-webgpu', '--enable-features=Vulkan'],
39 ['--headless=new', '--no-sandbox', '--enable-unsafe-webgpu', '--use-webgpu-adapter=swiftshader', '--enable-unsafe-swiftshader'],
40];
41if (process.env.SWIFTSHADER_ONLY) flagSets.shift();
43let ok = false;
44for (const flags of flagSets) {
45 const browser = await puppeteer.launch({ executablePath: CHROME, args: [...flags, '--window-size=1280,1400'], protocolTimeout: 1_800_000 });
46 const problems = [];
47 try {
48 const page = await browser.newPage();
49 await page.setViewport({ width: 1280, height: 1400 });
50 page.on('console', (m) => {
51 if (process.env.VERBOSE) console.log(` [${m.type()}] ${m.text()}`);
52 if (m.type() === 'error') problems.push(`console: ${m.text()}`);
53 });
54 page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
55 page.on('requestfailed', (r) => problems.push(`request failed: ${r.url()}`));
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 56 await page.goto(`http://127.0.0.1:${port}/index.html?noroom&problem=${PROBLEM}`, { waitUntil: 'load' });
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 57 await page.waitForFunction(() => /^ready/.test(document.getElementById('status')?.textContent ?? ''), { timeout: 60_000, polling: 250 });
58 console.log(await page.$eval('#device', (e) => e.textContent));
59 await page.select('#preset', CASE);
60 await page.select('#ngrid', '61');
61 const t0 = Date.now();
62 await page.click('#solve');
63 let lastStatus = '';
64 const poll = setInterval(async () => {
65 try {
66 const s = await page.$eval('#status', (e) => e.textContent);
67 if (s !== lastStatus && !/iteration/.test(s)) console.log(` ${s}`);
68 lastStatus = s;
69 } catch {}
70 }, 2000);
71 await page.waitForFunction(() => /^(done|failed)/.test(document.getElementById('status')?.textContent ?? ''), { timeout: 1_800_000, polling: 500 });
72 clearInterval(poll);
73 const status = await page.$eval('#status', (e) => e.textContent);
74 const err = await page.$eval('#err', (e) => e.textContent);
75 const rows = await page.$$eval('#results tr', (trs) => trs.map((tr) => [...tr.children].map((td) => td.textContent).join(': ')));
76 console.log(`status: ${status} (${((Date.now() - t0) / 1000).toFixed(1)} s)`);
77 for (const r of rows) console.log(` ${r}`);
78 if (err) problems.push(`error line: ${err}`);
79 await new Promise((r) => setTimeout(r, 1000));
70e5eeeAdd the cymbal problem and GIF exportJeremy Magland 80 await page.screenshot({ path: join(TMP, `smoke-${PROBLEM}.png`), fullPage: true });
81 console.log(`screenshot: ${join(TMP, `smoke-${PROBLEM}.png`)}`);
bc338a7WebGPU port of the fmm3dbie impedance solver as an interactive web appJeremy Magland 82 if (!/^done/.test(status)) problems.push(`status: ${status}`);
83 ok = problems.length === 0;
84 } catch (e) {
85 problems.push(`harness: ${e.message}`);
86 } finally {
87 await browser.close();
88 }
89 for (const p of problems) console.log(`problem: ${p}`);
90 if (ok) break;
91 console.log('retrying with next flag set…');
93server.close();
94console.log(ok ? 'smoke test passed' : 'smoke test FAILED');
95process.exit(ok ? 0 : 1);
↑↓ move↵openescclose