/ concept-collection / turing-sphere
Sign in
concept-collection / turing-sphere
87 lines · 3.2 KBCodeBlameHistory
2 * Soak test: drive the demo page for many steps and report JS heap growth and
3 * any crash, distinguishing a page crash from a renderer/driver death.
4 *
5 * Usage: node scripts/soak.mjs [steps] [lmax] [backend]
6 * e.g. node scripts/soak.mjs 1500 63 webgpu
7 */
8import { createServer } from 'node:http';
9import { readFile } from 'node:fs/promises';
10import { extname, join } from 'node:path';
11import puppeteer from 'puppeteer-core';
13const steps = Number(process.argv[2] ?? 1000);
14const lmax = process.argv[3] ?? '63';
15const backend = process.argv[4] ?? 'webgpu';
16const DIST = new URL('../dist/', import.meta.url).pathname;
17const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css' };
19const server = createServer(async (req, res) => {
20 try {
21 const path = req.url === '/' ? '/index.html' : req.url.split('?')[0];
22 const data = await readFile(join(DIST, path));
23 res.writeHead(200, { 'content-type': MIME[extname(path)] ?? 'application/octet-stream' });
24 res.end(data);
25 } catch {
26 res.writeHead(404);
27 res.end();
28 }
29});
30await new Promise((r) => server.listen(0, '127.0.0.1', r));
31const port = server.address().port;
33const browser = await puppeteer.launch({
34 executablePath: process.env.CHROME_PATH ?? '/usr/bin/google-chrome',
35 args: ['--headless=new', '--no-sandbox', '--enable-unsafe-webgpu',
36 '--use-webgpu-adapter=swiftshader', '--enable-unsafe-swiftshader'],
37});
38const page = await browser.newPage();
39await page.setViewport({ width: 1000, height: 900 });
41let crashed = null;
42page.on('error', (e) => { crashed = `page crash: ${e.message}`; });
43page.on('pageerror', (e) => { crashed = `page error: ${e.message}`; });
44page.on('console', (m) => {
45 const t = m.text();
46 if (!/GL Driver Message|Failed to load resource/.test(t)) console.log(' [page]', t);
47});
49await page.goto(`http://127.0.0.1:${port}/index.html`, { waitUntil: 'load' });
50await page.waitForFunction(() => /grid/.test(document.getElementById('stats')?.textContent ?? ''), { timeout: 120_000 });
51await page.select('#lmax', lmax);
52await page.select('#backend', backend);
53await page.waitForFunction(() => /grid/.test(document.getElementById('stats')?.textContent ?? ''), { timeout: 120_000 });
54await page.click('#runpause');
56const readStep = () =>
57 page.evaluate(() => {
58 const m = document.getElementById('stats')?.textContent?.match(/\((\d+) steps\)/);
59 return m ? Number(m[1]) : 0;
60 });
61const heapMB = async () => {
62 const m = await page.metrics();
63 return (m.JSHeapUsedSize / 1048576).toFixed(1);
64};
66const t0 = Date.now();
67let last = 0;
68let stalls = 0;
69try {
70 while (last < steps) {
71 await new Promise((r) => setTimeout(r, 5000));
72 if (crashed) throw new Error(crashed);
73 const now = await readStep();
74 console.log(` step ${now} heap ${await heapMB()} MB (+${now - last} in 5s)`);
75 if (now === last) {
76 if (++stalls >= 6) throw new Error(`stalled at step ${now}`);
77 } else stalls = 0;
78 last = now;
79 }
80 console.log(`SOAK PASS: ${last} steps in ${((Date.now() - t0) / 1000).toFixed(0)}s, heap ${await heapMB()} MB`);
81} catch (e) {
82 console.error(`SOAK FAIL at step ${last}: ${e.message}`);
83 process.exitCode = 1;
84} finally {
85 await browser.close().catch(() => {});
86 server.close();
moveopenescclose