1/**
2 * Browser validation, in the environment the demo actually ships to.
3 *
4 * Runs the same three check modules as `npm run test:node` — so both GPU stacks
5 * (Dawn on the desktop, the browser's own here) get the same guarantees — plus a
6 * long soak that only makes sense in a page.
7 *
8 * Results are posted to window.__RESULTS__ for the headless runner.
9 */
10import { requestShtDevice, describeAdapter } from '../src/sht/sht.ts';
11import { ModelSession } from '../src/mgpu/session.ts';
12import { mModels, defaultParams } from '../src/mgpu/registry.ts';
13import { digestOf, formatDigest, type StateDigest } from '../src/mgpu/digest.ts';
14import { parseArgs, modelForSpec, formatCommand } from '../src/bench/runSpec.ts';
15import { transformChecks } from './transformChecks.ts';
16import { analyticChecks } from './analyticChecks.ts';
17import { modelChecks } from './modelChecks.ts';
19declare global {
20 interface Window {
21 __RESULTS__?: { ok: boolean; fatal?: string; lines: string[] };
22 /** Set by the ?state= mode, for scripts/compare-env.mjs. */
23 __STATE__?: { digest: StateDigest; state: number[] };
24 }
25}
27const logEl = document.getElementById('log')!;
28const lines: string[] = [];
29let failures = 0;
31function log(s: string): void {
32 lines.push(s);
33 logEl.textContent = lines.join('\n');
34 console.log(s);
35}
37function check(name: string, ok: boolean, detail: string): void {
38 log(`${ok ? 'PASS' : 'FAIL'} ${name} ${detail}`);
39 if (!ok) failures++;
40}
42/**
43 * Solver-only soak, selected with ?soak=<steps>&lmax=<n>. No rendering, so it
44 * isolates the compiled .m and the transforms from three.js.
45 */
46async function soak(steps: number, lmax: number): Promise<void> {
47 const device = await requestShtDevice();
48 const model = mModels[0];
49 const session = await ModelSession.create({
50 device,
51 model,
52 params: defaultParams(model),
53 lmax,
54 });
55 session.seed(5);
56 log(
57 `soak: ${steps} steps at lmax ${lmax} ` +
58 `(grid ${session.cfg.nlat}x${session.cfg.nphi}), solver only`,
59 );
61 const BATCH = 25;
62 const t0 = performance.now();
63 for (let s = 0; s < steps; s += BATCH) {
64 session.step(Math.min(BATCH, steps - s));
65 const u = await session.read(model.species[0]);
66 let lo = Infinity;
67 let hi = -Infinity;
68 for (const v of u) {
69 if (v < lo) lo = v;
70 if (v > hi) hi = v;
71 }
72 if ((s + BATCH) % 100 === 0) {
73 const mem = (performance as Performance & { memory?: { usedJSHeapSize: number } })
74 .memory;
75 log(
76 ` step ${session.steps} u in [${lo.toFixed(4)}, ${hi.toFixed(4)}]` +
77 (mem ? ` heap ${(mem.usedJSHeapSize / 1048576).toFixed(1)} MB` : ''),
78 );
79 // yield so the page stays responsive and the runner can poll
80 await new Promise((r) => setTimeout(r, 0));
81 }
82 }
83 const ms = (performance.now() - t0) / steps;
85 const final = await session.read(model.species[0]);
86 let finite = true;
87 for (const v of final) if (!Number.isFinite(v)) finite = false;
88 check(`soak: ${steps} steps survived`, finite, `${ms.toFixed(1)} ms/step`);
90 session.destroy();
91 window.__RESULTS__ = { ok: failures === 0, lines };
92 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
93}
95/**
96 * Run one exact spec and post its final state, for scripts/compare-env.mjs to
97 * compare against the same spec run on the desktop. Query parameters map
98 * straight onto the benchmark's flags — `?state=1&lmax=31&steps=200` — and go
99 * through the same parseArgs, so neither side can quietly use different
100 * defaults.
101 */
102async function dumpState(q: URLSearchParams): Promise<void> {
103 const argv: string[] = [];
104 for (const [k, v] of q) {
105 if (k === 'state') continue;
106 argv.push(`--${k}`, v);
107 }
108 const spec = parseArgs(argv);
109 const model = modelForSpec(spec);
111 const device = await requestShtDevice();
112 const adapter = await describeAdapter(device);
113 const session = await ModelSession.create({
114 device,
115 model,
116 params: spec.params,
117 lmax: spec.lmax,
118 });
119 session.seed(spec.seed);
120 session.step(spec.steps);
121 await session.sync();
122 const state = await session.read(model.state[0]);
123 const digest = digestOf(state, session.sht.fourierMode, adapter);
125 log(`${formatCommand(spec)}\n`);
126 log(`state after ${spec.steps} steps from seed ${spec.seed}:`);
127 log(` ${formatDigest(digest)}`);
128 log(` adapter: ${adapter}`);
129 window.__STATE__ = { digest, state: [...state] };
130 session.destroy();
131}
133async function main(): Promise<void> {
134 const q = new URLSearchParams(location.search);
135 if (q.has('state')) return dumpState(q);
136 if (q.has('soak')) {
137 return soak(Number(q.get('soak')) || 500, Number(q.get('lmax')) || 63);
138 }
139 const device = await requestShtDevice();
141 await transformChecks(device, check, log);
142 await analyticChecks(device, check, log);
143 await modelChecks(device, check, log);
145 window.__RESULTS__ = { ok: failures === 0, lines };
146 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
147}
149main().catch((e) => {
150 const msg = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e);
151 log(`fatal: ${msg}`);
152 window.__RESULTS__ = { ok: false, fatal: msg, lines };
153});