Repository navigation
js: configurable wasm module/memory quotas, instantiateStreaming/compileStreaming - #341
Merged
Merged
Conversation
…ileStreaming Requested directly, not corpus-found: go-wasmcontainers asked whether go-webengine could validate their WebGPU stack's own WebAssembly layer - their own Go-compiled modules reach several hundred MiB, toward the wasm32 4 GiB address-space ceiling, which this engine's own hardcoded 32 MiB module cap and 16 MiB linear-memory cap (round 189) could never accommodate. First milestone of a longer roadmap (Dedicated Workers, SharedArrayBuffer/Atomics, OPFS, and WebGPU compute are each their own planned, larger future round). MaxWasmModuleBytes/MaxWasmMemoryBytes (new Engine fields): the compile-input-size and linear-memory-growth caps are now configurable, following this engine's own established convention exactly (MaxJSHeapBytes' own "zero selects a sane default" shape) - zero keeps today's unchanged behaviour (32 MiB / 16 MiB), any higher value raises the ceiling toward real module sizes. A byte value is rounded up to WebAssembly's own 64 KiB page size and clamped to the wasm32 ceiling (65536 pages = 4 GiB) - wazero's own WithMemoryLimitPages panics past that value, so the clamp is load-bearing, not cosmetic. There is deliberately no "disable" value for either cap: a module always has SOME finite bound, matching wazero's own API, which has no unlimited-size mode either. WebAssembly.compileStreaming/instantiateStreaming: did not exist at all before this change. Not genuine incremental streaming - this engine's own fetch() already fully buffers a response before resolving it (no .body ReadableStream exists anywhere), and wazero's own CompileModule([]byte) takes a complete byte slice regardless - but the JS-facing contract (Promise-based, accepting a Response or a promise of one, the overwhelmingly common real shape instantiateStreaming(fetch(url))) is satisfied exactly. Resolving the Response argument reuses a new small primitive, awaitSettled, which reads goja's own Promise.State()/Result() directly rather than driving real job-queue continuations - correct because every promise this engine's own code produces (including Response.arrayBuffer()) is already synchronously settled by construction. Tests cover the byte-to-page rounding/clamping math directly, the quota actually being consulted end-to-end, the real streaming round trip against a genuine fetch()-returned Response, and every error shape the Response-argument resolution can hit (a rejected promise, a promise resolving to a non-object, a direct non-promise object with no arrayBuffer() method, and arrayBuffer() itself throwing or returning a rejected promise) - each correctly rejecting with a TypeError, never panicking. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Nov 3, 2026
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.
Summary
Milestone 1 of the go-wasmcontainers WebGPU-readiness roadmap (requested
directly, not corpus-found): go-wasmcontainers asked whether go-webengine
could validate their WebGPU stack's own WebAssembly layer — their own
Go-compiled modules reach several hundred MiB, toward the wasm32 4 GiB
address-space ceiling, which this engine's own hardcoded 32 MiB module cap
and 16 MiB linear-memory cap (round 189) could never accommodate. Dedicated
Workers, SharedArrayBuffer/Atomics, OPFS, and WebGPU compute are each their
own planned, larger future milestone — not attempted here.
Engine.MaxWasmModuleBytes/MaxWasmMemoryBytes(new fields): thecompile-input-size and linear-memory-growth caps are now configurable,
following
MaxJSHeapBytes's own "zero selects a sane default" conventionexactly — zero keeps today's unchanged behaviour (32 MiB / 16 MiB), any
higher value raises the ceiling toward real module sizes. A byte value is
rounded UP to WebAssembly's own 64 KiB page size and clamped to the
wasm32 ceiling (65536 pages = 4 GiB) — wazero's own
WithMemoryLimitPagespanics past that value, so the clamp isload-bearing, not cosmetic. There is deliberately no "disable" value for
either cap: a module always has SOME finite bound, matching wazero's own
API, which has no unlimited-size mode either.
WebAssembly.compileStreaming/instantiateStreaming: did not existat all before this PR. Not genuine incremental streaming — this engine's
own
fetch()already fully buffers a response before resolving it (no.bodyReadableStream exists anywhere), and wazero's ownCompileModule([]byte)takes a complete byte slice regardless — but theJS-facing contract (Promise-based, accepting a
Responseor a promise ofone, the overwhelmingly common real shape
instantiateStreaming(fetch(url))) is satisfied exactly. A new smallprimitive,
awaitSettled, reads goja's ownPromise.State()/Result()directly rather than driving real job-queue continuations — correct
because every promise this engine's own code produces (including
Response.arrayBuffer()) is already synchronously settled byconstruction.
updated to reflect what's now closed vs. what round 189 already
disclosed and this PR doesn't touch).
Test plan
go test ./...— all green, no coverage-gate regressionTestMaxWasmModuleBytesResolution/TestMaxWasmMemoryPagesResolution:the byte-to-page rounding/clamping math directly, including past the
wasm32 ceiling
TestWasmModuleSizeQuotaEnforcedEndToEnd: the quota actuallyconsulted by the real
compile()path, not just the resolver inisolation
TestInstantiateStreamingAndCompileStreaming: the real streaminground trip against a genuine
fetch()-returnedResponse(a new/add.wasmcase on the sharednetServer()test fixture)TestStreamingWasmBytesRejectsVariousShapes: every error shape theResponse-argument resolution can hit — a rejected promise, a promise
resolving to a non-object, a direct non-promise object with no
arrayBuffer()method, andarrayBuffer()itself throwing orreturning a rejected promise — each correctly rejecting with a
TypeError, never panickinggo build ./... && go vet ./...clean;jspackage coverage netimproved (removed a provably-dead defensive branch the coverage
ratchet surfaced, closed the real gaps it also surfaced)
🤖 Generated with Claude Code