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:
parent
3054fccc33
commit
0c0bec7784
4 changed files with 124 additions and 82 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue