// Drives a simulator backend chunk by chunk with an adaptive chunk size, pausing and stopping on // request, and reports pressure chunks, diagnostics and progress to the UI. import type { Setup } from "./model/setup.ts"; import type { ChunkResult, Diagnostics, SimulatorBackend } from "./model/simulator.ts"; export interface RunCallbacks { onChunk(res: ChunkResult): void; onDiagnostics(d: Diagnostics): void; onProgress(samplesDone: number, wallSeconds: number): void; onDone(stopped: boolean): void; } export class Runner { private paused = false; private stopped = false; private resume: (() => void) | null = null; private waiting = false; private finished = false; private onPaused: (() => void)[] = []; private chunk = 512; readonly backend: SimulatorBackend; readonly setup: Setup; private readonly cb: RunCallbacks; constructor(backend: SimulatorBackend, setup: Setup, cb: RunCallbacks) { this.backend = backend; this.setup = setup; this.cb = cb; } async run() { const { setup, backend } = this, t0 = performance.now(); let lastDiag = t0; while (!this.stopped && backend.samplesDone < setup.nSamples) { if (this.paused) { this.waiting = true; for (const r of this.onPaused) r(); this.onPaused = []; await new Promise((r) => { this.resume = r; }); this.waiting = false; } if (this.stopped) break; const n = Math.min(this.chunk, backend.maxChunk, setup.nSamples - backend.samplesDone); const tc = performance.now(); const res = await backend.runChunk(n); const dtc = (performance.now() - tc) / 1000; // aim at about 0.4 s of wall time per chunk, in multiples of 128 samples, at most 8192 this.chunk = Math.max(128, Math.min(backend.maxChunk, Math.round((n * 0.4) / Math.max(dtc, 1e-3) / 128) * 128)); this.cb.onChunk(res); this.cb.onProgress(backend.samplesDone, (performance.now() - t0) / 1000); if (performance.now() - lastDiag > 1000 || backend.samplesDone >= setup.nSamples) { this.cb.onDiagnostics(await backend.diagnostics()); lastDiag = performance.now(); } await new Promise((r) => setTimeout(r, 0)); } this.finished = true; for (const r of this.onPaused) r(); this.onPaused = []; this.cb.onDone(this.stopped); } pause() { this.paused = true; } unpause() { this.paused = false; this.resume?.(); this.resume = null; } stop() { this.stopped = true; this.unpause(); } get isPaused() { return this.paused; } /** Resolves once the loop is idle between chunks (paused) or has finished, so that the backend's state is consistent. */ waitPaused(): Promise { if (this.finished || this.waiting) return Promise.resolve(); return new Promise((r) => this.onPaused.push(r)); } }