Skip to content

fix(wasm): bound warm-instance linear memory by recycling#369

Open
skhaz wants to merge 1 commit into
mainfrom
fix/wasm-instance-leak
Open

fix(wasm): bound warm-instance linear memory by recycling#369
skhaz wants to merge 1 commit into
mainfrom
fix/wasm-instance-leak

Conversation

@skhaz

@skhaz skhaz commented Jun 19, 2026

Copy link
Copy Markdown
Member

Why

A reused warm WASM component instance (per inline worker) keeps its linear memory alive across calls; WASM linear memory only ever grows, so each conversion's guest allocations accumulate without bound. Under sustained document conversion, RSS climbed past 6 GB and the Go heap past 2 GB (~62 KB retained per call for an image-heavy PDF).

What

Recycle the warm instance after recycleWarmInstanceAfter (512) synchronous calls: close it so the next call instantiates a fresh one and reclaims the linear memory, while keeping the fast warm path for the common case. Re-instantiation is now safe because wasm-runtime is bumped to a version that rebuilds the per-instance import bridges, so a fresh instance no longer reuses a bridge bound to a closed instance's core.

  • runtime/wasm/engine/process.go: warmCalls counter + recycle in stepSync.
  • go.mod: bump wasm-runtimecc678f6 (per-instance bridge rebuild).
  • regression test TestProcessRecyclesWarmInstanceAfterThreshold.

Validation (10,000× sustained convert loop)

before after
RSS → ~6 GB stable ~280 MB
Go heap (alloc) → ~2.2 GB 38–57 MB band
heap_objects ~flat
conversions trap on 2nd+ 10000/10000 ok

golangci-lint clean; full ./runtime/wasm/... suite green under -race; full make test-runtime green; independent code review verdict: ship.

Follow-up (not in this PR)

Re-instantiation recompiles the synthetic bridge and its wazero CompiledModule is not freed on Close (~42 objects per re-instantiation). Recycling amortizes this to ~0.07 obj/call (negligible), but a complete fix would free the bridge CompiledModule on Close in wasm-runtime. recycleWarmInstanceAfter (512) could be made env-tunable.

Why:
The inline pool keeps one warm component instance alive across calls; WASM
linear memory only ever grows, so each call's guest allocations accumulate
without bound (~62KB/call for an image-heavy PDF; RSS climbing past 6GB and the
Go heap past 2GB under sustained conversion).

What:
Recycle the warm instance after recycleWarmInstanceAfter (512) synchronous
calls: close it so the next call instantiates a fresh one and reclaims the
linear memory, keeping the fast warm path for the common case. Re-instantiation
is now safe because wasm-runtime is bumped to a version that rebuilds the
per-instance import bridges, so a fresh instance no longer reuses a bridge bound
to a closed instance's core. Adds a regression test asserting the instance is
recycled at the threshold.
@skhaz skhaz requested a review from wolfy-j June 19, 2026 22:27

@wolfy-j wolfy-j left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need better api

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants