wasm: make JS WebAssembly.instantiate operand stack/heap tunable#20
Open
e-fu wants to merge 1 commit into
Open
wasm: make JS WebAssembly.instantiate operand stack/heap tunable#20e-fu wants to merge 1 commit into
e-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.
Comment on lines
+27
to
+29
| // 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. |
Comment on lines
+144
to
+147
| // 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. |
Comment on lines
+20
to
+24
| // 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. |
Comment on lines
+724
to
+732
| 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); | ||
| """) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.