/** * Benchmark orchestration: for each run, synthesize one dataset (run r * uses seed + r, so the whole benchmark is reproducible from the config), * hand the same dataset to every enabled algorithm through the worker, * and score each result against the ground truth. Algorithms run * sequentially on one worker so timings are comparable and a runaway * script can be cancelled by killing the worker. */ import { buildTraining } from './training.ts'; import { score } from './score.ts'; import { synthesize } from './synthesize.ts'; import type { AlgorithmDef, RunResult, SnippetLibrary, SynthConfig } from './types.ts'; import { FS } from './types.ts'; import type { AlgoRequest, AlgoResponse } from './algo.worker.ts'; export interface BenchmarkProgress { runIndex: number; runCount: number; algorithmName: string; done: number; total: number; } export interface BenchmarkOutput { runs: { seed: number; x: Float32Array; truth: { t: number; typeIndex: number; gain: number }[]; /** Per type: the training snippets this run handed to every * algorithm, concatenated, so an export can reproduce the run. */ training: { data: Float32Array; count: number }[]; }[]; results: RunResult[]; } export class BenchmarkRunner { private worker: Worker | null = null; private nextId = 1; cancelled = false; cancel(): void { this.cancelled = true; this.worker?.terminate(); this.worker = null; } private exec(req: Omit): Promise { this.worker ??= new Worker(new URL('./algo.worker.ts', import.meta.url), { type: 'module' }); const worker = this.worker; const id = this.nextId++; return new Promise((resolve, reject) => { const onMsg = (e: MessageEvent) => { if (e.data.id !== id) return; worker.removeEventListener('message', onMsg); resolve(e.data); }; worker.addEventListener('message', onMsg); worker.addEventListener('error', (e) => reject(new Error(e.message)), { once: true }); worker.postMessage({ id, ...req }); }); } async run( lib: SnippetLibrary, cfg: SynthConfig, runCount: number, toleranceMs: number, algorithms: AlgorithmDef[], onProgress: (p: BenchmarkProgress) => void, ): Promise { this.cancelled = false; const runs: BenchmarkOutput['runs'] = []; const results: RunResult[] = []; const total = runCount * algorithms.length; let done = 0; for (let r = 0; r < runCount && !this.cancelled; r++) { const ds = synthesize(lib, cfg, r); const training = buildTraining(lib, ds.trainIndices); runs.push({ seed: cfg.seed + r, x: ds.x, truth: ds.truth, training: training.snips.map((s) => ({ data: Float32Array.from(s.data), count: s.count })), }); for (const algo of algorithms) { if (this.cancelled) break; onProgress({ runIndex: r, runCount, algorithmName: algo.name, done, total }); const resp = await this.exec({ source: algo.source, x: ds.x, fs: FS, training }); done++; if (resp.ok && resp.detections) { results.push({ runIndex: r, seed: cfg.seed + r, algorithmId: algo.id, score: score(ds.truth, resp.detections, lib.names.length, toleranceMs / 1000), elapsedMs: resp.elapsedMs, logs: resp.logs, detections: resp.detections, }); } else { results.push({ runIndex: r, seed: cfg.seed + r, algorithmId: algo.id, score: score(ds.truth, [], lib.names.length, toleranceMs / 1000), elapsedMs: resp.elapsedMs, error: resp.error ?? 'unknown error', logs: resp.logs, detections: [], }); } } } this.worker?.terminate(); this.worker = null; return { runs, results }; } }