Skip to content

perf(runtime): drop asyncify — sandbox on the sync QuickJS variant (ADR-0102 D2, #3296) - #3309

Merged
os-zhuang merged 1 commit into
mainfrom
claude/nested-write-sandbox-timeout-muw13i
Jul 19, 2026
Merged

perf(runtime): drop asyncify — sandbox on the sync QuickJS variant (ADR-0102 D2, #3296)#3309
os-zhuang merged 1 commit into
mainfrom
claude/nested-write-sandbox-timeout-muw13i

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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 (installApiMethod already uses vm.newPromise(), never newAsyncifiedFunction), 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

  • evalCodeAsyncevalCode; QuickJSAsyncContextQuickJSContext throughout; import newQuickJSWASMModule.
  • Disposal (verified against 0.32): the sync QuickJSWASMModule has no dispose(). We dispose the context (QuickJSWASMModule.newContext: "the runtime will be disposed when the context is disposed") and drop mod so 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.
  • 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). The asyncify build tolerated the leak; the sync build's JS_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.
  • New RSS soak test (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.
  • ADR-0102 D2 updated with the verified disposal model.

Verification

  • @objectstack/runtime full 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).
  • RSS soak green (per-invocation modules reclaimed; no ratchet).
  • eslint, check:doc-authoring, @objectstack/runtime build (tsup DTS typecheck) all green.

Notes

  • Behaviour and error strings are unchanged from Phase 1 — this is an engine/perf swap + a latent-leak fix, no API surface change.
  • The pre-existing unrelated @objectstack/console: major changeset on main (from a routine objectui bump) trips the no-major gate for every PR; per the Phase 1 decision this PR uses the allow-major label to bypass it.
  • Follow-up: Phase 3 sandbox Phase 3 — precompile the WASM module once, instantiate per invocation #3297 (precompile the WASM module once).

🤖 Generated with Claude Code


Generated by Claude Code

…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
@vercel

vercelBot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specBuildingBuildingPreview, CommentJul 19, 2026 5:18pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/index.mdx(via @objectstack/runtime)
  • content/docs/api/wire-format.mdx(via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx(via packages/runtime)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/runtime)
  • content/docs/deployment/index.mdx(via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx(via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx(via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx(via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx(via packages/runtime)
  • content/docs/plugins/packages.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx(via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 19, 2026 17:53
@os-zhuang
os-zhuang merged commit a2d6555 into mainJul 19, 2026
17 of 19 checks passed
@os-zhuang
os-zhuang deleted the claude/nested-write-sandbox-timeout-muw13i branch July 19, 2026 17:54
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

allow-majordocumentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sandbox Phase 2 — drop asyncify (sync QuickJS variant), keep per-invocation modules

2 participants

@os-zhuang@claude