wasm: make JS WebAssembly.instantiate operand stack/heap tunable#20
wasm: make JS WebAssembly.instantiate operand stack/heap tunable#20e-fu wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR makes the JS WebAssembly.instantiate guest WASM operand stack and aux heap sizes configurable at the runtime/pool level (:wasm_stack_size / :wasm_heap_size, defaulting to 65_536), bringing it in line with the native QuickBEAM.WASM path which already supports caller-supplied sizing.
Changes:
- Thread new runtime/pool options (
:wasm_stack_size,:wasm_heap_size) through Elixir → Zig config carriers → JSWebAssembly.instantiateimplementation. - Replace hardcoded
65_536stack/heap literals in the JS instantiate path with per-context state values. - Add regression tests covering stack overflow at the default size and success with a raised
:wasm_stack_size, including the ContextPool path.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/wasm_test.exs | Adds JS WebAssembly.instantiate regression tests for default-stack overflow and raised :wasm_stack_size (runtime + pool). |
| lib/quickbeam/worker.zig | Passes runtime-configured WASM stack/heap sizes into the WASM JS install hook. |
| lib/quickbeam/wasm.ex | Clarifies docs: native WASM options vs JS instantiate runtime-level sizing. |
| lib/quickbeam/wasm_js.zig | Stores per-context WASM stack/heap sizes and uses them when starting managed instances from JS. |
| lib/quickbeam/types.zig | Extends RuntimeData with wasm_stack_size / wasm_heap_size defaults. |
| lib/quickbeam/runtime.ex | Allows :wasm_stack_size / :wasm_heap_size through to the runtime NIF opts. |
| lib/quickbeam/quickbeam.zig | Parses and bounds-checks wasm_stack_size / wasm_heap_size from Elixir opts for runtime + pool. |
| lib/quickbeam/context_worker.zig | Copies pool WASM sizing into each created context’s RuntimeData. |
| lib/quickbeam/context_types.zig | Extends PoolData with wasm_stack_size / wasm_heap_size defaults. |
| lib/quickbeam/context_pool.ex | Documents and forwards the new pool options to the NIF. |
| lib/quickbeam.ex | Documents new runtime options and corrects/aligns :max_stack_size default wording. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // WASM operand stack / heap for the JS `WebAssembly.instantiate` path | ||
| // (distinct from `max_stack_size`, the JS call stack). Default mirrors the | ||
| // WASM NIF path; raised via the runtime `:wasm_stack_size` opt. |
| // WASM operand stack / heap for the JS `WebAssembly.instantiate` path | ||
| // (distinct from `max_stack_size`, the JS call stack). Default mirrors the | ||
| // WASM NIF path; raised via the pool `:wasm_stack_size` opt. Copied into | ||
| // each context's RuntimeData at create time. |
| // WASM operand stack / auxiliary heap for instances started via the JS | ||
| // `WebAssembly.instantiate` path. Distinct from the JS call stack | ||
| // (`max_stack_size`). Default mirrors the WASM NIF path; a consumer raises | ||
| // it (via the runtime/pool `:wasm_stack_size` opt) for guests whose deep | ||
| // init would otherwise overflow the 64 KB default. |
| test "JS instantiate path honors a raised :wasm_stack_size" do | ||
| {:ok, rt} = QuickBEAM.start(wasm_stack_size: 8 * 1024 * 1024) | ||
|
|
||
| assert {:ok, 0} = | ||
| QuickBEAM.eval(rt, """ | ||
| const bytes = #{@stack_deep_wasm}; | ||
| const {instance} = await WebAssembly.instantiate(bytes); | ||
| instance.exports.rec(10000); | ||
| """) |
|
Thanks for the PR! I merged this locally onto current QUICKBEAM_BUILD=1 mix test test/wasm_test.exs --max-cases 1The new ContextPool test crashes in WAMR: So I think this needs changes before merge. The option threading looks reasonable, but the new pooled deep-recursion case currently exposes a native panic path rather than a safe JS/WASM error. Could you rebase onto current |
What
Makes the WASM operand stack and auxiliary heap for guests started via the
JavaScript
WebAssembly.instantiatepath tunable, via two new runtime-level options:wasm_stack_size/:wasm_heap_size(both default65536— behavior is unchangedunless a consumer opts in).
Why
The JS
WebAssembly.instantiatepath hardcoded a 64 KB operand stack (and 64 KB auxheap) when starting an instance, while the native
QuickBEAM.WASMNIF path alreadyaccepts caller-supplied
:stack_size/:heap_size. Guests with deep initialization(e.g. Go
GOOS=js) overflow the 64 KB operand stack at boot — the native path can bootthem, the JS path can't. This closes that parity gap.
The new values are threaded from the runtime/pool opts down to the instantiate site,
mirroring exactly how the existing
:max_stack_size(the JS call stack, a separate8 MB limit) is already plumbed. The standard
instantiate(bytes, importObject)JSsignature stays spec-faithful — no extra JS argument; the limit comes purely from
per-runtime config.
How
wasm_stack_size/wasm_heap_sizefields (default65_536) on the two configcarriers
RuntimeData(types.zig) andPoolData(context_types.zig), and on theper-context
ContextState(wasm_js.zig).(
worker.zig→wasm_js.install→ensure_context_state), the pool'sPoolData → RuntimeDatacopy (context_worker.zig), the opts parsing(
quickbeam.zigstart_runtime+pool_start), and the ElixirKeyword.takeallow-lists (
runtime.ex,context_pool.ex).wasm_js.zignow readsstate.wasm_stack_size/state.wasm_heap_sizeinstead of the65_536, 65_536literals.u64 → u32cast (std.math.cast): an out-of-rangevalue returns a controlled error instead of trapping (Debug) / wrapping (ReleaseFast).
Note: the pre-existing
max_convert_depth/max_convert_nodescasts in the samefunction share this pattern and were left unchanged to keep this diff focused.
QuickBEAM,QuickBEAM.Runtime,QuickBEAM.ContextPool, andQuickBEAM.WASM(clarifying the NIF path keeps its per-call:stack_size/:heap_size).Tests
test/wasm_test.exsadds a small recursiverec(n)fixture (WAMR keeps call frames onthe
stack_sizebuffer, so deep recursion is the canonical operand-stack-overflow repro)and three regression assertions:
rec(10000)raises a…stack…error,QuickBEAM.start(wasm_stack_size: 8 MB)→rec(10000)returns{:ok, 0},QuickBEAM.ContextPool(exercises the poolPoolData → RuntimeDatathreading path, which the standalone test doesn't cover).
Verification
Built and tested in the CI-pinned container (OTP 27.0 / Elixir 1.18.3 / Zig 0.15.2,
MIX_ENV=test, Debug): 56 tests, 0 failures,mix compileclean. The diff alsocompiles clean against this branch's base.