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_FreeRuntime aborted (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

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jul 19, 2026 5:18pm

Request Review

@github-actions github-actions Bot 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 added the allow-major label Jul 19, 2026 — with Claude
@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 main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

allow-major documentation Improvements or additions to documentation size/m tests tooling

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