browser/ is a wasm32 module that runs every graph vector of ops-conformance on the WebGPU adapter a page is offered, the kernels compiled by the browser's own WGSL compiler, and holds each decode to the committed result and to the interpreter running in the same page. run.mjs serves it with the corpus and drives a headless browser (on macOS through Metal), and fails on a mismatch, a device error or a page error, or, under WAI_OPS_GPU_REQUIRE=1, when no adapter ran the corpus. tests/support/corpus.rs now parses a file apart from reading it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
92 lines
4.8 KiB
JavaScript
92 lines
4.8 KiB
JavaScript
// The ops-conformance graph vectors through a browser's WebGPU.
|
|
//
|
|
// Build the page's module first:
|
|
//
|
|
// cd wai/wai-ops-gpu/browser
|
|
// cargo build --locked --release --target wasm32-unknown-unknown
|
|
// wasm-bindgen --target web --out-dir pkg --out-name wai_ops_gpu_browser \
|
|
// target/wasm32-unknown-unknown/release/wai_ops_gpu_browser.wasm
|
|
//
|
|
// then, with Playwright installed under PLAYWRIGHT_HOST:
|
|
//
|
|
// [WAI_OPS_GPU_REQUIRE=1] [WAI_OPS_GPU_BROWSER_CHANNEL=chrome] \
|
|
// PLAYWRIGHT_HOST=<dir with node_modules/playwright>/ node wai/wai-ops-gpu/browser/run.mjs
|
|
//
|
|
// It serves this directory and ../../ops-conformance on 127.0.0.1, opens index.html in
|
|
// a headless browser and prints the page's report. It fails on a page error, a
|
|
// mismatch or a device error, and, under WAI_OPS_GPU_REQUIRE=1, when the page found no
|
|
// adapter or the report has no `browser webgpu: key=` line with mismatches=0 errors=0.
|
|
// Headless, macOS offers a page a hardware adapter only when the browser draws through
|
|
// Metal (`--use-angle=metal`); WAI_OPS_GPU_BROWSER_ARGS (space-separated) replaces the
|
|
// launch arguments.
|
|
import http from "node:http";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { createRequire } from "node:module";
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const CORPUS = path.resolve(HERE, "../../ops-conformance");
|
|
const REQUIRE = process.env.WAI_OPS_GPU_REQUIRE === "1";
|
|
const PORT = Number(process.env.PORT || 8797);
|
|
const TIMEOUT_MS = Number(process.env.WAI_OPS_GPU_TIMEOUT_MS || 20 * 60 * 1000);
|
|
const ARGS = process.env.WAI_OPS_GPU_BROWSER_ARGS !== undefined
|
|
? process.env.WAI_OPS_GPU_BROWSER_ARGS.split(/\s+/).filter(Boolean)
|
|
: (process.platform === "darwin" ? ["--use-angle=metal"] : []).concat(["--enable-unsafe-webgpu"]);
|
|
const { chromium } = createRequire(process.env.PLAYWRIGHT_HOST || HERE + "/")("playwright");
|
|
|
|
const files = ["vectors", "graphs"].flatMap((d) =>
|
|
fs.readdirSync(path.join(CORPUS, d)).filter((f) => f.endsWith(".jsonl")).sort().map((f) => `${d}/${f}`));
|
|
const ROOTS = { "/ops-conformance/": CORPUS, "/browser/": HERE };
|
|
const MIME = { ".js": "text/javascript", ".wasm": "application/wasm", ".html": "text/html", ".json": "application/json", ".jsonl": "text/plain" };
|
|
const server = http.createServer((req, res) => {
|
|
const url = decodeURIComponent(req.url.split("?")[0]);
|
|
if (url === "/browser/files.json") {
|
|
res.writeHead(200, { "content-type": "application/json" });
|
|
return res.end(JSON.stringify(files));
|
|
}
|
|
const prefix = Object.keys(ROOTS).find((p) => url.startsWith(p));
|
|
const root = prefix && ROOTS[prefix];
|
|
const f = root && path.resolve(root, "." + url.slice(prefix.length - 1));
|
|
if (!f || !f.startsWith(root + path.sep) || !fs.existsSync(f) || !fs.statSync(f).isFile()) {
|
|
res.writeHead(404);
|
|
return res.end("not found");
|
|
}
|
|
res.writeHead(200, { "content-type": MIME[path.extname(f)] || "application/octet-stream" });
|
|
fs.createReadStream(f).pipe(res);
|
|
});
|
|
|
|
let browser;
|
|
try {
|
|
if (!fs.existsSync(path.join(HERE, "pkg", "wai_ops_gpu_browser.js"))) {
|
|
console.error("no pkg/wai_ops_gpu_browser.js; build it as this file's header says");
|
|
process.exit(2);
|
|
}
|
|
await new Promise((r) => server.listen(PORT, "127.0.0.1", r));
|
|
const channel = process.env.WAI_OPS_GPU_BROWSER_CHANNEL || undefined;
|
|
browser = await chromium.launch({ args: ARGS, channel });
|
|
console.log(`browser: ${channel || "chromium"} ${browser.version()} args=${ARGS.join(" ")}`);
|
|
const page = await browser.newPage();
|
|
const errs = [];
|
|
page.on("pageerror", (e) => errs.push(String(e)));
|
|
await page.goto(`http://127.0.0.1:${PORT}/browser/index.html`, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
await page.waitForFunction(() => window.opsGpu && window.opsGpu.done === true, null, { timeout: TIMEOUT_MS, polling: 1000 });
|
|
const report = await page.evaluate(() => window.opsGpu.report);
|
|
const info = await page.evaluate(async () => {
|
|
if (!navigator.gpu) return "navigator.gpu is absent";
|
|
const a = await navigator.gpu.requestAdapter();
|
|
if (!a) return "requestAdapter gave none";
|
|
const i = a.info || {};
|
|
return `vendor=${i.vendor} architecture=${i.architecture} device=${i.device} description=${i.description}`;
|
|
});
|
|
console.log(`adapter info (navigator.gpu): ${info}`);
|
|
console.log(report);
|
|
for (const e of errs) console.log(`page error: ${e}`);
|
|
const ok = report.split("\n").some((l) => /^browser webgpu: key=\S+ .* mismatches=0 errors=0$/.test(l));
|
|
const bad = errs.length > 0 || /MISMATCH|DEVICE ERROR|page error|did not parse/.test(report) ||
|
|
report.split("\n").some((l) => /^browser webgpu: key=/.test(l) && !/mismatches=0 errors=0$/.test(l));
|
|
if (bad || (REQUIRE && !ok)) process.exitCode = 1;
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
server.close();
|
|
}
|