/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
87 lines · 3.4 KBBlameHistoryRaw
1/**
2 * Headless GPU test runner: serves dist/, opens test.html in headless
3 * Chrome (falling back to the SwiftShader software WebGPU adapter when no
4 * hardware GPU is available), and reports the suite results.
5 *
6 * Run after `vite build`: node scripts/test-gpu.mjs [--sweep]
7 *
8 * --sweep adds the niter x geometry sweep, which the page leaves out by
9 * default because it is far too slow here to belong in CI: every session it
10 * builds recompiles its whole unrolled step (445 kernels at niter 8), and
11 * software WebGPU compiles those at about a second each. See
12 * test/geometryChecks.ts.
13 */
14import { createServer } from 'node:http';
15import { readFile } from 'node:fs/promises';
16import { extname, join } from 'node:path';
17import puppeteer from 'puppeteer-core';
19const DIST = new URL('../dist/', import.meta.url).pathname;
20const CHROME = process.env.CHROME_PATH ?? '/usr/bin/google-chrome';
21const MIME = {
22 '.html': 'text/html',
23 '.js': 'text/javascript',
24 '.css': 'text/css',
25 '.json': 'application/json',
26 '.wasm': 'application/wasm',
27};
29const server = createServer(async (req, res) => {
30 try {
31 const path = req.url === '/' ? '/index.html' : req.url.split('?')[0];
32 const data = await readFile(join(DIST, path));
33 res.writeHead(200, { 'content-type': MIME[extname(path)] ?? 'application/octet-stream' });
34 res.end(data);
35 } catch {
36 res.writeHead(404);
37 res.end('not found');
38 }
39});
40await new Promise((r) => server.listen(0, '127.0.0.1', r));
41const port = server.address().port;
43const flagSets = [
44 // hardware first, then SwiftShader (software) WebGPU
45 ['--headless=new', '--no-sandbox', '--enable-unsafe-webgpu', '--enable-features=Vulkan'],
46 ['--headless=new', '--no-sandbox', '--enable-unsafe-webgpu', '--use-webgpu-adapter=swiftshader', '--enable-unsafe-swiftshader'],
47];
49const query = process.argv.includes('--sweep') ? '?sweep=1' : '';
51let final = null;
52for (const flags of flagSets) {
53 const browser = await puppeteer.launch({
54 executablePath: CHROME,
55 // A copy: puppeteer splices --enable-features out of the array it is given
56 // and re-adds it merged with its own, which would drop it from the
57 // diagnostic below and make a failure look like it ran with fewer flags.
58 args: [...flags],
59 // Puppeteer's default is 180 s, and it bounds the CDP call that
60 // waitForFunction polls inside — so without this the wait below silently
61 // caps at 3 minutes no matter what timeout it is given, and a suite that
62 // runs longer fails as 'Runtime.callFunctionOn timed out' with no results.
63 protocolTimeout: 900_000,
64 });
65 try {
66 const page = await browser.newPage();
67 page.on('console', (msg) => console.log(` [page] ${msg.text()}`));
68 page.on('pageerror', (err) => console.log(` [pageerror] ${err.message}`));
69 await page.goto(`http://127.0.0.1:${port}/test.html${query}`, { waitUntil: 'load' });
70 const results = await page.waitForFunction(() => window.__RESULTS__, { timeout: 600_000 });
71 final = await results.jsonValue();
72 } catch (e) {
73 console.error(`run with flags [${flags.join(' ')}] failed: ${e.message}`);
74 } finally {
75 await browser.close();
76 }
77 if (final && !final.fatal) break;
78 console.log('retrying with next flag set…');
80server.close();
82if (!final || final.fatal) {
83 console.error(`GPU tests could not run: ${final?.fatal ?? 'no results'}`);
84 process.exit(2);
86console.log(final.ok ? 'GPU SUITE: PASS' : 'GPU SUITE: FAIL');
87process.exit(final.ok ? 0 : 1);
moveopenescclose