perf(runtime): drop asyncify — sandbox on the sync QuickJS variant (ADR-0102 D2, #3296)#3309
Merged
Merged
Conversation
…DR-0102 D2, #3296) Phase 2 of #3275. Switch the QuickJS sandbox from the asyncify build (newAsyncContext) to the already-installed sync release variant (newQuickJSWASMModule().newContext()), keeping one physically isolated WASM module per invocation (D2/D4). Asyncify's only justification — suspending the WASM stack across a host call — disappeared with the #1867 deferred-promise + pump redesign, so nothing depended on it. Wins: smaller binary, faster compile/instantiate + per-instruction, and removal of the suspended-stack failure class. - evalCodeAsync → evalCode; QuickJSAsyncContext → QuickJSContext throughout. - Disposal: the sync QuickJSWASMModule has no dispose() — dispose the context (frees its runtime), drop `mod` so GC reclaims the WASM instance + linear memory. Isolation invariant preserved. - Latent-leak fix surfaced by the stricter sync teardown: host ctx.api calls hand the VM a vm.newPromise() deferred that was never dispose()d (the newPromise contract requires it). Asyncify tolerated the leak; the sync build's JS_FreeRuntime aborted ("Assertion failed: list_empty") when a context was torn down with a pending never-settled host call (the timeout path). Deferreds are now tracked and disposed before context teardown. - New RSS soak test guards that per-invocation modules don't ratchet RSS (GC reclaims them). - ADR-0102 D2 updated with the verified disposal model. 58 sandbox + 574 runtime tests green; RSS soak green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011iJLjqToxNv1aYP3syQtRp
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 19, 2026 17:53
This was referenced Jul 19, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 19, 2026
…ation (#3297) (#3318) Phase 3 of #3275. Records ADR-0102 D3 as deferred: verifying quickjs-emscripten 0.32 shows no clean, portable, edge-safe way to obtain the sync variant's .wasm bytes to precompile (Node fs/fetch breaks Workers/edge-safety; replicating locateFile is fragile), and the payoff is uncertain (engines already code-cache wasm compilation). Phase 2 (#3309) already delivered the dominant win. D1 (#3301) + D2 (#3309) stand on their own; revisit criterion recorded. allow-major label bypasses the unrelated pre-existing console:major changeset on main. Closes #3297.
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.
Implements Phase 2 of #3275 (ADR-0102 D2). Closes #3296. Builds on Phase 1 (#3301, merged).
What & why
Switch the QuickJS sandbox from the asyncify build (
newAsyncContext) to the already-installed sync release variant (newQuickJSWASMModule().newContext()), keeping one physically isolated WASM module per invocation (D2/D4 — separate modules = separate linear memory, so a QuickJS heap bug can't reach another invocation's data). Asyncify's only justification — suspending the WASM stack across a host call — disappeared with the #1867 deferred-promise + pump redesign (installApiMethodalready usesvm.newPromise(), nevernewAsyncifiedFunction), so nothing depended on it.Wins: smaller WASM binary, faster compile/instantiate, faster per-instruction, and removal of the whole suspended-stack failure class the code previously avoided only by convention.
Changes
evalCodeAsync→evalCode;QuickJSAsyncContext→QuickJSContextthroughout; importnewQuickJSWASMModule.QuickJSWASMModulehas nodispose(). We dispose the context (QuickJSWASMModule.newContext: "the runtime will be disposed when the context is disposed") and dropmodso GC reclaims its WebAssembly instance + linear memory. Per-invocation isolation is preserved; reclamation is GC-timed rather than the async path's explicit module dispose.ctx.apicalls hand the VM avm.newPromise()deferred that was neverdispose()d (thenewPromisecontract requires it). The asyncify build tolerated the leak; the sync build'sJS_FreeRuntimeaborted (Assertion failed: list_empty) when a context was torn down with a pending, never-settled host call — i.e. the timeout path. Deferreds are now tracked and disposed before context teardown.quickjs-runner.rss.test.ts, ADR-0102 D2 guard): 300 back-to-back invocations must not ratchet RSS — proves natural GC reclaims the per-invocation modules.Verification
@objectstack/runtimefull suite — 574 passed (incl. the 58-test sandbox suite: re-entrancy, fan-out, lvl4 nesting, nested-write real-sqlite, tx rollback, timeout/wall-ceiling — all green on the sync engine).eslint,check:doc-authoring,@objectstack/runtimebuild (tsup DTS typecheck) all green.Notes
@objectstack/console: majorchangeset onmain(from a routine objectui bump) trips the no-major gate for every PR; per the Phase 1 decision this PR uses theallow-majorlabel to bypass it.🤖 Generated with Claude Code
Generated by Claude Code