Skip to content

Commit a016e27

Browse files
authored
docs(adr): ADR-0102 D3 (precompile WASM) — DEFERRED after API verification (#3297) (#3318)
Phase 3 of #3275. Records ADR-0102 D3 as deferred: verifying quickjs-emscripten 0.32 shows no clean, portable, edge-safe way to obtain the sync variant's .wasm bytes to precompile (Node fs/fetch breaks Workers/edge-safety; replicating locateFile is fragile), and the payoff is uncertain (engines already code-cache wasm compilation). Phase 2 (#3309) already delivered the dominant win. D1 (#3301) + D2 (#3309) stand on their own; revisit criterion recorded. allow-major label bypasses the unrelated pre-existing console:major changeset on main. Closes #3297.
1 parent 524696a commit a016e27

1 file changed

Lines changed: 9 additions & 3 deletions

File tree

docs/adr/0102-sandbox-cpu-budget-and-engine-variant.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0102: Sandbox Execution Budget & Engine Variant — CPU-time budget with a wall ceiling; per-invocation sync WASM modules, precompiled
22

3-
**Status**: Accepted (2026-07-19) — **D1 landed** with Phase 1 (#3295): CPU-time budget + wall ceiling in `QuickJSScriptRunner`, nested-write integration tests now green at the stock 250ms budget. D2 (#3296, drop asyncify) and D3 (#3297, precompile) remain **Proposed**; D3 is explicitly deferrable without weakening D1/D2.
3+
**Status**: Accepted (2026-07-19) — **D1 + D2 landed**: Phase 1 (#3295 / PR #3301) shipped the CPU-time budget + wall ceiling (nested-write integration green at the stock 250ms budget); Phase 2 (#3296 / PR #3309) dropped asyncify for the sync variant with per-invocation isolation. **D3 (#3297, precompile) is DEFERRED** — verifying the 0.32 API showed no clean, portable, edge-safe way to obtain the variant's `.wasm` bytes to precompile (see D3), and it does not weaken D1/D2.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: the #1867 sandbox redesign (deferred-promise host calls + pump loop) — the mechanism that both retired asyncify's only remaining justification (D2) and created the discrete VM-entry points D1 meters. No earlier ADR covers the sandbox runner; prior art is code history (#3232 honor body `timeoutMs`, #3264 test de-flake, #3270 env-overridable defaults).
66
**Tracking**: framework#3275 (implementation-ready spec) · #3295 / #3296 / #3297 (phases) · #3259 (motivating CI flake, closed by the #3270 stopgap)
@@ -51,9 +51,15 @@ Switch `newAsyncContext()` → `newQuickJSWASMModule().newContext()` (the sync r
5151
- Disposal (verified against 0.32): `QuickJSWASMModule.newContext()` docs "the runtime will be disposed when the context is disposed", and — unlike the async convenience — the sync `QuickJSWASMModule` exposes **no `dispose()`**. So we `dispose()` the context (frees its runtime + QuickJS allocations), then drop the module reference; the module's WebAssembly instance + linear memory are reclaimed by **GC**. This is the library's intended sync pattern (only `newContext()` on a *shared* module shares memory), and it preserves the per-invocation-isolation invariant — but reclamation is GC-timed rather than the async path's explicit module dispose. **This is the one behavioural delta of D2 and MUST be gated by the RSS soak** (see Verification): burst hook-storm load must not ratchet RSS.
5252
- Wins: no asyncify state machine on every call (per-instruction speedup), smaller binary, faster compile/instantiate, and removal of an entire class of suspended-stack failure modes the current code only avoids by convention.
5353

54-
### D3 — Compile the WASM once; instantiate per invocation (deferrable)
54+
### D3 — Compile the WASM once; instantiate per invocation — DEFERRED
5555

56-
Cache one compiled `WebAssembly.Module` per process and build a customized variant via `newVariant(RELEASE_SYNC, { wasmModule })` (verified in 0.32: *"Emscripten will instantiate the WebAssembly.Instance from this existing WebAssembly.Module"*), so each invocation pays only instantiation — fresh linear memory, shared stateless bytecode. The `instantiateWasm` Emscripten hook is the lower-level fallback. **`wasmMemory` must never be passed** — injecting a shared memory would silently recreate the rejected D4 shape. If clean access to the variant's `.wasm` bytes proves awkward, D3 defers without weakening D1/D2.
56+
**Decision: deferred**, after verifying the 0.32 API against the installed variant. The intent held — cache one compiled `WebAssembly.Module` and build a customized variant via `newVariant(RELEASE_SYNC, { wasmModule })` so each invocation pays only instantiation (fresh linear memory, shared stateless bytecode) — but it does not clear the bar D3 set for itself:
57+
58+
- **No clean, portable, edge-safe way to obtain the bytes.** `@jitl/quickjs-wasmfile-release-sync` loads its `.wasm` lazily through the variant's own `importModuleLoader` (with `node` / `browser` / `workerd` export conditions). Precompiling requires the raw bytes first; sourcing them ourselves means either a Node `fs`/fetch of `emscripten-module.wasm` — which breaks the Workers/edge-safe design that motivated choosing quickjs-emscripten over isolated-vm — or replicating emscripten's per-runtime `locateFile` (fragile, bundler-specific). The `instantiateWasm` hook has the same bytes-sourcing problem.
59+
- **Uncertain payoff.** JS engines already code-cache WebAssembly compilation for identical bytes, so repeated `newQuickJSWASMModule()` largely amortizes the *compile* already; D3 removes only that, not the inherent per-invocation *instantiate* (fresh memory), which stays. Phase 2 already delivered the dominant win — dropping the asyncify build.
60+
- **`wasmMemory` must never be passed** — injecting a shared memory would silently recreate the rejected D4 shape.
61+
62+
Revisit only if profiling shows per-invocation compile is a real hotspot AND quickjs-emscripten exposes a portable precompiled-module handle. Until then, D1 (CPU budget) and D2 (sync variant) stand on their own.
5763

5864
### D4 — No shared singleton module (rejected alternative: shared module + per-invocation contexts)
5965

0 commit comments

Comments
 (0)