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

Class Method Details

.dirObject



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).

Returns:

  • (Boolean)


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