FEATURE: expose V8 ScriptCompiler::CachedData via Context#compile#413
Open
ursm wants to merge 1 commit into
Open
FEATURE: expose V8 ScriptCompiler::CachedData via Context#compile#413ursm wants to merge 1 commit into
ursm wants to merge 1 commit into
Conversation
Contributor
Author
|
For sequencing context: #412 (Module API) is the next planned PR but I'm holding it back until this one lands. The two share a lot of C++ surface (handle table, packet protocol, dispose ordering) so iterating patterns here once will be cheaper than rebasing #412 twice. Flagging in case it helps frame the review. |
a66648a to
f6eaa25
Compare
Adds Context#compile returning a MiniRacer::Script handle that can be re-run multiple times and exposes V8's per-script bytecode cache. Callers pass `cached_data:` to skip re-parsing on subsequent processes and opt in to `produce_cache: true` to read the freshly produced blob back via `script.cached_data` for persistence. The MiniRacer::V8_CACHED_DATA_VERSION_TAG constant exposes V8's CachedDataVersionTag() so callers can invalidate their cache when libv8-node is bumped. Safety constraints (documented in README and CHANGELOG): * produce_cache defaults to false; passing true from inside a host-fn callback raises MiniRacer::RuntimeError. V8's CreateCodeCache walks live isolate state and corrupts the parser when re-entered from a JS->Ruby->JS frame; warm the cache from the top level instead. * Cross-process reuse requires both processes to load byte-identical snapshot data via Snapshot#dump / Snapshot.load. Snapshot.new(src) is non-deterministic across processes, so feeding the same source string to both sides is not enough — the cache will be rejected. * Cross-process reuse is incompatible with Platform.set_flags!(:single_threaded). V8's single-threaded mode embeds process-local state in the cache blob; same-process reuse still works. TruffleRuby ships a shim that falls back to source replay since GraalJS has no equivalent per-script cache reachable from Polyglot::InnerContext. Refs rubyjs#411. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
f6eaa25 to
0329ecc
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
Implements #411 — exposes V8's
ScriptCompiler::CachedDataso callers can persist per-script bytecode cache and skip re-parsing large bundles on subsequent processes.API surface
MiniRacer::Context#compile(source, filename:, cached_data:, produce_cache:)→MiniRacer::ScriptScript#run— executes the compiled script; safe to call multiple timesScript#cached_data— bytecode blob (nil when the suppliedcached_data:was accepted; populated on initial compile or after rejection, only whenproduce_cache: truewas set)Script#cache_rejected?— boolean for cache-key invalidation telemetryScript#dispose/Script#disposed?— eager handle releaseMiniRacer::V8_CACHED_DATA_VERSION_TAG— module-level constant (populated on firstContext.new) wrappingv8::ScriptCompiler::CachedDataVersionTag(); mix into cache keys so a libv8-node bump invalidates blobs automaticallySafety constraints
Found while wiring this into a real embedder (capybara-simulated driving Discourse). All three are documented in the README; the first two are also enforced at runtime.
produce_cache:defaults tofalse. Passingtruefrom inside a host-fn callback raisesMiniRacer::RuntimeError. V8'sCreateCodeCachewalks live isolate state and corrupts the parser when re-entered from a JS → Ruby → JS frame; standalone repro at https://github.com/rubyjs/mini_racer/issues — TODO. Warm the cache from the top level instead.MiniRacer::Snapshot.new(src).dumpis non-deterministic across processes, so feeding the same source string to twoSnapshot.newcalls produces different blobs and V8 rejects every cached_data crossing that boundary. UseSnapshot#dump→ persist →Snapshot.load(bytes)instead.Platform.set_flags!(:single_threaded). V8's single-threaded mode embeds process-local state in the cache blob, so cached_data is always rejected when consumed in a fresh process. Same-process reuse (e.g. a Context pool) still works. Embedders that need both will need to disable:single_threadedfor the cache-producing / cache-consuming path.Design notes
Context dispose ordering:
State::~State()walksst.scriptsand resets eachv8::Persistent<v8::Script>under the existing Locker/Isolate::Scope beforeisolate->Dispose(). Handle table is owned per-State.Concurrency: compile/run/dispose RPCs go through the existing
rendezvousmutex path; the handle table is only touched from the V8 thread. The newState::in_callbackcounter is incremented inv8_api_callback(also V8-thread-only) and read insidev8_compile; no cross-thread access, no atomic needed.GC finalizer trade-off:
script_freedoes NOT send a dispose RPC — takingrr_mtxfrom a Ruby finalizer thread risks deadlock. Handles freed via finalizer rely onState::~State()walking the table at isolate teardown. Long-lived Contexts with many short-lived Scripts will accumulate handles untilContext#dispose. Documented in README;Script#disposeis available for eager release.CachedData buffer policy: input blob uses
BufferNotOwnedpointing at the ValueDeserializer's ArrayBuffer backing store (valid for the v8_compile call), avoiding a copy of potentially MB-sized blobs.Packet protocol: new tags
'K'(compile),'R'(run),'D'(dispose) added todispatch1.'C'was already taken by call, hence'K'for compile.Refs #411.