/ proof-of-concept / plate-mechanics-sim
Sign in
proof-of-concept / plate-mechanics-sim
plate-mechanics-sim / packages / core / src / runner / workerRunner.ts
93 lines · 3.0 KBCodeBlameHistory
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 1import type { BackendInfo, RunRequest, RunResult, Runner } from "./types";
2import type { FromWorker, ToWorker } from "./protocol";
4interface Pending {
5 resolve: (r: RunResult) => void;
6 onOutput?: (text: string) => void;
7 output: string[];
8 t0: number;
9}
11/**
12 * Runs numbl in a Web Worker the runner owns. Cancelling terminates the worker
13 * and a fresh one is created for the next run, so a runaway time-stepping loop
14 * can always be stopped, with or without cross-origin isolation.
15 */
16export class WorkerRunner implements Runner {
17 private worker: Worker | null = null;
18 private nextId = 1;
19 private pending = new Map<number, Pending>();
20 private infoResolvers = new Map<number, (b: BackendInfo) => void>();
21 private cachedBackend: BackendInfo | null = null;
22 private readonly createWorker: () => Worker;
24 constructor(createWorker?: () => Worker) {
25 this.createWorker =
26 createWorker ?? (() => new Worker(new URL("./worker.ts", import.meta.url), { type: "module" }));
27 }
29 get backend(): Promise<BackendInfo> {
30 if (this.cachedBackend) return Promise.resolve(this.cachedBackend);
31 return new Promise((resolve) => {
32 const id = this.nextId++;
33 this.infoResolvers.set(id, (b) => {
34 this.cachedBackend = b;
35 resolve(b);
36 });
37 this.ensureWorker().postMessage({ type: "info", id } satisfies ToWorker);
38 });
39 }
41 private ensureWorker(): Worker {
42 if (this.worker) return this.worker;
43 const w = this.createWorker();
44 w.onmessage = (e: MessageEvent<FromWorker>) => this.onMessage(e.data);
45 w.onerror = (e) => this.failAll(`numbl worker crashed: ${e.message || "unknown error"}`);
46 this.worker = w;
47 return w;
48 }
50 private onMessage(msg: FromWorker) {
51 if (msg.type === "info") {
52 this.infoResolvers.get(msg.id)?.(msg.backend);
53 this.infoResolvers.delete(msg.id);
54 return;
55 }
56 const p = this.pending.get(msg.id);
57 if (!p) return;
58 if (msg.type === "output") {
59 p.output.push(msg.text);
60 p.onOutput?.(msg.text);
61 } else if (msg.type === "done") {
62 this.pending.delete(msg.id);
63 p.resolve(msg.result);
64 }
65 }
67 private failAll(error: string, cancelled = false) {
68 const backend = this.cachedBackend ?? { engine: "numbl", version: "unknown", optimization: "1", host: "web-worker" };
69 for (const p of this.pending.values()) {
70 p.resolve({ ok: false, error, output: p.output, wallMs: performance.now() - p.t0, backend, cancelled });
71 }
72 this.pending.clear();
73 }
75 run(req: RunRequest, onOutput?: (text: string) => void): Promise<RunResult> {
76 return new Promise((resolve) => {
77 const id = this.nextId++;
78 this.pending.set(id, { resolve, onOutput, output: [], t0: performance.now() });
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 79 this.ensureWorker().postMessage({ type: "run", id, bundle: req.bundle, inputs: req.inputs, gpu: req.gpu } satisfies ToWorker);
81 }
83 cancel(): void {
84 if (!this.worker) return;
85 this.worker.terminate();
86 this.worker = null;
87 this.failAll("run cancelled", true);
88 }
90 dispose(): void {
91 this.cancel();
92 }
↑↓ move↵openescclose