1// Drives a simulator backend chunk by chunk with an adaptive chunk size, pausing and stopping on
2// request, and reports pressure chunks, diagnostics and progress to the UI.
3import type { Setup } from "./model/setup.ts";
4import type { ChunkResult, Diagnostics, SimulatorBackend } from "./model/simulator.ts";
6export interface RunCallbacks {
7 onChunk(res: ChunkResult): void;
8 onDiagnostics(d: Diagnostics): void;
9 onProgress(samplesDone: number, wallSeconds: number): void;
10 onDone(stopped: boolean): void;
11}
13export class Runner {
14 private paused = false; private stopped = false; private resume: (() => void) | null = null;
15 private waiting = false; private finished = false; private onPaused: (() => void)[] = [];
16 private chunk = 512;
17 readonly backend: SimulatorBackend; readonly setup: Setup; private readonly cb: RunCallbacks;
19 constructor(backend: SimulatorBackend, setup: Setup, cb: RunCallbacks) { this.backend = backend; this.setup = setup; this.cb = cb; }
21 async run() {
22 const { setup, backend } = this, t0 = performance.now();
23 let lastDiag = t0;
24 while (!this.stopped && backend.samplesDone < setup.nSamples) {
25 if (this.paused) {
26 this.waiting = true; for (const r of this.onPaused) r(); this.onPaused = [];
27 await new Promise<void>((r) => { this.resume = r; });
28 this.waiting = false;
29 }
30 if (this.stopped) break;
31 const n = Math.min(this.chunk, backend.maxChunk, setup.nSamples - backend.samplesDone);
32 const tc = performance.now();
33 const res = await backend.runChunk(n);
34 const dtc = (performance.now() - tc) / 1000;
35 // aim at about 0.4 s of wall time per chunk, in multiples of 128 samples, at most 8192
36 this.chunk = Math.max(128, Math.min(backend.maxChunk, Math.round((n * 0.4) / Math.max(dtc, 1e-3) / 128) * 128));
37 this.cb.onChunk(res);
38 this.cb.onProgress(backend.samplesDone, (performance.now() - t0) / 1000);
39 if (performance.now() - lastDiag > 1000 || backend.samplesDone >= setup.nSamples) {
40 this.cb.onDiagnostics(await backend.diagnostics());
41 lastDiag = performance.now();
42 }
43 await new Promise((r) => setTimeout(r, 0));
44 }
45 this.finished = true; for (const r of this.onPaused) r(); this.onPaused = [];
46 this.cb.onDone(this.stopped);
47 }
49 pause() { this.paused = true; }
50 unpause() { this.paused = false; this.resume?.(); this.resume = null; }
51 stop() { this.stopped = true; this.unpause(); }
52 get isPaused() { return this.paused; }
53 /** Resolves once the loop is idle between chunks (paused) or has finished, so that the backend's state is consistent. */
54 waitPaused(): Promise<void> {
55 if (this.finished || this.waiting) return Promise.resolve();
56 return new Promise((r) => this.onPaused.push(r));
57 }
58}