lumbda/www/repl/c/lumbda-c.loader.js
russell@unturf.com ff2ef382c7
gc diagnostics: per-tier heap pressure surfaced in repl tabbar
Step 1 of the GC effort. Each tier loader now exposes heapStats():
  - asm-wasm — lumbda_heap_used / lumbda_heap_total wat exports
  - c-wasm   — emscripten linear memory size (no free path right now,
               so used = total; documented in the loader)
  - python   — pyodide module linear memory size; CPython GC cycles
               this naturally

Worker handles a "heap" message kind that round-trips the active tab's
loaded tiers; repl tabbar shows a compact "py 12M · c 32M · asm 4M"
strip next to the buttons. Polls every 2s.

Doesn't solve the leak — just makes pressure visible so the user knows
when to use "reboot tier". Real GC (Cheney over the WAT bump allocator,
Boehm-em or custom mark-sweep for c-wasm) coming next.
2026-06-14 17:32:48 -04:00

64 lines
2.3 KiB
JavaScript

// wasm/c/lumbda-c.loader.js
// C tier loader — Emscripten ES module factory.
//
// Exports createCTier({ baseURL }) -> Promise<{ evalLisp(src) -> Promise<string> }>.
// Output capture: Emscripten routes stdout/stderr through Module.print /
// Module.printErr callbacks. We accumulate per-eval and return joined.
async function _bootstrap() {
// Use import.meta.url so paths resolve relative to THIS loader file —
// not the caller. Works in both window and Worker contexts because both
// have a defined import.meta.url for ES modules.
const factoryURL = new URL("./lumbda-c.js", import.meta.url).href;
const wasmDir = new URL("./", import.meta.url).href;
const { default: createLumbdaC } = await import(/* @vite-ignore */ factoryURL);
let outBuf = [];
let errBuf = [];
const module = await createLumbdaC({
locateFile: (p) => wasmDir + p,
print: (line) => outBuf.push(line),
printErr: (line) => errBuf.push(line),
});
const _init = module.cwrap("lumbda_wasm_init", null, []);
const _eval = module.cwrap("lumbda_wasm_eval", "number", ["string"]);
const _free = module.cwrap("lumbda_wasm_free_result", null, ["number"]);
_init();
return {
async evalLisp(src) {
outBuf = [];
errBuf = [];
const errPtr = _eval(src);
let errMsg = "";
if (errPtr) {
errMsg = module.UTF8ToString(errPtr);
_free(errPtr);
}
let out = outBuf.join("\n");
if (out) out += "\n";
if (errBuf.length) out += errBuf.join("\n") + "\n";
if (errMsg) out += errMsg + "\n";
return out;
},
heapStats() {
// Emscripten exposes the linear memory directly. There's no
// free path right now (LUMBDA_NO_BOEHM), so used = total —
// every malloc accumulates until reload. Documented and
// surfaced in the REPL so the user sees the pressure.
const total = module.HEAPU8.byteLength;
return { used: total, total };
},
};
}
// Closure-encapsulated singleton (see lumbda-c.loader.js for rationale).
export const createCTier = (() => {
let tier = null;
return async () => {
if (!tier) tier = await _bootstrap();
return tier;
};
})();