Skip to content

sandbox Phase 3 — precompile the WASM module once, instantiate per invocation #3297

Closed

Description

@os-zhuang

Part 3 of #3275 — see its Phase 3 section for the full spec. Blocked by #3296 (Phase 2).

Verified API (quickjs-emscripten@0.32.0): newVariant(baseVariant, { wasmModule }) where wasmModule?: OrLoader<WebAssembly.Module> — "Emscripten will instantiate the WebAssembly.Instance from this existing WebAssembly.Module." The compiled WebAssembly.Module is stateless bytecode; each newQuickJSWASMModule(variant) still allocates a fresh linear memory (isolation preserved) as long as wasmMemory is NOT also passed (that would share memory = the rejected model).

Change (packages/runtime/src/sandbox/quickjs-runner.ts), module-level once per process:

const wasmModule = await WebAssembly.compile(baseWasmBytes);      // compile ONCE
const FAST_VARIANT = newVariant(RELEASE_SYNC, { wasmModule });    // reuse across instantiations

then Phase 2's newQuickJSWASMModule() → newQuickJSWASMModule(FAST_VARIANT).

  • Confirm how to obtain baseWasmBytes once for the RELEASE_SYNC variant (variant wasmLocation; CustomizeVariantOptions.wasmBinary). Fallback: the Emscripten instantiateWasm(imports, onSuccess) hook via emscriptenModule — new WebAssembly.Instance(cachedModule, imports) per call.
  • If clean bytes access proves awkward in 0.32, use the instantiateWasm fallback; if neither is clean, this phase is deferrable — Phase 2 already removes the asyncify cost.

Acceptance criteria

  • Compiled module cached once; per-invocation instantiate only.
  • Isolation invariant asserted (RSS soak): fresh instance ⇒ fresh linear memory; RSS returns on dispose across N invocations.
  • Bench shows the compile-cost removal vs Phase 2.
  • Sandbox suite green unchanged.

Do NOT take the shared-singleton-module route (see #3275 §REJECTED — cross-tenant blast radius + RSS ratchet).

Refs: #3275 (parent) · #3296 (Phase 2).

🤖 Generated with Claude Code

Activity

  1. os-zhuang commented on Jul 19, 2026

    @os-zhuang
    ContributorAuthor

    Deferred (PR #3318, ADR-0102 D3). After verifying the quickjs-emscripten@0.32.0 API against the installed sync variant, precompile-once does not clear D3's own bar:

    • No clean, portable, edge-safe way to obtain the .wasm bytes. @jitl/quickjs-wasmfile-release-sync loads its wasm lazily via the variant's importModuleLoader (node/browser/workerd conditions). Precompiling needs the raw bytes first — sourcing them ourselves means a Node fs/fetch of emscripten-module.wasm (breaks the Workers/edge-safe design that motivated quickjs-emscripten over isolated-vm) or replicating emscripten's per-runtime locateFile (fragile). The instantiateWasm hook has the same problem.
    • Uncertain payoff. Engines already code-cache wasm compilation for identical bytes, so newQuickJSWASMModule() largely amortizes the compile already; D3 wouldn't remove the inherent per-invocation instantiate. Phase 2 (perf(runtime): drop asyncify — sandbox on the sync QuickJS variant (ADR-0102 D2, #3296) #3309) already shipped the dominant win — dropping asyncify.

    Revisit only if profiling shows per-invocation compile is a real hotspot and quickjs-emscripten exposes a portable precompiled-module handle. Until then, D1 (#3301) + D2 (#3309) stand on their own. Closed via #3318.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions