Previously every (display ...) call buffered into the tier's outBuf /
sys.stdout / WAT output-buffer and only landed in the playground panel
after evalLisp returned. During the 18-second cuda-secp256k1-bench
dispatch the panel stayed blank, then everything (probe + telemetry +
heavy result) appeared at once. Fox: 'demo waits for the full program
to return before emitting instead of line by line as it finishes'.
Each tier's print sink now ALSO calls a per-eval onChunk callback
that the worker forwards to the main thread as a {kind:"chunk"}
postMessage. The main thread spawns an in-progress tier-block on
the first chunk and appends each subsequent chunk to its <pre>,
auto-scrolling. On done/error the timing header finalizes in place;
the block looks identical to a non-streamed appendBlock at the end.
C tier (Emscripten): the Module.print + Module.printErr callbacks
that already fired per-line now invoke currentOnChunk in addition
to buffering. evalLisp accepts an onChunk arg and threads it.
Python tier (pyodide): _lumbda_eval swaps sys.stdout for a
_StreamingStdout subclass of io.StringIO whose write() also calls
into globalThis._lumbdaPyEmitChunk on the JS side. The final
auto-printed value also emits.
Asm tier (WAT): output stays buffered until evalLisp returns —
streaming there needs a new wasm import (out_line) and a wasm
rebuild. Caller-side fallback: app.js's appendBlock path still
runs for asm so its block appears at the end as before.
worker.mjs forwards each chunk via postMessage; app.js's
runOnTierInWorker handles 'chunk' messages by lazily creating
a startLiveBlock on first chunk, appending text on each, and
finalizing the timing in finalize() once done arrives.
57 lines
2.4 KiB
JavaScript
57 lines
2.4 KiB
JavaScript
// wasm/app/worker.mjs
|
|
// Tier-evaluation Web Worker. Keeps the main thread responsive so the
|
|
// live ms counter actually ticks AND so cancel works (main thread
|
|
// terminates this worker via worker.terminate()).
|
|
|
|
import { evalOnTier, setBendUrl, heapStats, portalSave, portalLoad } from "./runner.js";
|
|
|
|
self.onmessage = async (e) => {
|
|
const { kind } = e.data;
|
|
if (kind === "config") {
|
|
setBendUrl(e.data.bendUrl);
|
|
return;
|
|
}
|
|
if (kind === "heap") {
|
|
// Synchronous read — no eval is running because the worker is
|
|
// single-threaded and main thread only sends this between evals.
|
|
self.postMessage({ kind: "heap", runId: e.data.runId, tier: e.data.tier, stats: heapStats(e.data.tier) });
|
|
return;
|
|
}
|
|
if (kind === "portal-save") {
|
|
// Pulls the most-recent /tmp/<name>.portal out of the tier's
|
|
// MEMFS after the lisp side ran (portal-snapshot! NAME). Main
|
|
// thread encrypts and stuffs it into the vault.
|
|
const blob = portalSave(e.data.tier, e.data.name);
|
|
self.postMessage({ kind: "portal-save", runId: e.data.runId, name: e.data.name, blob });
|
|
return;
|
|
}
|
|
if (kind === "portal-load") {
|
|
// Hydrates MEMFS from a vault-decrypted blob so a subsequent
|
|
// (portal-load! NAME) eval finds the file ready.
|
|
const ok = portalLoad(e.data.tier, e.data.name, e.data.blob);
|
|
self.postMessage({ kind: "portal-load", runId: e.data.runId, name: e.data.name, ok });
|
|
return;
|
|
}
|
|
if (kind !== "eval") return;
|
|
const { runId, tier, src } = e.data;
|
|
try {
|
|
const output = await evalOnTier(
|
|
tier,
|
|
src,
|
|
(loadingTier) => {
|
|
self.postMessage({ kind: "loading", runId, tier: loadingTier });
|
|
},
|
|
// Stream every print/display from the tier to the main
|
|
// thread as it happens. Sync XHR inside bend!-call still
|
|
// blocks the worker, but displays BEFORE/AFTER the bend
|
|
// round-trip surface immediately instead of waiting for
|
|
// the whole eval to finish. Long demos feel alive.
|
|
(chunk) => {
|
|
self.postMessage({ kind: "chunk", runId, tier, chunk });
|
|
},
|
|
);
|
|
self.postMessage({ kind: "done", runId, output });
|
|
} catch (err) {
|
|
self.postMessage({ kind: "error", runId, message: err && err.message ? err.message : String(err) });
|
|
}
|
|
};
|