Module: Capybara::Simulated::ScriptCache
- Defined in:
- lib/capybara/simulated/script_cache.rb
Overview
Process-wide + on-disk cache of V8 ‘ScriptCompiler::CachedData` blobs keyed by `(version_tag, sha256(body))`. V8Runtime feeds every `__csim_runScript` body through here so the second visit (and every subsequent process) skips the parse + JIT step on large app bundles — Discourse’s main chunk goes from ~140 ms of recompile per visit to a deserialize + run path.
Cross-process portability requires snapshot-bytes equality: ‘RustyRacer::Snapshot.new(source)` is non-deterministic, so the blobs we produce here are only consumable by another process if that process loads the SAME snapshot bytes via `Snapshot.load`. `V8Runtime.build_snapshot` handles that side.
**Produce path stays at top level.** ‘Context#compile(produce_ cache: true)` is unsafe inside a host-fn callback (V8’s ‘CreateCodeCache` corrupts the parser when re-entered — rusty_racer raises immediately). The host-fn consumer queues misses; `V8Runtime#call` drains the queue from top-level Ruby after each bridge call returns.
Constant Summary collapse
- DEFAULT_DIR =
File.join(ENV['HOME'] || '/tmp', '.cache', 'capybara-simulated', 'script-cache')
Class Method Summary collapse
- .dir ⇒ Object
-
.enabled? ⇒ Boolean
‘V8Runtime#call` drains `warm_pending!` after EVERY host-fn call (finds, polls, dispatch), so this is one of the hottest Ruby methods — memoize the env decision instead of re-reading ENV + allocating a string per call (the var is fixed per process).
- .flush! ⇒ Object
- .lookup(sha, version_tag, kind: :script) ⇒ Object
-
.queue_warm(ctx, sha, label, body, version_tag, kind: :script, stale: false) ⇒ Object
‘stale: true` = the caller HAD a blob and V8 rejected it (the snapshot bytes changed under the same version tag — a bridge edit rebuilds the snapshot, and `Snapshot.new` is non-deterministic).
- .store(sha, version_tag, blob, kind: :script) ⇒ Object
- .warm_pending! ⇒ Object
Class Method Details
.dir ⇒ Object
39 40 41 42 43 |
# File 'lib/capybara/simulated/script_cache.rb', line 39 def dir = ENV['CSIM_SCRIPT_CACHE_DIR'] || DEFAULT_DIR # `V8Runtime#call` drains `warm_pending!` after EVERY host-fn call (finds, # polls, dispatch), so this is one of the hottest Ruby methods — memoize # the env decision instead of re-reading ENV + allocating a string per # call (the var is fixed per process). |
.enabled? ⇒ Boolean
‘V8Runtime#call` drains `warm_pending!` after EVERY host-fn call (finds, polls, dispatch), so this is one of the hottest Ruby methods — memoize the env decision instead of re-reading ENV + allocating a string per call (the var is fixed per process).
44 45 46 47 |
# File 'lib/capybara/simulated/script_cache.rb', line 44 def enabled? return @enabled unless @enabled.nil? @enabled = !ENV['CSIM_SCRIPT_CACHE'].to_s.casecmp('off').zero? end |
.flush! ⇒ Object
112 113 114 115 116 117 118 |
# File 'lib/capybara/simulated/script_cache.rb', line 112 def flush! q = @lock.synchronize { @writer_q } return unless q done = Queue.new q.push([:sync, done]) done.pop end |
.lookup(sha, version_tag, kind: :script) ⇒ Object
49 50 51 52 53 54 55 56 57 |
# File 'lib/capybara/simulated/script_cache.rb', line 49 def lookup(sha, version_tag, kind: :script) return nil unless enabled? key = cache_key(sha, version_tag, kind) mem_hit = @lock.synchronize { @mem[key] } return mem_hit if mem_hit blob = File.binread(disk_path_for(sha, version_tag, kind)) rescue nil return nil unless blob @lock.synchronize { @mem[key] ||= blob } end |
.queue_warm(ctx, sha, label, body, version_tag, kind: :script, stale: false) ⇒ Object
‘stale: true` = the caller HAD a blob and V8 rejected it (the snapshot bytes changed under the same version tag — a bridge edit rebuilds the snapshot, and `Snapshot.new` is non-deterministic). The rejected blob must be evicted from `@mem`, or this guard would treat it as “already cached” and skip the re-produce forever: every visit then hits the stale blob, rejects, and full-parses —a silent, permanent loss of the bytecode cache (caught 2026-06-12: 113/113 rejects on the Discourse perf sample).
74 75 76 77 78 79 80 81 82 |
# File 'lib/capybara/simulated/script_cache.rb', line 74 def queue_warm(ctx, sha, label, body, version_tag, kind: :script, stale: false) return unless enabled? key = cache_key(sha, version_tag, kind) @lock.synchronize { @mem.delete(key) if stale return if @mem.key?(key) || @pending.key?(key) @pending[key] = {ctx: ctx, label: label, body: body, version_tag: version_tag, sha: sha, kind: kind} } end |
.store(sha, version_tag, blob, kind: :script) ⇒ Object
59 60 61 62 63 64 |
# File 'lib/capybara/simulated/script_cache.rb', line 59 def store(sha, version_tag, blob, kind: :script) return unless enabled? && blob key = cache_key(sha, version_tag, kind) @lock.synchronize { @mem[key] = blob } enqueue_disk_write(disk_path_for(sha, version_tag, kind), blob) end |
.warm_pending! ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/capybara/simulated/script_cache.rb', line 84 def warm_pending! # Fast path: `V8Runtime#call` drains here after every host-fn # call (including hot polling like `__settleGenGet` / # `__hasReadyTimer`), so the empty-queue branch must not pay # the mutex round-trip. `@pending.empty?` lock-free read is # safe — at worst a queued miss waits one extra `call`. return unless enabled? && !@pending.empty? pending = @lock.synchronize { p = @pending; @pending = {}; p } return if pending.empty? pending.each_value {|job| ctx = job[:ctx] next unless ctx.respond_to?(:compile) begin handle = if job[:kind] == :module ctx.compile_module(job[:body], filename: job[:label].to_s, produce_cache: true) else ctx.compile(job[:body], filename: job[:label].to_s, produce_cache: true) end blob = handle.cached_data handle.dispose store(job[:sha], job[:version_tag], blob, kind: job[:kind]) if blob rescue StandardError # Best effort: a body that fails to produce a cache # just keeps doing a parse-from-source next encounter. end } end |