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 +4 -4
- data/CHANGELOG.md +7 -0
- data/COMPATIBILITY.md +1 -1
- data/Cargo.lock +4 -4
- data/Cargo.toml +1 -1
- data/README.md +19 -13
- data/crates/mini_racer_engine/src/process/code_cache.rs +61 -25
- data/crates/mini_racer_engine/src/process/sources.rs +244 -50
- data/crates/mini_racer_engine/src/runner.rs +20 -10
- data/lib/mini_racer_rs/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a8339f7ec9dcc06bd136d68328c3b5a8acd48bcb59061fe0e1de97d8163e7a5
|
|
4
|
+
data.tar.gz: fdb040a4e14239cce537be6724a36410432093b6f6ca0f62d6eec5830d32c6b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
492
|
+
version = "1.0.4"
|
|
493
493
|
dependencies = [
|
|
494
494
|
"proptest",
|
|
495
495
|
"thiserror",
|
data/Cargo.toml
CHANGED
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
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
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.
|
|
164
|
-
| Terser minify of a 78 KB library, through ExecJS | 51.
|
|
165
|
-
| React server render of 1,000 items (17 KB of HTML), through ExecJS |
|
|
166
|
-
| React in a new context: load the bundle and render 1,000 items, through ExecJS |
|
|
167
|
-
| Handlebars render of 50 rows, through `Context#call` |
|
|
168
|
-
| Autoprefixer on Bootstrap's 280 KB stylesheet, through ExecJS |
|
|
169
|
-
| MUI dashboard server render of 200 rows and 299 icons (469 KB of HTML), through ExecJS | 32.
|
|
170
|
-
| MUI in a new context: load the 8.2 MB bundle and render once, through ExecJS | 167 ms |
|
|
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
|
-
|
|
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
|
|
25
|
-
///
|
|
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:
|
|
81
|
-
/// key being built elsewhere or
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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!(
|
|
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
|
|
67
|
-
/// compiling it
|
|
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
|
|
172
|
-
/// source is invalid UTF-8, or too large for
|
|
173
|
-
/// Only a source that may fit is converted,
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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(&
|
|
364
|
+
.plan(&JsText::from("bundle.js"), text, None, true)
|
|
301
365
|
.key()
|
|
302
366
|
.cloned()
|
|
303
|
-
.unwrap_or_else(|| panic!("a
|
|
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
|
|
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!(
|
|
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!(
|
|
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
|
|
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(©, &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(©, &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!(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
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(©, &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(©, &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(©, &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(
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
//
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
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
|
|
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
|
}
|
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.
|
|
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-
|
|
10
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rb_sys
|