perf(plugins): handle-based VM dispatch, native base64, and indexed content-API lookups#29
Merged
Merged
Conversation
…bench Adds two sections to the plugin sandbox bench so the marshaling and base64 hot paths have before/after evidence: - runHookFilter identity-filter sweep at 1 KB / 100 KB / 1 MB payloads, isolating the host->VM->host payload marshaling cost that publish.html filters pay on whole-page HTML. - bodyEncoding bytesToBase64/base64ToBytes encode+decode at 1 MB / 10 MB of deterministic pseudo-random bytes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…source Every PluginVm entry point (runRoute/runHookListener/runHookFilter/ runLoopFetch/runLoopPreview/runSchedule/runMediaAdapterCall/ runMediaUrlTransformer/updateSettings/runLifecycle/runMigrate) and the module-pack render()/preview() used to JSON.stringify the payload TWICE (value + source escaping) and splice it into a source string for ctx.evalCode — forcing the WASM-interpreted QuickJS tokenizer/parser to compile payload-sized JS per call. For publish.html filters that is the entire page HTML; for module-pack renders the accumulated children HTML re-marshals at every nesting level. Now: - vm.ts grabs persistent function handles for the bootstrap's __run* dispatchers right after the bootstrap evaluates (before the plugin bundle runs, so a plugin reassigning the globals cannot intercept host dispatch) and disposes them in dispose(). - eval.ts grows callString/callVoid (+ sync callStringSync for module packs) built on a shared settleResolved pipeline: arguments cross via ctx.newString (a memcpy) — no compile, no second stringify. The promise-poll + wall-clock deadline machinery is unchanged. - modulePackVm.ts renders through __renderModule/__previewModule handles. - evalVoid/evalString are gone; evalCode-based eval remains only for one-time setup (hook detection, pack init). Bench (plugin suite, same scenario code before/after): runHookFilter identity 1 KB: 37.2us -> measured after fix runHookFilter identity 100 KB: 1.71ms -> measured after fix runHookFilter identity 1 MB: 17.18ms -> measured after fix Adds pluginVmPayloadMarshaling.test.ts pinning byte-identical round-trips for quotes/backslashes/template syntax/U+2028/U+2029/control chars/lone surrogates, plus the dispatch-handle hijack guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bytesToBase64/base64ToBytes ran a chunked String.fromCharCode + btoa / atob + per-byte loop in the Bun host process on every binary body crossing the bridge (route uploads, binary fetch responses, crypto bridge, media adapters) — tens of ms and ~3x transient allocation per multi-MB crossing, all on the event loop. Both now go through native Buffer: encode wraps the existing bytes without copying; decode copies Buffer.from(b64, 'base64') into a fresh ArrayBuffer to preserve the documented no-sibling-view guarantee. Output is bit-identical — pinned by a new cross-check test against the pure-JS reference (the algorithm quickjs/bootstrap/base64.ts still uses inside the VM, where Buffer does not exist; that shim is untouched). Bench (plugin suite base64 rows): encode 10 MB 82.7ms before; decode 10 MB 7.0ms before — after numbers in the bench report. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…content.* resolveTableBySlug ran listDataTables (full scan + fields_json parse of every table) on EVERY cms.content.* api-call (twice for moveTable); it now uses a new getDataTableBySlug repository read — one indexed lookup via data_tables_slug_active_idx. handleContentTablesGet drops its second full scan (listDataTablesWithCounts with a per-table COUNT subselect) in favour of one countDataRows COUNT for the requested table plus the id->slug lookup the relation-field projection genuinely needs. The bulk handlers (updateMany validation/diff, deleteMany validation) read N rows with N sequential getDataRow round-trips; both now use a new getDataRowMany — ONE hydrated IN-list query built from the dialect-aware placeholder() helper. Iteration stays in input order, so first-bad-id error messages, per-row hook emission order, and reply shapes are unchanged. Also dedupes softDeleteDataTable's inline row-count guard onto countDataRows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dispatch
The architecture gate pinned the old eval-source dispatch pattern
(`__runSchedule(${JSON.stringify(scheduleId)})`). Dispatch now goes
through a persistent handle to the bootstrap's __runSchedule dispatcher
(callVoid in eval.ts) — same invariant, the host still never holds the
plugin's own handler function, only the bootstrap dispatcher.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ddd1230 to
81c0557
Compare
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
Plugin-bridge performance cluster from the audit behind #24 (stacked on
perf/critical-performance-fixes— merge that first). Three adversarially-verified findings. Wire format, plugin-visible semantics, and rendered output are unchanged (pinned by round-trip and cross-check tests).Benchmarks — before / after
Bench scenarios committed first (
scripts/bench/benches/plugin.ts: hook-filter payload sweep + base64 rows), before/after runs on identical bench code.(The hook-filter remainder is the VM-side
JSON.parse/stringifyinherent to the wire contract — the eliminated part is exactly the payload-sized WASM source compile + the host's double-stringify.)What changed
Host→VM dispatch compiled every payload as source (
quickjs/vm.ts,modulePackVm.ts): payloads were JSON.stringify'd twice and spliced into a code string forevalCode, making QuickJS (WASM-interpreted) tokenize/parse/compile payload-sized source per call — forpublish.htmlfilters that's the whole page HTML, and module-pack renders re-marshaled accumulated children HTML at every nesting level. Dispatch now goes through persistent function handles grabbed right after bootstrap (before plugin code runs — a plugin reassigning the dispatch globals can no longer intercept host dispatch, pinned by a hijack-guard test) via newcallString/callVoid/callStringSyncineval.ts: arguments cross viactx.newString(a memcpy), promise-poll/deadline machinery unchanged. Marshaling round-trip fidelity (quotes, backslashes, U+2028/9, control chars, lone surrogates) pinned byte-identical.Pure-JS base64 on the Bun side (
protocol/bodyEncoding.ts): chunkedString.fromCharCode+btoa/atob+per-byte loop ran for every binary body crossing the bridge (route uploads, binary fetch responses, media adapters). Now nativeBufferbase64 with the documented no-sibling-view guarantee preserved; the in-VM shim (quickjs/bootstrap/base64.ts) stays pure JS as it must. Outputs cross-checked byte-identical against the old implementation.cms.content.*re-listed ALL tables per api-call; bulk handlers were N+1 (host/handlers/content.ts):resolveTableBySlugnow uses a new indexedgetDataTableBySlug;updateMany/deleteManyvalidate ids with one IN-listgetDataRowMany(input-order error/emission semantics preserved);tables.getdrops its second full scan for onecountDataRows. Reply shapes byte-identical.Verification
bun teston this branch: 5,123 pass / 0 fail; build + lint clean;bootstrap:syncfreshness gate green.🤖 Generated with Claude Code