mini_racer_rs 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfd3987a3aebfd95013e67e552b2041b991fd7b0da4933f98ae92335f7caa3a4
4
- data.tar.gz: 51ccdf38b39ab3f33f771f3cf7fdbd7422bda16037e8bc026cfd64dc6fdfbdae
3
+ metadata.gz: 7a8339f7ec9dcc06bd136d68328c3b5a8acd48bcb59061fe0e1de97d8163e7a5
4
+ data.tar.gz: fdb040a4e14239cce537be6724a36410432093b6f6ca0f62d6eec5830d32c6b5
5
5
  SHA512:
6
- metadata.gz: ca5321d48ea8913f86d8966b6f475857d11b962c1f109e15f049fed590a517233f2cc3772fad99a9ff189f110995d3b2a0268f54e9f90aa48fcdd3cc1ac2b916
7
- data.tar.gz: 3c2a6c804148cfd0bdf198d5a63ef4b440d63c667d7468ab646e8b85b277eb15ec0fa26f5d9877f426c01427e8f841723cf31d00bec0ee546881c895d373c61b
6
+ metadata.gz: e0fa4008ea43a6f4b7b23a5d324d8c58b659da2688243681b59ddb0af21b16fb54bb2677b9952f24381df1965c66ae47722f1c93d5e82d53b89748c2d7e8a60f
7
+ data.tar.gz: 0b8bb09c4ca879533aa582fc800c9779b56326dc1c52c61d9d78a95132efa96f0898d596a550255d77839c6c51d09695dfe042021b5992141d5dcb3bbbcc57c8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.4](https://github.com/andymai/mini_racer_rs/compare/v1.0.3...v1.0.4) (2026-10-19)
4
+
5
+
6
+ ### Performance
7
+
8
+ * **engine:** share a large source from its first evaluation ([#91](https://github.com/andymai/mini_racer_rs/issues/91)) ([31425e1](https://github.com/andymai/mini_racer_rs/commit/31425e1fc72083fd1c6564d5f50d30b8e2d746aa))
9
+
3
10
  ## [1.0.3](https://github.com/andymai/mini_racer_rs/compare/v1.0.2...v1.0.3) (2026-10-18)
4
11
 
5
12
 
data/COMPATIBILITY.md CHANGED
@@ -182,7 +182,7 @@ Approved 2026-10-16.
182
182
 
183
183
  ### `behavior:shared-source-heap-stats@ruby`
184
184
 
185
- `Context#heap_stats` can report large source text differently from mini_racer 0.22.1. When a context evaluates a source of 1 MiB or more that the process has evaluated before under the same file name and startup snapshot, mini_racer_rs gives V8 one copy shared by every such context and held outside V8's heap. It reports that source in `:external_memory` and omits it from `:used_heap_size`. V8 counts the shared source by its length in characters, so two-byte text reports half its bytes. mini_racer 0.22.1 reports `:external_memory` as 0 and counts each context's own copy in `:used_heap_size`. JavaScript sees the same source text in both gems. Contexts using `max_memory:` and sources that are not valid UTF-8 retain individual copies. Setting `MINI_RACER_RS_CODE_CACHE=off` before creating the first context disables sharing and makes `heap_stats` count every source as mini_racer does.
185
+ `Context#heap_stats` reports shared sources differently from mini_racer 0.22.1: when a context evaluates a source of 1 MiB or more, mini_racer_rs includes it in `:external_memory` and excludes it from `:used_heap_size`. mini_racer_rs gives V8 one copy held outside V8's heap, shared starting with the first evaluation by every context using the same source, file name, and startup snapshot. The process shares at most 8 such sources and 256 MiB of them. Past that bound, a context evaluating another large source keeps its own copy, and its `heap_stats` count that source as mini_racer's do. V8 counts shared sources in `:external_memory` by character length, so two-byte text reports half its bytes. mini_racer 0.22.1 reports `:external_memory` as 0 and counts each context's own copy in `:used_heap_size`. JavaScript sees the same source text in both gems. Contexts created with `max_memory:` and sources that are not valid UTF-8 keep their own copies. Setting `MINI_RACER_RS_CODE_CACHE=off` before the first context prevents source sharing and makes `heap_stats` count every source as mini_racer does.
186
186
 
187
187
  Approved 2026-10-18.
188
188
 
data/Cargo.lock CHANGED
@@ -458,7 +458,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
458
458
 
459
459
  [[package]]
460
460
  name = "mini_racer_engine"
461
- version = "1.0.3"
461
+ version = "1.0.4"
462
462
  dependencies = [
463
463
  "deno_core_icudata",
464
464
  "libc",
@@ -470,7 +470,7 @@ dependencies = [
470
470
 
471
471
  [[package]]
472
472
  name = "mini_racer_rs"
473
- version = "1.0.3"
473
+ version = "1.0.4"
474
474
  dependencies = [
475
475
  "magnus",
476
476
  "mini_racer_engine",
@@ -481,7 +481,7 @@ dependencies = [
481
481
 
482
482
  [[package]]
483
483
  name = "mini_racer_sync"
484
- version = "1.0.3"
484
+ version = "1.0.4"
485
485
  dependencies = [
486
486
  "loom",
487
487
  "thiserror",
@@ -489,7 +489,7 @@ dependencies = [
489
489
 
490
490
  [[package]]
491
491
  name = "mini_racer_wire"
492
- version = "1.0.3"
492
+ version = "1.0.4"
493
493
  dependencies = [
494
494
  "proptest",
495
495
  "thiserror",
data/Cargo.toml CHANGED
@@ -14,7 +14,7 @@ license = "MIT"
14
14
  publish = false
15
15
  repository = "https://github.com/andymai/mini_racer_rs"
16
16
  rust-version = "1.91"
17
- version = "1.0.3"
17
+ version = "1.0.4"
18
18
 
19
19
  [workspace.dependencies]
20
20
  deno_core_icudata = "=0.78.0"
data/README.md CHANGED
@@ -21,10 +21,10 @@ mini_racer_rs implements the `MiniRacer` Ruby API from mini_racer 0.22.1, with f
21
21
 
22
22
  ## Highlights
23
23
 
24
- - **4 to 5 times faster Ruby round trips.** An eval, call, or callback round trip takes 1.3 to 1.6 µs, against 6.1 to 6.6 µs in mini_racer. A call on a context with `timeout:` takes 1.6 µs instead of 21.1 µs because one watchdog serves the process.
25
- - **1.9 times faster Babel transforms.** A transform through ExecJS takes 5.3 ms instead of 10.0 ms. Babel itself takes 5.3 ms inside V8 15.2, against 10.3 ms inside V8 13.6. Terser and React rendering take about the same time in both gems.
26
- - **4.6 times faster production bundle loads.** Scripts of 1 MB or more compile from a process-wide V8 code cache. A 44 MB production server rendering bundle loads into a new context in 215 ms against 983 ms. The 8.2 MB MUI bundle loads and renders once in 121 ms against 167 ms, and startup plus Babel takes 14.8 ms against 44.8 ms.
27
- - **Less memory for loaded and abandoned contexts.** A context holding the 44 MB bundle takes 136 MB against 301 MB, and one holding the MUI bundle takes 56 MB against 110 MB. When 20,000 contexts are left to Ruby's garbage collector, resident memory peaks at 102 MB against 2,916 MB.
24
+ - **JavaScript round trips are 5 to 9 times faster.** An eval takes 1.09 µs against 6.91 µs, a call takes 0.72 µs against 6.51 µs, and a callback takes 1.12 µs against 6.12 µs. A call on a context with `timeout:` takes 0.76 µs instead of 20.48 µs because one watchdog serves the process.
25
+ - **A Babel transform through ExecJS is 1.95x faster.** It takes 5.15 ms instead of 10.05 ms. Babel itself takes 5.3 ms inside V8 15.2, against 10.3 ms inside V8 13.6, while Terser and React rendering take about the same time in both gems.
26
+ - **A 44 MB production server rendering bundle loads 4.6x faster.** It loads into a new context in 215 ms against 983 ms, measured at commit `2513a48`, and scripts of 1 MB or more compile from a process-wide V8 code cache. At commit `220bfe7`, the 8.2 MB MUI bundle loads and renders once in 105 ms against 167 ms, while startup plus Babel takes 13.3 ms against 44.4 ms.
27
+ - **A context holding the MUI bundle uses 44 MB instead of 110 MB.** At commit `220bfe7`, a context holding Babel uses 15.2 MB against 28.5 MB, and contexts loading the same large script read one shared copy of its source. A context holding the 44 MB production bundle used 136 MB against 301 MB at commit `2513a48`, before sources were shared. When 20,000 contexts are left to Ruby's garbage collector, resident memory peaks at 102 MB against 2,920 MB.
28
28
  - **Forks pause worker work, and children use their own threads.** For forks made through Ruby's `fork`, `IO.popen("-")`, or `Process.daemon`, mini_racer_rs waits for context runners, pauses its worker pool, and waits for running tasks; an async `WebAssembly.compile` in a new default-mode child completes on the child's own pool. On Ubuntu 24.04 with glibc 2.39, 20 of 20 mini_racer children aborted with signal 6 after loading and rendering React, while 20 of 20 mini_racer_rs children exited cleanly. On Fedora, mini_racer children hung within the first 50 forks of `test/stress/fork_busy.rb`, while mini_racer_rs completed 300 forks per run on Fedora and Ubuntu.
29
29
  - **V8 15.2.124.1.** mini_racer 0.22.1 uses V8 13.6.233.17.
30
30
  - **No compiler at release install.** Native gems cover glibc Linux on x86_64 and aarch64, plus macOS on arm64 and x86_64. Each carries extensions for Ruby 3.3, 3.4, and 4.0. mini_racer 0.22.1 ships as source and compiles C++ against libv8-node.
@@ -156,21 +156,27 @@ gem "mini_racer", github: "andymai/mini_racer_rs", tag: "vX.Y.Z", glob: "shim/*.
156
156
 
157
157
  The repository tests shim resolution with `path:` sources and rtlcss in `test/consumers/transitive/`. The `github:` form has not been run end to end. A dependency whose `mini_racer` requirement excludes version 0.22.1 cannot use the shim.
158
158
 
159
+ ### Forking servers such as Puma
160
+
161
+ Prime the process-wide V8 code cache before forking by evaluating each bundle of 1 MB or more twice in the master, using exactly the source text and file name the renderer uses, then dispose those contexts. The second evaluation builds the cache, later matching evaluations compile from it, and forked children inherit it. For Babel's 2.3 MB bundle at commit `5e13c1c`, a forked child's first load took, as medians across 5 trials, 60.8 ms when the parent had not loaded it, 65.5 ms after one load, and 25.0 ms after two loads. You can prime a `react-rails` app by building its server renderer twice. Parent contexts are not used in children. Only the cache carries over.
162
+
163
+ With `mini_racer` 0.22.1, each worker compiles the bundle from source on its first load because there is no code cache.
164
+
159
165
  ## Benchmarks
160
166
 
161
167
  | Workload | mini_racer 0.22.1 | mini_racer_rs | Ratio |
162
168
  |---|---:|---:|---:|
163
- | Babel transform of a 3 KB JSX component, through ExecJS | 10.0 ms | 5.3 ms | 1.9x |
164
- | Terser minify of a 78 KB library, through ExecJS | 51.6 ms | 51.2 ms | 1.0x |
165
- | React server render of 1,000 items (17 KB of HTML), through ExecJS | 192 µs | 174 µs | 1.1x |
166
- | React in a new context: load the bundle and render 1,000 items, through ExecJS | 12.0 ms | 12.1 ms | 1.0x |
167
- | Handlebars render of 50 rows, through `Context#call` | 47.9 µs | 40.1 µs | 1.2x |
168
- | Autoprefixer on Bootstrap's 280 KB stylesheet, through ExecJS | 27.2 ms | 23.7 ms | 1.1x |
169
- | MUI dashboard server render of 200 rows and 299 icons (469 KB of HTML), through ExecJS | 32.2 ms | 30.5 ms | 1.1x |
170
- | MUI in a new context: load the 8.2 MB bundle and render once, through ExecJS | 167 ms | 121 ms | 1.4x |
169
+ | Babel transform of a 3 KB JSX component, through ExecJS | 10.1 ms | 5.2 ms | 2.0x |
170
+ | Terser minify of a 78 KB library, through ExecJS | 51.1 ms | 52.0 ms | 1.0x |
171
+ | React server render of 1,000 items (17 KB of HTML), through ExecJS | 191 µs | 174 µs | 1.1x |
172
+ | React in a new context: load the bundle and render 1,000 items, through ExecJS | 11.9 ms | 11.8 ms | 1.0x |
173
+ | Handlebars render of 50 rows, through `Context#call` | 49.1 µs | 34.9 µs | 1.4x |
174
+ | Autoprefixer on Bootstrap's 280 KB stylesheet, through ExecJS | 26.3 ms | 23.7 ms | 1.1x |
175
+ | MUI dashboard server render of 200 rows and 299 icons (469 KB of HTML), through ExecJS | 32.6 ms | 29.8 ms | 1.1x |
176
+ | MUI in a new context: load the 8.2 MB bundle and render once, through ExecJS | 167 ms | 105 ms | 1.6x |
171
177
  | Load a 44 MB production server rendering bundle into a new context, through ExecJS | 983 ms | 215 ms | 4.6x |
172
178
 
173
- The table reports the eight library workloads plus the 44 MB production server rendering bundle load. Results are medians of 5 runs from the release build of commit `2513a48`, with every workload producing identical output in both gems. They were measured on Ruby 3.4.2 and Linux on an AMD Ryzen 9 7950X3D, with the CPU governor set to `performance` and both gems pinned to CPUs 0 through 7.
179
+ Results are medians of 5 runs, with every workload producing identical output in both gems. The eight library workloads use the release build of commit `220bfe7`, and the 44 MB production server rendering bundle load uses commit `2513a48`. Measurements ran on Ruby 3.4.2 and Linux on an AMD Ryzen 9 7950X3D, with the CPU governor set to `performance` and both gems pinned to CPUs 0 through 7.
174
180
 
175
181
  ```sh
176
182
  bundle exec ruby bench/run.rb --cpus 0-7
@@ -21,8 +21,9 @@ const SAMPLE_POINTS: usize = 8;
21
21
  /// compiling it again.
22
22
  ///
23
23
  /// A script is cached the second time the process sees it. The first sighting
24
- /// costs a sampled digest, not a full one or a cache, as a large script
25
- /// evaluated once (per-request data, say) gains nothing from either.
24
+ /// costs a sampled digest and no cache, as a large script evaluated once
25
+ /// (per-request data, say) gains nothing from one; it costs the full digest
26
+ /// too only in a context that reads the script from its shared source.
26
27
  #[derive(Default)]
27
28
  pub struct CodeCache {
28
29
  // Most recently used last.
@@ -77,8 +78,9 @@ impl Drop for Building {
77
78
 
78
79
  /// How the runner compiles a large script.
79
80
  pub(crate) enum Plan {
80
- /// From source: a first sighting or the cache is off, or with its key, a
81
- /// key being built elsewhere or a refused cache.
81
+ /// From source: the cache is off or a first sighting, or with its key, a
82
+ /// first sighting whose source is shared, a key being built elsewhere or
83
+ /// a refused cache.
82
84
  Compile(Option<Key>),
83
85
  /// From source, then a cache made once the dispatch is over.
84
86
  Make(Building),
@@ -87,7 +89,7 @@ pub(crate) enum Plan {
87
89
  }
88
90
 
89
91
  impl Plan {
90
- /// The key of a script the process has seen before.
92
+ /// The script's key, unless the plan needed none.
91
93
  pub(crate) const fn key(&self) -> Option<&Key> {
92
94
  match self {
93
95
  Self::Compile(key) => key.as_ref(),
@@ -98,11 +100,14 @@ impl Plan {
98
100
  }
99
101
 
100
102
  impl CodeCache {
103
+ /// `shared` asks for a first sighting's key as well, which only a
104
+ /// context reading the script from its shared source needs.
101
105
  pub(crate) fn plan(
102
106
  &'static self,
103
107
  filename: &JsText,
104
108
  source: &JsText,
105
109
  snapshot: Option<u64>,
110
+ shared: bool,
106
111
  ) -> Plan {
107
112
  if !enabled() {
108
113
  return Plan::Compile(None);
@@ -113,7 +118,8 @@ impl CodeCache {
113
118
  let cached = lock(&self.entries)
114
119
  .iter()
115
120
  .any(|entry| entry.key.sighting == sighting);
116
- if !seen && !cached {
121
+ let first = !seen && !cached;
122
+ if first && !shared {
117
123
  return Plan::Compile(None);
118
124
  }
119
125
  let key = Key {
@@ -123,6 +129,9 @@ impl CodeCache {
123
129
  digest: self.hasher.hash_one(source),
124
130
  sighting,
125
131
  };
132
+ if first {
133
+ return Plan::Compile(Some(key));
134
+ }
126
135
  let mut entries = lock(&self.entries);
127
136
  if let Some(at) = entries.iter().position(|entry| entry.key == key) {
128
137
  let entry = entries.remove(at);
@@ -300,8 +309,8 @@ mod tests {
300
309
 
301
310
  // Sees `text` twice and stores a cache for it.
302
311
  fn cached(cache: &'static CodeCache, text: &JsText, data: Box<[u8]>) {
303
- let _ = cache.plan(&name(), text, None);
304
- let Plan::Make(building) = cache.plan(&name(), text, None) else {
312
+ let _ = cache.plan(&name(), text, None, true);
313
+ let Plan::Make(building) = cache.plan(&name(), text, None, true) else {
305
314
  panic!("a second sighting makes a cache");
306
315
  };
307
316
  cache.store(building, data);
@@ -311,22 +320,46 @@ mod tests {
311
320
  fn caches_a_script_the_second_time_it_is_seen() {
312
321
  let cache = cache();
313
322
  assert!(matches!(
314
- cache.plan(&name(), &source(1), None),
315
- Plan::Compile(None)
323
+ cache.plan(&name(), &source(1), None, true),
324
+ Plan::Compile(Some(_))
316
325
  ));
317
326
  cached(cache, &source(1), Box::from(&b"compiled"[..]));
318
327
  assert!(matches!(
319
- cache.plan(&name(), &source(1), None),
328
+ cache.plan(&name(), &source(1), None, true),
320
329
  Plan::Consume(..)
321
330
  ));
322
331
  assert!(matches!(
323
- cache.plan(&name(), &source(1), Some(7)),
324
- Plan::Compile(None)
332
+ cache.plan(&name(), &source(1), Some(7), true),
333
+ Plan::Compile(Some(_))
325
334
  ));
326
335
  assert!(matches!(
327
- cache.plan(&JsText::from("other.js"), &source(1), None),
336
+ cache.plan(&JsText::from("other.js"), &source(1), None, true),
337
+ Plan::Compile(Some(_))
338
+ ));
339
+ }
340
+
341
+ // The key a first sighting has for its shared source is the one the
342
+ // cache is then made and found under.
343
+ #[test]
344
+ fn a_first_sighting_has_its_key_only_for_a_shared_source() {
345
+ let cache = cache();
346
+ assert!(matches!(
347
+ cache.plan(&name(), &source(8), None, false),
328
348
  Plan::Compile(None)
329
349
  ));
350
+ let first = cache.plan(&name(), &source(9), None, true);
351
+ assert!(matches!(first, Plan::Compile(Some(_))));
352
+ let second = cache.plan(&name(), &source(9), None, false);
353
+ assert!(matches!(second, Plan::Make(..)));
354
+ assert!(first.key() == second.key());
355
+ let Plan::Make(building) = second else {
356
+ panic!("a second sighting makes a cache");
357
+ };
358
+ cache.store(building, Box::from(&b"compiled"[..]));
359
+ let Plan::Consume(_, key) = cache.plan(&name(), &source(9), None, true) else {
360
+ panic!("a stored cache is found");
361
+ };
362
+ assert!(first.key() == Some(&key));
330
363
  }
331
364
 
332
365
  // Sources that match on every sampled unit and differ elsewhere share a
@@ -344,7 +377,10 @@ mod tests {
344
377
  cache.sighting(&name(), &second, None)
345
378
  );
346
379
  cached(cache, &first, Box::from(&b"compiled"[..]));
347
- assert!(matches!(cache.plan(&name(), &second, None), Plan::Make(..)));
380
+ assert!(matches!(
381
+ cache.plan(&name(), &second, None, true),
382
+ Plan::Make(..)
383
+ ));
348
384
  }
349
385
 
350
386
  #[test]
@@ -353,10 +389,10 @@ mod tests {
353
389
  cached(cache, &source(2), Box::from(&b"compiled"[..]));
354
390
  // More sightings than the list keeps.
355
391
  for seed in 10_u8..80 {
356
- let _ = cache.plan(&name(), &source(seed), None);
392
+ let _ = cache.plan(&name(), &source(seed), None, false);
357
393
  }
358
394
  assert!(matches!(
359
- cache.plan(&name(), &source(2), None),
395
+ cache.plan(&name(), &source(2), None, true),
360
396
  Plan::Consume(..)
361
397
  ));
362
398
  }
@@ -364,15 +400,15 @@ mod tests {
364
400
  #[test]
365
401
  fn a_script_being_built_is_built_once() {
366
402
  let cache = cache();
367
- let _ = cache.plan(&name(), &source(3), None);
368
- let first = cache.plan(&name(), &source(3), None);
403
+ let _ = cache.plan(&name(), &source(3), None, true);
404
+ let first = cache.plan(&name(), &source(3), None, true);
369
405
  assert!(matches!(first, Plan::Make(..)));
370
- let elsewhere = cache.plan(&name(), &source(3), None);
406
+ let elsewhere = cache.plan(&name(), &source(3), None, true);
371
407
  assert!(matches!(elsewhere, Plan::Compile(Some(_))));
372
408
  assert!(elsewhere.key() == first.key());
373
409
  drop(first);
374
410
  assert!(matches!(
375
- cache.plan(&name(), &source(3), None),
411
+ cache.plan(&name(), &source(3), None, true),
376
412
  Plan::Make(..)
377
413
  ));
378
414
  }
@@ -383,7 +419,7 @@ mod tests {
383
419
  cached(cache, &source(4), Box::from(&b"compiled"[..]));
384
420
  let child: &'static CodeCache = Box::leak(Box::new(cache.inherited()));
385
421
  assert!(matches!(
386
- child.plan(&name(), &source(4), None),
422
+ child.plan(&name(), &source(4), None, true),
387
423
  Plan::Consume(..)
388
424
  ));
389
425
  }
@@ -396,7 +432,7 @@ mod tests {
396
432
  let child: &'static CodeCache = Box::leak(Box::new(cache.inherited()));
397
433
  drop(held);
398
434
  assert!(matches!(
399
- child.plan(&name(), &source(5), None),
435
+ child.plan(&name(), &source(5), None, true),
400
436
  Plan::Make(..)
401
437
  ));
402
438
  }
@@ -412,11 +448,11 @@ mod tests {
412
448
  );
413
449
  assert_eq!(cache.stored(), 1);
414
450
  assert!(matches!(
415
- cache.plan(&name(), &source(7), None),
451
+ cache.plan(&name(), &source(7), None, true),
416
452
  Plan::Compile(Some(_))
417
453
  ));
418
454
  assert!(matches!(
419
- cache.plan(&name(), &source(6), None),
455
+ cache.plan(&name(), &source(6), None, true),
420
456
  Plan::Consume(..)
421
457
  ));
422
458
  }
@@ -9,6 +9,7 @@ use crate::process::code_cache::Key;
9
9
  const MAX_SOURCES: usize = 8;
10
10
  const MAX_BYTES: usize = 256 << 20;
11
11
  const MAX_REFUSED: usize = 8;
12
+ const MAX_SWEPT: usize = 8;
12
13
 
13
14
  /// A large script's source as V8 stores a string of it: a byte a unit when
14
15
  /// every unit fits Latin-1, two otherwise. Never changed once made.
@@ -63,8 +64,9 @@ impl Units {
63
64
 
64
65
  /// One copy of each large script's source, outside every V8 heap.
65
66
  ///
66
- /// A script is kept once the process has seen it before, and every context
67
- /// compiling it then reads the kept copy in place of one on its own heap.
67
+ /// A script is kept by the first context to compile it, and every context
68
+ /// compiling it, that one included, reads the kept copy in place of one on
69
+ /// its own heap.
68
70
  /// Each string V8 makes from one holds a reference to it, which V8 hands back
69
71
  /// when the string dies (`sys::external_string`). A source is dropped only
70
72
  /// when no string holds one, so V8 never releases the last reference itself.
@@ -72,6 +74,8 @@ pub struct Sources {
72
74
  list: Mutex<List>,
73
75
  // How many sources, and how many bytes of them, may be kept and read.
74
76
  bounds: (usize, usize),
77
+ // The number the next context's reads are told apart by.
78
+ next_reader: AtomicU64,
75
79
  made: AtomicU64,
76
80
  reads: AtomicU64,
77
81
  conversions: AtomicU64,
@@ -91,6 +95,9 @@ struct List {
91
95
  // left or, as invalid UTF-8, unbounded, so that while it is among the last
92
96
  // `MAX_REFUSED` refused it is not converted again without room for it.
93
97
  refused: Vec<(Key, usize)>,
98
+ // The last `MAX_SWEPT` sources swept, so one made again, which a second
99
+ // context has now read after all, is kept as such.
100
+ swept: Vec<Key>,
94
101
  }
95
102
 
96
103
  struct Kept {
@@ -99,23 +106,60 @@ struct Kept {
99
106
  // The references from this process's list and every ancestor's, which a
100
107
  // forked child never touches again; the strings reading it hold the rest.
101
108
  held: usize,
109
+ readers: Readers,
110
+ }
111
+
112
+ // The contexts that have read a kept source. One alone, as a script evaluated
113
+ // once has, gains nothing from a copy no string reads any more; a source more
114
+ // have read is likely to be read again, and an inherited one is only copied
115
+ // again if a child drops it, so both stay until a new source needs the room.
116
+ #[derive(Clone, Copy, PartialEq, Eq)]
117
+ enum Readers {
118
+ One(u64),
119
+ Many,
102
120
  }
103
121
 
104
122
  impl Kept {
105
123
  fn unread(&self) -> bool {
106
124
  self.units.references() == self.held
107
125
  }
126
+
127
+ fn spent(&self) -> bool {
128
+ matches!(self.readers, Readers::One(_)) && self.unread()
129
+ }
108
130
  }
109
131
 
110
132
  impl List {
111
- fn find(&mut self, key: &Key) -> Option<Units> {
133
+ fn find(&mut self, key: &Key, reader: u64) -> Option<Units> {
112
134
  let at = self.kept.iter().position(|kept| kept.key == *key)?;
113
- let found = self.kept.remove(at);
135
+ let mut found = self.kept.remove(at);
136
+ if found.readers != Readers::One(reader) {
137
+ found.readers = Readers::Many;
138
+ }
114
139
  let units = found.units.clone();
115
140
  self.kept.push(found);
116
141
  Some(units)
117
142
  }
118
143
 
144
+ // V8's release of a string touches only the count, so a copy whose last
145
+ // string has gone waits for the next `Sources::units` call.
146
+ fn sweep(&mut self) -> Vec<Kept> {
147
+ let spent: Vec<Kept> = self.kept.extract_if(.., |kept| kept.spent()).collect();
148
+ for kept in &spent {
149
+ if self.swept.len() >= MAX_SWEPT {
150
+ self.swept.remove(0);
151
+ }
152
+ self.swept.push(kept.key.clone());
153
+ }
154
+ spent
155
+ }
156
+
157
+ fn unswept(&mut self, key: &Key) -> bool {
158
+ let before = self.swept.len();
159
+ self.swept.retain(|swept| swept != key);
160
+ self.swept.len() < before
161
+ }
162
+
119
163
  // Whether `bytes` more fit beside what strings still read, which nothing
120
164
  // can drop.
121
165
  fn room(&self, bytes: usize, (sources, most): (usize, usize)) -> bool {
@@ -161,30 +205,43 @@ impl Sources {
161
205
  Self {
162
206
  list: Mutex::default(),
163
207
  bounds: (sources, bytes),
208
+ next_reader: AtomicU64::new(0),
164
209
  made: AtomicU64::new(0),
165
210
  reads: AtomicU64::new(0),
166
211
  conversions: AtomicU64::new(0),
167
212
  }
168
213
  }
169
214
 
215
+ /// A number for a new context to read shared sources as, told apart from
216
+ /// every other context of this process.
217
+ pub(crate) fn reader(&self) -> u64 {
218
+ self.next_reader.fetch_add(1, Ordering::Relaxed)
219
+ }
220
+
170
221
  /// The shared source of the script `text` under `key`, kept from the first
171
- /// context that asks for it. `None` leaves the context its own copy: the
172
- /// source is invalid UTF-8, or too large for what is kept and still read.
173
- /// Only a source that may fit is converted, outside the lock.
174
- pub(crate) fn units(&self, key: &Key, text: &JsText) -> Option<Units> {
222
+ /// context that asks for it, for the context `reader`. `None` leaves the
223
+ /// context its own copy: the source is invalid UTF-8, or too large for
224
+ /// what is kept and still read. Only a source that may fit is converted,
225
+ /// outside the lock. Every call looks up the requested source, then drops
226
+ /// each other copy that no string reads and that one context alone ever
227
+ /// read.
228
+ pub(crate) fn units(&self, key: &Key, text: &JsText, reader: u64) -> Option<Units> {
175
229
  let mut list = lock(&self.list);
176
- if let Some(units) = list.find(key) {
177
- return Some(units);
178
- }
179
- if !list.room(list.least_bytes(key, text), self.bounds) {
180
- return None;
181
- }
230
+ // Found before the sweep, so a copy the context before this one left
231
+ // unread is read again, and kept, rather than copied again.
232
+ let found = list.find(key, reader);
233
+ let spent = list.sweep();
234
+ let room = found.is_none() && list.room(list.least_bytes(key, text), self.bounds);
182
235
  drop(list);
236
+ drop(spent);
237
+ if !room {
238
+ return found;
239
+ }
183
240
  self.conversions.fetch_add(1, Ordering::Relaxed);
184
241
  let units = Units::of(text);
185
242
  let mut list = lock(&self.list);
186
243
  // Made meanwhile by a context loading the same script.
187
- if let Some(found) = list.find(key) {
244
+ if let Some(found) = list.find(key, reader) {
188
245
  return Some(found);
189
246
  }
190
247
  let bytes = units.as_ref().map_or(usize::MAX, Units::bytes);
@@ -211,13 +268,20 @@ impl Sources {
211
268
  };
212
269
  dropped.push(list.kept.remove(at));
213
270
  }
271
+ let readers = if list.unswept(key) {
272
+ Readers::Many
273
+ } else {
274
+ Readers::One(reader)
275
+ };
214
276
  list.kept.push(Kept {
215
277
  key: key.clone(),
216
278
  units: units.clone(),
217
279
  held: 1,
280
+ readers,
218
281
  });
219
282
  drop(list);
220
- // Freed once the lock is released, as a large source takes a while.
283
+ // Freed once the lock is released, as a large source takes a while,
284
+ // like the spent copies above.
221
285
  drop(dropped);
222
286
  self.made.fetch_add(1, Ordering::Relaxed);
223
287
  Some(units)
@@ -242,9 +306,11 @@ impl Sources {
242
306
  key: kept.key.clone(),
243
307
  units: kept.units.clone(),
244
308
  held: kept.held + 1,
309
+ readers: Readers::Many,
245
310
  })
246
311
  .collect(),
247
312
  refused: list.refused.clone(),
313
+ swept: list.swept.clone(),
248
314
  })
249
315
  .unwrap_or_default();
250
316
  Self {
@@ -291,16 +357,14 @@ mod tests {
291
357
  use crate::control::JsText;
292
358
  use crate::process::code_cache::{CodeCache, Key};
293
359
 
294
- // A script's key once the code cache has seen it twice.
360
+ // A script's key, as its first sighting in a context sharing it has it.
295
361
  fn key(text: &JsText) -> Key {
296
362
  let cache: &'static CodeCache = Box::leak(Box::default());
297
- let name = JsText::from("bundle.js");
298
- let _ = cache.plan(&name, text, None);
299
363
  cache
300
- .plan(&name, text, None)
364
+ .plan(&JsText::from("bundle.js"), text, None, true)
301
365
  .key()
302
366
  .cloned()
303
- .unwrap_or_else(|| panic!("a second sighting has a key"))
367
+ .unwrap_or_else(|| panic!("a shared first sighting has a key"))
304
368
  }
305
369
 
306
370
  fn script(seed: u8) -> JsText {
@@ -356,8 +420,8 @@ mod tests {
356
420
  let sources = Sources::default();
357
421
  let text = script(1);
358
422
  let key = key(&text);
359
- let first = one_byte(sources.units(&key, &text));
360
- let second = one_byte(sources.units(&key, &text));
423
+ let first = one_byte(sources.units(&key, &text, sources.reader()));
424
+ let second = one_byte(sources.units(&key, &text, sources.reader()));
361
425
  assert!(Arc::ptr_eq(&first, &second));
362
426
  assert_eq!(sources.made(), 1);
363
427
  assert_eq!(sources.reading(), 2);
@@ -377,6 +441,12 @@ mod tests {
377
441
  }
378
442
  }
379
443
 
444
+ // A copy two contexts have read, the second still reading it.
445
+ fn shared(sources: &Sources, key: &Key, text: &JsText) -> Option<Units> {
446
+ drop(sources.units(key, text, sources.reader()));
447
+ sources.units(key, text, sources.reader())
448
+ }
449
+
380
450
  #[test]
381
451
  fn a_source_still_read_is_kept_and_an_unread_one_makes_room() {
382
452
  let sources = Sources::default();
@@ -386,31 +456,108 @@ mod tests {
386
456
  let mut read: Vec<Option<Units>> = texts
387
457
  .iter()
388
458
  .zip(&keys)
389
- .map(|(text, key)| sources.units(key, text))
459
+ .map(|(text, key)| shared(&sources, key, text))
390
460
  .collect();
391
461
  let extra = script(100);
392
462
  let extra_key = key(&extra);
393
- assert!(sources.units(&extra_key, &extra).is_none());
463
+ assert!(shared(&sources, &extra_key, &extra).is_none());
394
464
  drop(read.remove(3));
395
465
  drop(read.remove(0));
396
- drop(sources.units(&keys[3], &texts[3]));
466
+ drop(sources.units(&keys[3], &texts[3], sources.reader()));
397
467
  // Of the two unread, the least recently used makes room.
398
- assert!(sources.units(&extra_key, &extra).is_some());
468
+ assert!(shared(&sources, &extra_key, &extra).is_some());
399
469
  let made = sources.made();
400
470
  assert_eq!(made, u64::from(count) + 1);
401
- drop(sources.units(&keys[3], &texts[3]));
471
+ drop(sources.units(&keys[3], &texts[3], sources.reader()));
402
472
  assert_eq!(sources.made(), made);
403
- drop(sources.units(&keys[0], &texts[0]));
473
+ drop(sources.units(&keys[0], &texts[0], sources.reader()));
404
474
  assert_eq!(sources.made(), made + 1);
405
475
  for (at, held) in [1, 2, 4, 5, 6, 7].into_iter().zip(&read) {
406
476
  assert!(same(
407
- sources.units(&keys[at], &texts[at]).as_ref(),
477
+ sources
478
+ .units(&keys[at], &texts[at], sources.reader())
479
+ .as_ref(),
408
480
  held.as_ref()
409
481
  ));
410
482
  }
411
483
  assert_eq!(sources.made(), made + 1);
412
484
  }
413
485
 
486
+ // One context's copy, however often it read it, waits for no new source
487
+ // once no string reads it: the next call drops and frees it.
488
+ #[test]
489
+ fn a_copy_one_context_read_goes_at_the_next_call_once_unread() {
490
+ let sources = Sources::default();
491
+ let (text, other) = (script(70), script(71));
492
+ let (key, other_key) = (key(&text), key(&other));
493
+ let reader = sources.reader();
494
+ let first = one_byte(sources.units(&key, &text, reader));
495
+ let again = one_byte(sources.units(&key, &text, reader));
496
+ assert!(Arc::ptr_eq(&first, &again));
497
+ let copy = Arc::downgrade(&first);
498
+ drop((first, again));
499
+ assert!(copy.upgrade().is_some());
500
+ drop(sources.units(&other_key, &other, sources.reader()));
501
+ assert!(copy.upgrade().is_none());
502
+ drop(sources.units(&key, &text, sources.reader()));
503
+ assert_eq!(sources.made(), 3);
504
+ }
505
+
506
+ #[test]
507
+ fn a_copy_two_contexts_read_in_turn_stays_once_unread() {
508
+ let sources = Sources::default();
509
+ let (text, other) = (script(72), script(73));
510
+ let (key, other_key) = (key(&text), key(&other));
511
+ let copy = Arc::downgrade(&one_byte(shared(&sources, &key, &text)));
512
+ drop(sources.units(&other_key, &other, sources.reader()));
513
+ let again = one_byte(sources.units(&key, &text, sources.reader()));
514
+ assert!(
515
+ copy.upgrade()
516
+ .is_some_and(|copy| Arc::ptr_eq(&copy, &again))
517
+ );
518
+ assert_eq!(sources.made(), 2);
519
+ }
520
+
521
+ #[test]
522
+ fn a_copy_a_second_context_reads_while_the_first_does_stays_once_unread() {
523
+ let sources = Sources::default();
524
+ let (text, other) = (script(74), script(75));
525
+ let (key, other_key) = (key(&text), key(&other));
526
+ let first = one_byte(sources.units(&key, &text, sources.reader()));
527
+ let second = one_byte(sources.units(&key, &text, sources.reader()));
528
+ let copy = Arc::downgrade(&first);
529
+ drop((first, second));
530
+ drop(sources.units(&other_key, &other, sources.reader()));
531
+ let again = one_byte(sources.units(&key, &text, sources.reader()));
532
+ assert!(
533
+ copy.upgrade()
534
+ .is_some_and(|copy| Arc::ptr_eq(&copy, &again))
535
+ );
536
+ assert_eq!(sources.made(), 2);
537
+ }
538
+
539
+ // Contexts loading two scripts in turn, each gone before the next, sweep
540
+ // each other's copy; a copy made again after its sweep is kept as one more
541
+ // than one context read, so each script is converted twice in all.
542
+ #[test]
543
+ fn scripts_loaded_in_turn_by_short_lived_contexts_stay_after_their_second_copy() {
544
+ let sources = Sources::default();
545
+ let texts = [script(78), script(79)];
546
+ let keys = texts.each_ref().map(key);
547
+ for _ in 0..2 {
548
+ for (key, text) in keys.iter().zip(&texts) {
549
+ drop(sources.units(key, text, sources.reader()));
550
+ }
551
+ }
552
+ assert_eq!(sources.made(), 4);
553
+ for _ in 0..3 {
554
+ for (key, text) in keys.iter().zip(&texts) {
555
+ drop(sources.units(key, text, sources.reader()));
556
+ }
557
+ }
558
+ assert_eq!(sources.made(), 4);
559
+ }
560
+
414
561
  #[test]
415
562
  fn a_full_list_converts_nothing() {
416
563
  let sources = Sources::default();
@@ -418,18 +565,26 @@ mod tests {
418
565
  let read: Vec<Option<Units>> = (40..40 + count)
419
566
  .map(|seed| {
420
567
  let text = script(seed);
421
- sources.units(&key(&text), &text)
568
+ sources.units(&key(&text), &text, sources.reader())
422
569
  })
423
570
  .collect();
424
571
  let converted = sources.conversions();
425
572
  let extra = script(100);
426
573
  let extra_key = key(&extra);
427
574
  for _ in 0..3 {
428
- assert!(sources.units(&extra_key, &extra).is_none());
575
+ assert!(
576
+ sources
577
+ .units(&extra_key, &extra, sources.reader())
578
+ .is_none()
579
+ );
429
580
  }
430
581
  assert_eq!(sources.conversions(), converted);
431
582
  drop(read);
432
- assert!(sources.units(&extra_key, &extra).is_some());
583
+ assert!(
584
+ sources
585
+ .units(&extra_key, &extra, sources.reader())
586
+ .is_some()
587
+ );
433
588
  assert_eq!(sources.conversions(), converted + 1);
434
589
  }
435
590
 
@@ -440,21 +595,29 @@ mod tests {
440
595
  fn a_source_is_converted_only_while_it_may_fit() {
441
596
  let sources = Sources::bounded(MAX_SOURCES, 5 << 20);
442
597
  let read = script(50);
443
- let still_read = sources.units(&key(&read), &read);
598
+ let still_read = sources.units(&key(&read), &read, sources.reader());
444
599
  assert!(still_read.is_some());
445
600
  let too_large = JsText::Latin1(vec![51; 4 << 20]);
446
- assert!(sources.units(&key(&too_large), &too_large).is_none());
601
+ assert!(
602
+ sources
603
+ .units(&key(&too_large), &too_large, sources.reader())
604
+ .is_none()
605
+ );
447
606
  assert_eq!(sources.conversions(), 1);
448
607
  let mut astral = vec![b'a'; 2 << 20];
449
608
  astral.extend_from_slice("\u{1F600}".as_bytes());
450
609
  let astral = JsText::Utf8(astral);
451
610
  let astral_key = key(&astral);
452
611
  for _ in 0..3 {
453
- assert!(sources.units(&astral_key, &astral).is_none());
612
+ assert!(
613
+ sources
614
+ .units(&astral_key, &astral, sources.reader())
615
+ .is_none()
616
+ );
454
617
  }
455
618
  assert_eq!(sources.conversions(), 2);
456
619
  drop(still_read);
457
- assert!(two_byte(sources.units(&astral_key, &astral)).len() > 2 << 20);
620
+ assert!(two_byte(sources.units(&astral_key, &astral, sources.reader())).len() > 2 << 20);
458
621
  assert_eq!((sources.conversions(), sources.made()), (3, 2));
459
622
  }
460
623
 
@@ -466,7 +629,11 @@ mod tests {
466
629
  let invalid = JsText::Utf8(invalid);
467
630
  let invalid_key = key(&invalid);
468
631
  for _ in 0..3 {
469
- assert!(sources.units(&invalid_key, &invalid).is_none());
632
+ assert!(
633
+ sources
634
+ .units(&invalid_key, &invalid, sources.reader())
635
+ .is_none()
636
+ );
470
637
  }
471
638
  assert_eq!((sources.conversions(), sources.made()), (1, 0));
472
639
  }
@@ -476,32 +643,56 @@ mod tests {
476
643
  let parent: &'static Sources = Box::leak(Box::default());
477
644
  let text = script(2);
478
645
  let key = key(&text);
479
- let shared = parent.units(&key, &text);
646
+ let shared = parent.units(&key, &text, parent.reader());
480
647
  let child = parent.inherited();
481
- let in_child = child.units(&key, &text);
648
+ let in_child = child.units(&key, &text, child.reader());
482
649
  assert!(same(shared.as_ref(), in_child.as_ref()));
483
650
  assert_eq!(child.reading(), 2);
484
651
  let copy = Arc::downgrade(&one_byte(shared));
485
652
  drop(in_child);
653
+ // Sources the child still reads fill its list, so the unread
654
+ // inherited copy makes room.
486
655
  let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
487
- for seed in 30..30 + count {
488
- let other = script(seed);
489
- drop(child.units(&self::key(&other), &other));
490
- }
656
+ let others: Vec<Option<Units>> = (30..30 + count)
657
+ .map(|seed| {
658
+ let other = script(seed);
659
+ child.units(&self::key(&other), &other, child.reader())
660
+ })
661
+ .collect();
491
662
  assert_eq!(copy.strong_count(), 1);
492
- let again = one_byte(child.units(&key, &text));
663
+ drop(others);
664
+ let again = one_byte(child.units(&key, &text, child.reader()));
493
665
  assert!(
494
666
  !copy
495
667
  .upgrade()
496
668
  .is_some_and(|copy| Arc::ptr_eq(&copy, &again))
497
669
  );
498
- let from_parent = one_byte(parent.units(&key, &text));
670
+ let from_parent = one_byte(parent.units(&key, &text, parent.reader()));
499
671
  assert!(
500
672
  copy.upgrade()
501
673
  .is_some_and(|copy| Arc::ptr_eq(&copy, &from_parent))
502
674
  );
503
675
  }
504
676
 
677
+ // Dropping an inherited copy frees nothing in a child, and its next
678
+ // context loading the script would copy it again, so the child keeps
679
+ // one only its parent's context read.
680
+ #[test]
681
+ fn a_child_keeps_an_inherited_copy_one_context_read() {
682
+ let parent: &'static Sources = Box::leak(Box::default());
683
+ let (text, other) = (script(76), script(77));
684
+ let (key, other_key) = (key(&text), key(&other));
685
+ let copy = Arc::downgrade(&one_byte(parent.units(&key, &text, parent.reader())));
686
+ let child = parent.inherited();
687
+ drop(child.units(&other_key, &other, child.reader()));
688
+ let in_child = one_byte(child.units(&key, &text, child.reader()));
689
+ assert!(
690
+ copy.upgrade()
691
+ .is_some_and(|copy| Arc::ptr_eq(&copy, &in_child))
692
+ );
693
+ assert_eq!(child.made(), 1);
694
+ }
695
+
505
696
  // A child's inherited copies stay read while its parent's strings hold
506
697
  // them, so a child forked with a full list must not convert either.
507
698
  #[test]
@@ -511,14 +702,14 @@ mod tests {
511
702
  let read: Vec<Option<Units>> = (60..60 + count)
512
703
  .map(|seed| {
513
704
  let text = script(seed);
514
- parent.units(&key(&text), &text)
705
+ parent.units(&key(&text), &text, parent.reader())
515
706
  })
516
707
  .collect();
517
708
  let child = parent.inherited();
518
709
  let extra = script(110);
519
710
  let extra_key = key(&extra);
520
711
  for _ in 0..3 {
521
- assert!(child.units(&extra_key, &extra).is_none());
712
+ assert!(child.units(&extra_key, &extra, child.reader()).is_none());
522
713
  }
523
714
  assert_eq!(child.conversions(), 0);
524
715
  drop(read);
@@ -529,11 +720,14 @@ mod tests {
529
720
  let parent = Sources::default();
530
721
  let text = script(3);
531
722
  let key = key(&text);
532
- let shared = one_byte(parent.units(&key, &text));
723
+ let shared = one_byte(parent.units(&key, &text, parent.reader()));
533
724
  let held = parent.list.lock();
534
725
  let child = parent.inherited();
535
726
  drop(held);
536
- assert!(!Arc::ptr_eq(&shared, &one_byte(child.units(&key, &text))));
727
+ assert!(!Arc::ptr_eq(
728
+ &shared,
729
+ &one_byte(child.units(&key, &text, child.reader()))
730
+ ));
537
731
  assert_eq!(child.made(), 1);
538
732
  }
539
733
  }
@@ -62,6 +62,9 @@ struct Runtime {
62
62
  freed: AtomicBool,
63
63
  // The startup snapshot this isolate began from, for the code cache's key.
64
64
  snapshot: Option<u64>,
65
+ // Tells this context's reads of a shared source from other contexts', as
66
+ // a copy only one context read is dropped once no string reads it.
67
+ reader: u64,
65
68
  // Large scripts to cache once their dispatch is over, so making a cache
66
69
  // never counts against a timeout.
67
70
  uncached: Mutex<Vec<Uncached>>,
@@ -142,6 +145,7 @@ impl Runtime {
142
145
  running: AtomicU32::new(0),
143
146
  freed: AtomicBool::new(false),
144
147
  snapshot,
148
+ reader: generation.sources().reader(),
145
149
  uncached: Mutex::default(),
146
150
  hoisting: Mutex::default(),
147
151
  frames: Mutex::default(),
@@ -770,10 +774,12 @@ fn evaluate(
770
774
  }
771
775
  let plan = if large(source) {
772
776
  runtime.freed.store(true, Ordering::Relaxed);
773
- runtime
774
- .generation
775
- .code_cache()
776
- .plan(filename, source, runtime.snapshot)
777
+ runtime.generation.code_cache().plan(
778
+ filename,
779
+ source,
780
+ runtime.snapshot,
781
+ shares_sources(runtime),
782
+ )
777
783
  } else {
778
784
  Plan::Compile(None)
779
785
  };
@@ -796,21 +802,25 @@ fn evaluate(
796
802
  Ok(reply)
797
803
  }
798
804
 
799
- // A large script the process has seen before reads its source from the copy
800
- // every context compiling it shares, outside the V8 heap. `max_memory:`
801
- // measures the heap, where mini_racer keeps the source, so a context with it
802
- // keeps its own copy.
805
+ // `max_memory:` measures the heap, where mini_racer keeps the source, so a
806
+ // context with it keeps its own copy.
807
+ fn shares_sources(runtime: &Runtime) -> bool {
808
+ runtime.block.config.max_memory.is_none()
809
+ }
810
+
811
+ // A large script reads its source from the copy every context compiling it
812
+ // shares, outside the V8 heap, made by the first of them.
803
813
  fn shared_source<'s>(
804
814
  scope: &v8::PinScope<'s, '_>,
805
815
  runtime: &Runtime,
806
816
  key: Option<&Key>,
807
817
  source: &JsText,
808
818
  ) -> Option<v8::Local<'s, v8::String>> {
809
- if runtime.block.config.max_memory.is_some() {
819
+ if !shares_sources(runtime) {
810
820
  return None;
811
821
  }
812
822
  let sources = runtime.generation.sources();
813
- let string = sys::external_string(scope, &sources.units(key?, source)?)?;
823
+ let string = sys::external_string(scope, &sources.units(key?, source, runtime.reader)?)?;
814
824
  sources.read();
815
825
  Some(string)
816
826
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacerRs
4
- VERSION = "1.0.3"
4
+ VERSION = "1.0.4"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer_rs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Aragon
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-18 00:00:00.000000000 Z
10
+ date: 2026-10-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rb_sys