playground: race 3 tiers in parallel workers

One Worker per tier (python/c/asm). "All three" mode dispatches
Promise.all so the tiers race on independent threads — a slow Pyodide
no longer blocks C and asm. Each tier-block ticks its own ms counter
until its worker resolves.

Output grid: 3 columns when 3 tier-blocks render, else stacked.
Status bar announces the winner: "ok — c won in 47 ms".

Cancel terminates every active worker.
This commit is contained in:
russell@unturf.com 2026-06-14 12:38:22 -04:00
parent 3054fccc33
commit 0c0bec7784
No known key found for this signature in database
4 changed files with 124 additions and 82 deletions

View file

@ -72,37 +72,41 @@ function setStatus(text, cls) {
}
// ─── Worker plumbing ───────────────────────────────────────────────
// One worker hosts all tiers. Cancel terminates it; the next eval
// creates a fresh one. Cached Pyodide / Emscripten state is lost on
// cancel, which is the price of true cancellation.
// ONE worker per tier so the three tiers run on independent threads —
// in "All three" mode they race, and a slow tier never blocks a fast one.
// Cancel terminates every active worker; next eval recreates them.
const workerState = { worker: null, runId: 0, pending: null };
const workerState = {
workers: { python: null, c: null, asm: null },
pending: { python: null, c: null, asm: null },
nextRunId: 0,
};
function spawnWorker() {
return new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" });
}
function ensureWorker() {
if (!workerState.worker) workerState.worker = spawnWorker();
return workerState.worker;
function ensureWorker(tier) {
if (!workerState.workers[tier]) workerState.workers[tier] = spawnWorker();
return workerState.workers[tier];
}
function runOnTierInWorker(tier, src, onLoading) {
return new Promise((resolve, reject) => {
const w = ensureWorker();
const myRunId = ++workerState.runId;
workerState.pending = { runId: myRunId, resolve, reject };
const w = ensureWorker(tier);
const myRunId = ++workerState.nextRunId;
workerState.pending[tier] = { runId: myRunId, resolve, reject };
const handler = (e) => {
if (e.data.runId !== myRunId) return;
if (e.data.kind === "loading") {
onLoading && onLoading(e.data.tier);
} else if (e.data.kind === "done") {
w.removeEventListener("message", handler);
workerState.pending = null;
workerState.pending[tier] = null;
resolve(e.data.output);
} else if (e.data.kind === "error") {
w.removeEventListener("message", handler);
workerState.pending = null;
workerState.pending[tier] = null;
reject(new Error(e.data.message));
}
};
@ -112,13 +116,15 @@ function runOnTierInWorker(tier, src, onLoading) {
}
function cancelCurrentRun() {
if (workerState.worker) {
workerState.worker.terminate();
workerState.worker = null;
}
if (workerState.pending) {
workerState.pending.reject(new Error("cancelled"));
workerState.pending = null;
for (const t of Object.keys(workerState.workers)) {
if (workerState.workers[t]) {
workerState.workers[t].terminate();
workerState.workers[t] = null;
}
if (workerState.pending[t]) {
workerState.pending[t].reject(new Error("cancelled"));
workerState.pending[t] = null;
}
}
}
@ -172,39 +178,46 @@ async function runAll() {
cancelBtn.disabled = false;
setStatus("running…", "busy");
outputEl.innerHTML = "";
let anyErr = false;
let cancelled = false;
try {
const tiers = selectedTiers();
const src = getEditorText();
for (const t of tiers) {
const live = makeLiveBlock(t);
const startMark = live.startTimer();
try {
const output = await runOnTierInWorker(t, src, (loadingTier) => {
setStatus(`loading ${loadingTier} tier…`, "busy");
});
setStatus(`running ${t}`, "busy");
const tiers = selectedTiers();
const src = getEditorText();
// Each tier runs in its own worker — Promise.all so they race.
const tierPromises = tiers.map((t) => {
const live = makeLiveBlock(t);
const startMark = live.startTimer();
return runOnTierInWorker(t, src, (loadingTier) => {
// Status reflects whichever tier most recently announced a load.
setStatus(`loading ${loadingTier}`, "busy");
})
.then((output) => {
const elapsed = performance.now() - startMark;
live.stop(elapsed);
live.setOutput(output);
} catch (e) {
return { tier: t, ok: true, elapsed };
})
.catch((e) => {
const elapsed = performance.now() - startMark;
if (e.message === "cancelled") {
live.cancelled(elapsed);
cancelled = true;
break;
return { tier: t, cancelled: true, elapsed };
}
live.stop(elapsed);
live.setError(e.message || String(e));
anyErr = true;
}
}
return { tier: t, error: e.message, elapsed };
});
});
try {
const results = await Promise.all(tierPromises);
const cancelled = results.some((r) => r.cancelled);
const anyErr = results.some((r) => r.error);
if (cancelled) setStatus("cancelled", "warn");
else setStatus(anyErr ? "completed with errors" : "ok", anyErr ? "err" : "ok");
else if (anyErr) setStatus("completed with errors", "err");
else {
// Winner of the race for ok styling.
const fastest = results.reduce((a, b) => (a.elapsed < b.elapsed ? a : b));
setStatus(`ok — ${fastest.tier} won in ${fastest.elapsed.toFixed(0)} ms`, "ok");
}
} catch (e) {
setStatus(`fatal: ${e.message}`, "err");
outputEl.textContent = e.stack || e.message;
} finally {
inFlight = false;
runBtn.disabled = false;

View file

@ -233,8 +233,16 @@ header code {
padding: 0.6rem 0.8rem;
font-size: 13px;
line-height: 1.35;
display: grid;
gap: 0.6rem;
align-content: start;
}
#output .tier-block { margin-bottom: 1rem; }
/* When 3 tier blocks render (All three mode), lay them out as 3 columns
* for race-style comparison. With fewer, they stack as a single column. */
#output:has(.tier-block:nth-child(3)) {
grid-template-columns: 1fr 1fr 1fr;
}
#output .tier-block { margin-bottom: 0; }
#output .tier-block:last-child { margin-bottom: 0; }
#output .tier-block h3 {
margin: 0 0 0.25rem;

View file

@ -72,37 +72,41 @@ function setStatus(text, cls) {
}
// ─── Worker plumbing ───────────────────────────────────────────────
// One worker hosts all tiers. Cancel terminates it; the next eval
// creates a fresh one. Cached Pyodide / Emscripten state is lost on
// cancel, which is the price of true cancellation.
// ONE worker per tier so the three tiers run on independent threads —
// in "All three" mode they race, and a slow tier never blocks a fast one.
// Cancel terminates every active worker; next eval recreates them.
const workerState = { worker: null, runId: 0, pending: null };
const workerState = {
workers: { python: null, c: null, asm: null },
pending: { python: null, c: null, asm: null },
nextRunId: 0,
};
function spawnWorker() {
return new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" });
}
function ensureWorker() {
if (!workerState.worker) workerState.worker = spawnWorker();
return workerState.worker;
function ensureWorker(tier) {
if (!workerState.workers[tier]) workerState.workers[tier] = spawnWorker();
return workerState.workers[tier];
}
function runOnTierInWorker(tier, src, onLoading) {
return new Promise((resolve, reject) => {
const w = ensureWorker();
const myRunId = ++workerState.runId;
workerState.pending = { runId: myRunId, resolve, reject };
const w = ensureWorker(tier);
const myRunId = ++workerState.nextRunId;
workerState.pending[tier] = { runId: myRunId, resolve, reject };
const handler = (e) => {
if (e.data.runId !== myRunId) return;
if (e.data.kind === "loading") {
onLoading && onLoading(e.data.tier);
} else if (e.data.kind === "done") {
w.removeEventListener("message", handler);
workerState.pending = null;
workerState.pending[tier] = null;
resolve(e.data.output);
} else if (e.data.kind === "error") {
w.removeEventListener("message", handler);
workerState.pending = null;
workerState.pending[tier] = null;
reject(new Error(e.data.message));
}
};
@ -112,13 +116,15 @@ function runOnTierInWorker(tier, src, onLoading) {
}
function cancelCurrentRun() {
if (workerState.worker) {
workerState.worker.terminate();
workerState.worker = null;
}
if (workerState.pending) {
workerState.pending.reject(new Error("cancelled"));
workerState.pending = null;
for (const t of Object.keys(workerState.workers)) {
if (workerState.workers[t]) {
workerState.workers[t].terminate();
workerState.workers[t] = null;
}
if (workerState.pending[t]) {
workerState.pending[t].reject(new Error("cancelled"));
workerState.pending[t] = null;
}
}
}
@ -172,39 +178,46 @@ async function runAll() {
cancelBtn.disabled = false;
setStatus("running…", "busy");
outputEl.innerHTML = "";
let anyErr = false;
let cancelled = false;
try {
const tiers = selectedTiers();
const src = getEditorText();
for (const t of tiers) {
const live = makeLiveBlock(t);
const startMark = live.startTimer();
try {
const output = await runOnTierInWorker(t, src, (loadingTier) => {
setStatus(`loading ${loadingTier} tier…`, "busy");
});
setStatus(`running ${t}`, "busy");
const tiers = selectedTiers();
const src = getEditorText();
// Each tier runs in its own worker — Promise.all so they race.
const tierPromises = tiers.map((t) => {
const live = makeLiveBlock(t);
const startMark = live.startTimer();
return runOnTierInWorker(t, src, (loadingTier) => {
// Status reflects whichever tier most recently announced a load.
setStatus(`loading ${loadingTier}`, "busy");
})
.then((output) => {
const elapsed = performance.now() - startMark;
live.stop(elapsed);
live.setOutput(output);
} catch (e) {
return { tier: t, ok: true, elapsed };
})
.catch((e) => {
const elapsed = performance.now() - startMark;
if (e.message === "cancelled") {
live.cancelled(elapsed);
cancelled = true;
break;
return { tier: t, cancelled: true, elapsed };
}
live.stop(elapsed);
live.setError(e.message || String(e));
anyErr = true;
}
}
return { tier: t, error: e.message, elapsed };
});
});
try {
const results = await Promise.all(tierPromises);
const cancelled = results.some((r) => r.cancelled);
const anyErr = results.some((r) => r.error);
if (cancelled) setStatus("cancelled", "warn");
else setStatus(anyErr ? "completed with errors" : "ok", anyErr ? "err" : "ok");
else if (anyErr) setStatus("completed with errors", "err");
else {
// Winner of the race for ok styling.
const fastest = results.reduce((a, b) => (a.elapsed < b.elapsed ? a : b));
setStatus(`ok — ${fastest.tier} won in ${fastest.elapsed.toFixed(0)} ms`, "ok");
}
} catch (e) {
setStatus(`fatal: ${e.message}`, "err");
outputEl.textContent = e.stack || e.message;
} finally {
inFlight = false;
runBtn.disabled = false;

View file

@ -233,8 +233,16 @@ header code {
padding: 0.6rem 0.8rem;
font-size: 13px;
line-height: 1.35;
display: grid;
gap: 0.6rem;
align-content: start;
}
#output .tier-block { margin-bottom: 1rem; }
/* When 3 tier blocks render (All three mode), lay them out as 3 columns
* for race-style comparison. With fewer, they stack as a single column. */
#output:has(.tier-block:nth-child(3)) {
grid-template-columns: 1fr 1fr 1fr;
}
#output .tier-block { margin-bottom: 0; }
#output .tier-block:last-child { margin-bottom: 0; }
#output .tier-block h3 {
margin: 0 0 0.25rem;