mini_racer_rs 1.0.1 → 1.0.2
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 +7 -1
- data/Cargo.lock +4 -4
- data/Cargo.toml +1 -1
- data/README.md +1 -1
- data/crates/mini_racer_engine/src/process/code_cache.rs +26 -15
- data/crates/mini_racer_engine/src/process/fork.rs +12 -2
- data/crates/mini_racer_engine/src/process/sources.rs +539 -0
- data/crates/mini_racer_engine/src/process.rs +1 -0
- data/crates/mini_racer_engine/src/runner.rs +25 -4
- data/crates/mini_racer_engine/src/sys.rs +75 -1
- data/lib/mini_racer_rs/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b53466394528b78785dc0cac7e884a5b59f0200ca037d405c95375152a12d72
|
|
4
|
+
data.tar.gz: 06b9b34066ec568f0a41186f5f7c533c3a17f7acb90a625ec8e9fff22b3069f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e9f4ee092b37374b56a701831bd4ba868af74de863c220a9f58aae583505cc3c4000106107cfe8c8e709552863d5b484b8715cd016306ecb06241f2d84fe759
|
|
7
|
+
data.tar.gz: d5dc38c171b8d11683d53840d6c671cbf90ae042dba02f1a46e9786f343e555356808e41e2fb460b00a440c326a2da23f737b1149ab96d359b4e2269981c8b36
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.2](https://github.com/andymai/mini_racer_rs/compare/v1.0.1...v1.0.2) (2026-10-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Performance
|
|
7
|
+
|
|
8
|
+
* **engine:** share one copy of a large repeated source across contexts ([#85](https://github.com/andymai/mini_racer_rs/issues/85)) ([9fe050a](https://github.com/andymai/mini_racer_rs/commit/9fe050a7a612314264e255f9bcc40ca146e93571))
|
|
9
|
+
|
|
3
10
|
## [1.0.1](https://github.com/andymai/mini_racer_rs/compare/v1.0.0...v1.0.1) (2026-10-18)
|
|
4
11
|
|
|
5
12
|
|
data/COMPATIBILITY.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| Engine | Done | Planned | Deviation |
|
|
6
6
|
|---|---|---|---|
|
|
7
|
-
| CRuby | 54 | 0 |
|
|
7
|
+
| CRuby | 54 | 0 | 26 |
|
|
8
8
|
| TruffleRuby | 58 | 0 | 0 |
|
|
9
9
|
|
|
10
10
|
## Surface
|
|
@@ -180,6 +180,12 @@ mini_racer_rs supports glibc Linux on x86_64 and aarch64, plus macOS on arm64 an
|
|
|
180
180
|
|
|
181
181
|
Approved 2026-10-16.
|
|
182
182
|
|
|
183
|
+
### `behavior:shared-source-heap-stats@ruby`
|
|
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.
|
|
186
|
+
|
|
187
|
+
Approved 2026-10-18.
|
|
188
|
+
|
|
183
189
|
### `behavior:snapshot-filter-context@ruby`
|
|
184
190
|
|
|
185
191
|
mini_racer_rs returns the same filtered results from a snapshot-based context as from a context without a snapshot. In a context made from `MiniRacer::Snapshot.new("Object.getOwnPropertyDescriptors = () => ({})")`, `({a: 1, f() {}})` returns `{"a" => 1}`, and `[1, function() {}]` returns `[1]`. mini_racer 0.22.1 returns `nil` for both. Both gems copy results that V8 refuses to serialize as is through a filter in a separate safe context. mini_racer_rs keeps an untouched context in each snapshot blob and uses it for that filter, while mini_racer 0.22.1 builds the safe context from the snapshot, exposing the filter's built-ins to snapshot code. User code still sees the replacement in both gems: `Object.getOwnPropertyDescriptors({a: 1})` returns `{}`.
|
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.2"
|
|
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.2"
|
|
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.2"
|
|
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.2"
|
|
493
493
|
dependencies = [
|
|
494
494
|
"proptest",
|
|
495
495
|
"thiserror",
|
data/Cargo.toml
CHANGED
data/README.md
CHANGED
|
@@ -77,15 +77,26 @@ impl Drop for Building {
|
|
|
77
77
|
|
|
78
78
|
/// How the runner compiles a large script.
|
|
79
79
|
pub(crate) enum Plan {
|
|
80
|
-
/// From source: a first sighting
|
|
81
|
-
///
|
|
82
|
-
Compile,
|
|
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.
|
|
82
|
+
Compile(Option<Key>),
|
|
83
83
|
/// From source, then a cache made once the dispatch is over.
|
|
84
84
|
Make(Building),
|
|
85
85
|
/// From the entry, falling back to source if V8 refuses it.
|
|
86
86
|
Consume(Arc<Entry>, Key),
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
impl Plan {
|
|
90
|
+
/// The key of a script the process has seen before.
|
|
91
|
+
pub(crate) const fn key(&self) -> Option<&Key> {
|
|
92
|
+
match self {
|
|
93
|
+
Self::Compile(key) => key.as_ref(),
|
|
94
|
+
Self::Make(building) => Some(&building.key),
|
|
95
|
+
Self::Consume(_, key) => Some(key),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
impl CodeCache {
|
|
90
101
|
pub(crate) fn plan(
|
|
91
102
|
&'static self,
|
|
@@ -94,7 +105,7 @@ impl CodeCache {
|
|
|
94
105
|
snapshot: Option<u64>,
|
|
95
106
|
) -> Plan {
|
|
96
107
|
if !enabled() {
|
|
97
|
-
return Plan::Compile;
|
|
108
|
+
return Plan::Compile(None);
|
|
98
109
|
}
|
|
99
110
|
let sighting = self.sighting(filename, source, snapshot);
|
|
100
111
|
let seen = touch(&self.sightings, sighting, MAX_SIGHTINGS);
|
|
@@ -103,7 +114,7 @@ impl CodeCache {
|
|
|
103
114
|
.iter()
|
|
104
115
|
.any(|entry| entry.key.sighting == sighting);
|
|
105
116
|
if !seen && !cached {
|
|
106
|
-
return Plan::Compile;
|
|
117
|
+
return Plan::Compile(None);
|
|
107
118
|
}
|
|
108
119
|
let key = Key {
|
|
109
120
|
filename: filename.clone(),
|
|
@@ -121,9 +132,10 @@ impl CodeCache {
|
|
|
121
132
|
}
|
|
122
133
|
drop(entries);
|
|
123
134
|
if lock(&self.refused).contains(&key) {
|
|
124
|
-
return Plan::Compile;
|
|
135
|
+
return Plan::Compile(Some(key));
|
|
125
136
|
}
|
|
126
|
-
self.begin(key
|
|
137
|
+
self.begin(key.clone())
|
|
138
|
+
.map_or(Plan::Compile(Some(key)), Plan::Make)
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
/// Claims `key` for this context to build, unless another is building it.
|
|
@@ -300,7 +312,7 @@ mod tests {
|
|
|
300
312
|
let cache = cache();
|
|
301
313
|
assert!(matches!(
|
|
302
314
|
cache.plan(&name(), &source(1), None),
|
|
303
|
-
Plan::Compile
|
|
315
|
+
Plan::Compile(None)
|
|
304
316
|
));
|
|
305
317
|
cached(cache, &source(1), Box::from(&b"compiled"[..]));
|
|
306
318
|
assert!(matches!(
|
|
@@ -309,11 +321,11 @@ mod tests {
|
|
|
309
321
|
));
|
|
310
322
|
assert!(matches!(
|
|
311
323
|
cache.plan(&name(), &source(1), Some(7)),
|
|
312
|
-
Plan::Compile
|
|
324
|
+
Plan::Compile(None)
|
|
313
325
|
));
|
|
314
326
|
assert!(matches!(
|
|
315
327
|
cache.plan(&JsText::from("other.js"), &source(1), None),
|
|
316
|
-
Plan::Compile
|
|
328
|
+
Plan::Compile(None)
|
|
317
329
|
));
|
|
318
330
|
}
|
|
319
331
|
|
|
@@ -355,10 +367,9 @@ mod tests {
|
|
|
355
367
|
let _ = cache.plan(&name(), &source(3), None);
|
|
356
368
|
let first = cache.plan(&name(), &source(3), None);
|
|
357
369
|
assert!(matches!(first, Plan::Make(..)));
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
));
|
|
370
|
+
let elsewhere = cache.plan(&name(), &source(3), None);
|
|
371
|
+
assert!(matches!(elsewhere, Plan::Compile(Some(_))));
|
|
372
|
+
assert!(elsewhere.key() == first.key());
|
|
362
373
|
drop(first);
|
|
363
374
|
assert!(matches!(
|
|
364
375
|
cache.plan(&name(), &source(3), None),
|
|
@@ -402,7 +413,7 @@ mod tests {
|
|
|
402
413
|
assert_eq!(cache.stored(), 1);
|
|
403
414
|
assert!(matches!(
|
|
404
415
|
cache.plan(&name(), &source(7), None),
|
|
405
|
-
Plan::Compile
|
|
416
|
+
Plan::Compile(Some(_))
|
|
406
417
|
));
|
|
407
418
|
assert!(matches!(
|
|
408
419
|
cache.plan(&name(), &source(6), None),
|
|
@@ -7,6 +7,7 @@ use mini_racer_sync::{ForkGate, Phase, lock};
|
|
|
7
7
|
|
|
8
8
|
use crate::control::ControlBlock;
|
|
9
9
|
use crate::process::code_cache::CodeCache;
|
|
10
|
+
use crate::process::sources::Sources;
|
|
10
11
|
use crate::process::tasks::TaskRegistry;
|
|
11
12
|
use crate::process::watchdog::Watchdog;
|
|
12
13
|
use crate::process::workers::Workers;
|
|
@@ -51,13 +52,14 @@ pub struct Generation {
|
|
|
51
52
|
watchdog: Mutex<Option<Arc<Watchdog>>>,
|
|
52
53
|
workers: Workers,
|
|
53
54
|
code_cache: CodeCache,
|
|
55
|
+
sources: Sources,
|
|
54
56
|
next: OnceLock<Box<Self>>,
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
static ROOT: OnceLock<Generation> = OnceLock::new();
|
|
58
60
|
|
|
59
61
|
impl Generation {
|
|
60
|
-
fn new(epoch: u64, quiesced: bool, code_cache: CodeCache) -> Self {
|
|
62
|
+
fn new(epoch: u64, quiesced: bool, code_cache: CodeCache, sources: Sources) -> Self {
|
|
61
63
|
Self {
|
|
62
64
|
epoch,
|
|
63
65
|
quiesced,
|
|
@@ -69,6 +71,7 @@ impl Generation {
|
|
|
69
71
|
watchdog: Mutex::new(None),
|
|
70
72
|
workers: Workers::default(),
|
|
71
73
|
code_cache,
|
|
74
|
+
sources,
|
|
72
75
|
next: OnceLock::new(),
|
|
73
76
|
}
|
|
74
77
|
}
|
|
@@ -94,6 +97,11 @@ impl Generation {
|
|
|
94
97
|
&self.code_cache
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
#[must_use]
|
|
101
|
+
pub const fn sources(&self) -> &Sources {
|
|
102
|
+
&self.sources
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
pub fn register(&self, block: &Arc<ControlBlock>) {
|
|
98
106
|
let mut blocks = lock(&self.blocks);
|
|
99
107
|
blocks.retain(|block| block.strong_count() > 0);
|
|
@@ -115,7 +123,8 @@ impl Generation {
|
|
|
115
123
|
|
|
116
124
|
pub fn current() -> &'static Generation {
|
|
117
125
|
let epoch = epoch();
|
|
118
|
-
let mut generation =
|
|
126
|
+
let mut generation =
|
|
127
|
+
ROOT.get_or_init(|| Generation::new(epoch, true, CodeCache::default(), Sources::default()));
|
|
119
128
|
while generation.epoch != epoch {
|
|
120
129
|
let quiesced = generation.quiesced && generation.prepared.load(Ordering::SeqCst);
|
|
121
130
|
let next = generation.epoch.saturating_add(1);
|
|
@@ -125,6 +134,7 @@ pub fn current() -> &'static Generation {
|
|
|
125
134
|
next,
|
|
126
135
|
quiesced,
|
|
127
136
|
parent.code_cache.inherited(),
|
|
137
|
+
parent.sources.inherited(),
|
|
128
138
|
))
|
|
129
139
|
});
|
|
130
140
|
}
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
2
|
+
use std::sync::{Arc, Mutex};
|
|
3
|
+
|
|
4
|
+
use mini_racer_sync::lock;
|
|
5
|
+
|
|
6
|
+
use crate::control::JsText;
|
|
7
|
+
use crate::process::code_cache::Key;
|
|
8
|
+
|
|
9
|
+
const MAX_SOURCES: usize = 8;
|
|
10
|
+
const MAX_BYTES: usize = 256 << 20;
|
|
11
|
+
const MAX_REFUSED: usize = 8;
|
|
12
|
+
|
|
13
|
+
/// A large script's source as V8 stores a string of it: a byte a unit when
|
|
14
|
+
/// every unit fits Latin-1, two otherwise. Never changed once made.
|
|
15
|
+
#[derive(Clone)]
|
|
16
|
+
pub(crate) enum Units {
|
|
17
|
+
OneByte(Arc<[u8]>),
|
|
18
|
+
TwoByte(Arc<[u16]>),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl Units {
|
|
22
|
+
// V8 decodes valid UTF-8 to the same units; invalid UTF-8 is left to its
|
|
23
|
+
// own decoder, whose replacements this does not repeat.
|
|
24
|
+
fn of(text: &JsText) -> Option<Self> {
|
|
25
|
+
Some(match text {
|
|
26
|
+
JsText::Latin1(bytes) => Self::OneByte(Arc::from(bytes.as_slice())),
|
|
27
|
+
JsText::Utf8(bytes) if bytes.is_ascii() => Self::OneByte(Arc::from(bytes.as_slice())),
|
|
28
|
+
JsText::Utf8(bytes) => {
|
|
29
|
+
let text = std::str::from_utf8(bytes).ok()?;
|
|
30
|
+
text.chars()
|
|
31
|
+
.map(u8::try_from)
|
|
32
|
+
.collect::<Result<_, _>>()
|
|
33
|
+
.map_or_else(
|
|
34
|
+
|_| Self::TwoByte(text.encode_utf16().collect()),
|
|
35
|
+
Self::OneByte,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
JsText::Utf16(units) => units
|
|
39
|
+
.iter()
|
|
40
|
+
.map(|&unit| u8::try_from(unit))
|
|
41
|
+
.collect::<Result<_, _>>()
|
|
42
|
+
.map_or_else(
|
|
43
|
+
|_| Self::TwoByte(Arc::from(units.as_slice())),
|
|
44
|
+
Self::OneByte,
|
|
45
|
+
),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn bytes(&self) -> usize {
|
|
50
|
+
match self {
|
|
51
|
+
Self::OneByte(bytes) => bytes.len(),
|
|
52
|
+
Self::TwoByte(units) => units.len() * 2,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn references(&self) -> usize {
|
|
57
|
+
match self {
|
|
58
|
+
Self::OneByte(bytes) => Arc::strong_count(bytes),
|
|
59
|
+
Self::TwoByte(units) => Arc::strong_count(units),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// One copy of each large script's source, outside every V8 heap.
|
|
65
|
+
///
|
|
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.
|
|
68
|
+
/// Each string V8 makes from one holds a reference to it, which V8 hands back
|
|
69
|
+
/// when the string dies (`sys::external_string`). A source is dropped only
|
|
70
|
+
/// when no string holds one, so V8 never releases the last reference itself.
|
|
71
|
+
pub struct Sources {
|
|
72
|
+
list: Mutex<List>,
|
|
73
|
+
// How many sources, and how many bytes of them, may be kept and read.
|
|
74
|
+
bounds: (usize, usize),
|
|
75
|
+
made: AtomicU64,
|
|
76
|
+
reads: AtomicU64,
|
|
77
|
+
conversions: AtomicU64,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Default for Sources {
|
|
81
|
+
fn default() -> Self {
|
|
82
|
+
Self::bounded(MAX_SOURCES, MAX_BYTES)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[derive(Default)]
|
|
87
|
+
struct List {
|
|
88
|
+
// Most recently used last.
|
|
89
|
+
kept: Vec<Kept>,
|
|
90
|
+
// What each source converted and left unkept takes, too much for the room
|
|
91
|
+
// left or, as invalid UTF-8, unbounded, so that while it is among the last
|
|
92
|
+
// `MAX_REFUSED` refused it is not converted again without room for it.
|
|
93
|
+
refused: Vec<(Key, usize)>,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
struct Kept {
|
|
97
|
+
key: Key,
|
|
98
|
+
units: Units,
|
|
99
|
+
// The references from this process's list and every ancestor's, which a
|
|
100
|
+
// forked child never touches again; the strings reading it hold the rest.
|
|
101
|
+
held: usize,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl Kept {
|
|
105
|
+
fn unread(&self) -> bool {
|
|
106
|
+
self.units.references() == self.held
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl List {
|
|
111
|
+
fn find(&mut self, key: &Key) -> Option<Units> {
|
|
112
|
+
let at = self.kept.iter().position(|kept| kept.key == *key)?;
|
|
113
|
+
let found = self.kept.remove(at);
|
|
114
|
+
let units = found.units.clone();
|
|
115
|
+
self.kept.push(found);
|
|
116
|
+
Some(units)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Whether `bytes` more fit beside what strings still read, which nothing
|
|
120
|
+
// can drop.
|
|
121
|
+
fn room(&self, bytes: usize, (sources, most): (usize, usize)) -> bool {
|
|
122
|
+
let (read, read_bytes) = self
|
|
123
|
+
.kept
|
|
124
|
+
.iter()
|
|
125
|
+
.filter(|kept| !kept.unread())
|
|
126
|
+
.fold((0, 0), |(count, total), kept| {
|
|
127
|
+
(count + 1, total + kept.units.bytes())
|
|
128
|
+
});
|
|
129
|
+
read < sources && read_bytes.saturating_add(bytes) <= most
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn refuse(&mut self, key: &Key, bytes: usize) {
|
|
133
|
+
self.refused.retain(|(refused, _)| refused != key);
|
|
134
|
+
if self.refused.len() >= MAX_REFUSED {
|
|
135
|
+
self.refused.remove(0);
|
|
136
|
+
}
|
|
137
|
+
self.refused.push((key.clone(), bytes));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn least_bytes(&self, key: &Key, text: &JsText) -> usize {
|
|
141
|
+
self.refused
|
|
142
|
+
.iter()
|
|
143
|
+
.find(|(refused, _)| refused == key)
|
|
144
|
+
.map_or_else(|| least_bytes(text), |&(_, bytes)| bytes)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The fewest bytes `text` can take as units, so a source that cannot fit is
|
|
149
|
+
// never converted: UTF-8 takes at least a byte for every two, as Latin-1
|
|
150
|
+
// written in UTF-8 does.
|
|
151
|
+
const fn least_bytes(text: &JsText) -> usize {
|
|
152
|
+
match text {
|
|
153
|
+
JsText::Latin1(bytes) => bytes.len(),
|
|
154
|
+
JsText::Utf8(bytes) => bytes.len().div_ceil(2),
|
|
155
|
+
JsText::Utf16(units) => units.len(),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
impl Sources {
|
|
160
|
+
fn bounded(sources: usize, bytes: usize) -> Self {
|
|
161
|
+
Self {
|
|
162
|
+
list: Mutex::default(),
|
|
163
|
+
bounds: (sources, bytes),
|
|
164
|
+
made: AtomicU64::new(0),
|
|
165
|
+
reads: AtomicU64::new(0),
|
|
166
|
+
conversions: AtomicU64::new(0),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// 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> {
|
|
175
|
+
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
|
+
}
|
|
182
|
+
drop(list);
|
|
183
|
+
self.conversions.fetch_add(1, Ordering::Relaxed);
|
|
184
|
+
let units = Units::of(text);
|
|
185
|
+
let mut list = lock(&self.list);
|
|
186
|
+
// Made meanwhile by a context loading the same script.
|
|
187
|
+
if let Some(found) = list.find(key) {
|
|
188
|
+
return Some(found);
|
|
189
|
+
}
|
|
190
|
+
let bytes = units.as_ref().map_or(usize::MAX, Units::bytes);
|
|
191
|
+
if !list.room(bytes, self.bounds) {
|
|
192
|
+
list.refuse(key, bytes);
|
|
193
|
+
drop(list);
|
|
194
|
+
return None;
|
|
195
|
+
}
|
|
196
|
+
let units = units?;
|
|
197
|
+
list.refused.retain(|(refused, _)| refused != key);
|
|
198
|
+
let (sources, most) = self.bounds;
|
|
199
|
+
let mut dropped = Vec::new();
|
|
200
|
+
while list.kept.len() >= sources
|
|
201
|
+
|| list
|
|
202
|
+
.kept
|
|
203
|
+
.iter()
|
|
204
|
+
.map(|kept| kept.units.bytes())
|
|
205
|
+
.sum::<usize>()
|
|
206
|
+
+ bytes
|
|
207
|
+
> most
|
|
208
|
+
{
|
|
209
|
+
let Some(at) = list.kept.iter().position(Kept::unread) else {
|
|
210
|
+
break;
|
|
211
|
+
};
|
|
212
|
+
dropped.push(list.kept.remove(at));
|
|
213
|
+
}
|
|
214
|
+
list.kept.push(Kept {
|
|
215
|
+
key: key.clone(),
|
|
216
|
+
units: units.clone(),
|
|
217
|
+
held: 1,
|
|
218
|
+
});
|
|
219
|
+
drop(list);
|
|
220
|
+
// Freed once the lock is released, as a large source takes a while.
|
|
221
|
+
drop(dropped);
|
|
222
|
+
self.made.fetch_add(1, Ordering::Relaxed);
|
|
223
|
+
Some(units)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pub(crate) fn read(&self) {
|
|
227
|
+
self.reads.fetch_add(1, Ordering::Relaxed);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// A forked child's sources: what its parent kept, unless a thread of the
|
|
231
|
+
/// parent held them at the fork. The parent's list keeps its references
|
|
232
|
+
/// in the child, so the child frees none of these.
|
|
233
|
+
pub(crate) fn inherited(&self) -> Self {
|
|
234
|
+
let list = self
|
|
235
|
+
.list
|
|
236
|
+
.try_lock()
|
|
237
|
+
.map(|list| List {
|
|
238
|
+
kept: list
|
|
239
|
+
.kept
|
|
240
|
+
.iter()
|
|
241
|
+
.map(|kept| Kept {
|
|
242
|
+
key: kept.key.clone(),
|
|
243
|
+
units: kept.units.clone(),
|
|
244
|
+
held: kept.held + 1,
|
|
245
|
+
})
|
|
246
|
+
.collect(),
|
|
247
|
+
refused: list.refused.clone(),
|
|
248
|
+
})
|
|
249
|
+
.unwrap_or_default();
|
|
250
|
+
Self {
|
|
251
|
+
list: Mutex::new(list),
|
|
252
|
+
..Self::bounded(self.bounds.0, self.bounds.1)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Sources this process made and kept.
|
|
257
|
+
#[must_use]
|
|
258
|
+
pub fn made(&self) -> u64 {
|
|
259
|
+
self.made.load(Ordering::Relaxed)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Strings this process's contexts made from a kept source.
|
|
263
|
+
#[must_use]
|
|
264
|
+
pub fn reads(&self) -> u64 {
|
|
265
|
+
self.reads.load(Ordering::Relaxed)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Sources this process converted to the units V8 reads, kept or not.
|
|
269
|
+
#[must_use]
|
|
270
|
+
pub fn conversions(&self) -> u64 {
|
|
271
|
+
self.conversions.load(Ordering::Relaxed)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Strings alive now that read a kept source.
|
|
275
|
+
#[must_use]
|
|
276
|
+
pub fn reading(&self) -> usize {
|
|
277
|
+
lock(&self.list)
|
|
278
|
+
.kept
|
|
279
|
+
.iter()
|
|
280
|
+
.map(|kept| kept.units.references().saturating_sub(kept.held))
|
|
281
|
+
.sum()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[cfg(test)]
|
|
286
|
+
#[allow(clippy::panic, reason = "a test fails by panicking")]
|
|
287
|
+
mod tests {
|
|
288
|
+
use std::sync::Arc;
|
|
289
|
+
|
|
290
|
+
use super::{MAX_SOURCES, Sources, Units};
|
|
291
|
+
use crate::control::JsText;
|
|
292
|
+
use crate::process::code_cache::{CodeCache, Key};
|
|
293
|
+
|
|
294
|
+
// A script's key once the code cache has seen it twice.
|
|
295
|
+
fn key(text: &JsText) -> Key {
|
|
296
|
+
let cache: &'static CodeCache = Box::leak(Box::default());
|
|
297
|
+
let name = JsText::from("bundle.js");
|
|
298
|
+
let _ = cache.plan(&name, text, None);
|
|
299
|
+
cache
|
|
300
|
+
.plan(&name, text, None)
|
|
301
|
+
.key()
|
|
302
|
+
.cloned()
|
|
303
|
+
.unwrap_or_else(|| panic!("a second sighting has a key"))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fn script(seed: u8) -> JsText {
|
|
307
|
+
JsText::Latin1(vec![seed; 2 << 20])
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn one_byte(units: Option<Units>) -> Arc<[u8]> {
|
|
311
|
+
match units {
|
|
312
|
+
Some(Units::OneByte(bytes)) => bytes,
|
|
313
|
+
_ => panic!("one byte a unit"),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn two_byte(units: Option<Units>) -> Arc<[u16]> {
|
|
318
|
+
match units {
|
|
319
|
+
Some(Units::TwoByte(units)) => units,
|
|
320
|
+
_ => panic!("two bytes a unit"),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[test]
|
|
325
|
+
fn keeps_a_source_in_the_form_v8_stores_a_string_of_it() {
|
|
326
|
+
assert_eq!(
|
|
327
|
+
*one_byte(Units::of(&JsText::Latin1(vec![b'a', 0xE9]))),
|
|
328
|
+
[b'a', 0xE9]
|
|
329
|
+
);
|
|
330
|
+
assert_eq!(*one_byte(Units::of(&JsText::from("a\0b"))), *b"a\0b");
|
|
331
|
+
assert_eq!(
|
|
332
|
+
*two_byte(Units::of(&JsText::from("\u{FEFF}a"))),
|
|
333
|
+
[0xFEFF, 0x61]
|
|
334
|
+
);
|
|
335
|
+
assert_eq!(
|
|
336
|
+
*one_byte(Units::of(&JsText::from("caf\u{E9}\u{FF}"))),
|
|
337
|
+
*b"caf\xE9\xFF"
|
|
338
|
+
);
|
|
339
|
+
assert_eq!(
|
|
340
|
+
*two_byte(Units::of(&JsText::from("a\u{100}\u{1F600}"))),
|
|
341
|
+
[0x61, 0x100, 0xD83D, 0xDE00]
|
|
342
|
+
);
|
|
343
|
+
assert_eq!(
|
|
344
|
+
*one_byte(Units::of(&JsText::Utf16(vec![0x61, 0xE9]))),
|
|
345
|
+
[0x61, 0xE9]
|
|
346
|
+
);
|
|
347
|
+
assert_eq!(
|
|
348
|
+
*two_byte(Units::of(&JsText::Utf16(vec![0xD800, 0x61, 0xDC00]))),
|
|
349
|
+
[0xD800, 0x61, 0xDC00]
|
|
350
|
+
);
|
|
351
|
+
assert!(Units::of(&JsText::Utf8(vec![b'a', 0xFF])).is_none());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn every_context_compiling_a_script_reads_one_copy() {
|
|
356
|
+
let sources = Sources::default();
|
|
357
|
+
let text = script(1);
|
|
358
|
+
let key = key(&text);
|
|
359
|
+
let first = one_byte(sources.units(&key, &text));
|
|
360
|
+
let second = one_byte(sources.units(&key, &text));
|
|
361
|
+
assert!(Arc::ptr_eq(&first, &second));
|
|
362
|
+
assert_eq!(sources.made(), 1);
|
|
363
|
+
assert_eq!(sources.reading(), 2);
|
|
364
|
+
drop((first, second));
|
|
365
|
+
assert_eq!(sources.reading(), 0);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn same(first: Option<&Units>, second: Option<&Units>) -> bool {
|
|
369
|
+
match (first, second) {
|
|
370
|
+
(Some(Units::OneByte(first)), Some(Units::OneByte(second))) => {
|
|
371
|
+
Arc::ptr_eq(first, second)
|
|
372
|
+
}
|
|
373
|
+
(Some(Units::TwoByte(first)), Some(Units::TwoByte(second))) => {
|
|
374
|
+
Arc::ptr_eq(first, second)
|
|
375
|
+
}
|
|
376
|
+
_ => false,
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[test]
|
|
381
|
+
fn a_source_still_read_is_kept_and_an_unread_one_makes_room() {
|
|
382
|
+
let sources = Sources::default();
|
|
383
|
+
let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
|
|
384
|
+
let texts: Vec<JsText> = (10..10 + count).map(script).collect();
|
|
385
|
+
let keys: Vec<Key> = texts.iter().map(key).collect();
|
|
386
|
+
let mut read: Vec<Option<Units>> = texts
|
|
387
|
+
.iter()
|
|
388
|
+
.zip(&keys)
|
|
389
|
+
.map(|(text, key)| sources.units(key, text))
|
|
390
|
+
.collect();
|
|
391
|
+
let extra = script(100);
|
|
392
|
+
let extra_key = key(&extra);
|
|
393
|
+
assert!(sources.units(&extra_key, &extra).is_none());
|
|
394
|
+
drop(read.remove(3));
|
|
395
|
+
drop(read.remove(0));
|
|
396
|
+
drop(sources.units(&keys[3], &texts[3]));
|
|
397
|
+
// Of the two unread, the least recently used makes room.
|
|
398
|
+
assert!(sources.units(&extra_key, &extra).is_some());
|
|
399
|
+
let made = sources.made();
|
|
400
|
+
assert_eq!(made, u64::from(count) + 1);
|
|
401
|
+
drop(sources.units(&keys[3], &texts[3]));
|
|
402
|
+
assert_eq!(sources.made(), made);
|
|
403
|
+
drop(sources.units(&keys[0], &texts[0]));
|
|
404
|
+
assert_eq!(sources.made(), made + 1);
|
|
405
|
+
for (at, held) in [1, 2, 4, 5, 6, 7].into_iter().zip(&read) {
|
|
406
|
+
assert!(same(
|
|
407
|
+
sources.units(&keys[at], &texts[at]).as_ref(),
|
|
408
|
+
held.as_ref()
|
|
409
|
+
));
|
|
410
|
+
}
|
|
411
|
+
assert_eq!(sources.made(), made + 1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[test]
|
|
415
|
+
fn a_full_list_converts_nothing() {
|
|
416
|
+
let sources = Sources::default();
|
|
417
|
+
let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
|
|
418
|
+
let read: Vec<Option<Units>> = (40..40 + count)
|
|
419
|
+
.map(|seed| {
|
|
420
|
+
let text = script(seed);
|
|
421
|
+
sources.units(&key(&text), &text)
|
|
422
|
+
})
|
|
423
|
+
.collect();
|
|
424
|
+
let converted = sources.conversions();
|
|
425
|
+
let extra = script(100);
|
|
426
|
+
let extra_key = key(&extra);
|
|
427
|
+
for _ in 0..3 {
|
|
428
|
+
assert!(sources.units(&extra_key, &extra).is_none());
|
|
429
|
+
}
|
|
430
|
+
assert_eq!(sources.conversions(), converted);
|
|
431
|
+
drop(read);
|
|
432
|
+
assert!(sources.units(&extra_key, &extra).is_some());
|
|
433
|
+
assert_eq!(sources.conversions(), converted + 1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Under a 5 MiB bound with 2 MiB still read, 4 MiB of Latin-1 cannot fit
|
|
437
|
+
// and is never converted; 2 MiB of UTF-8 may fit, until converting it
|
|
438
|
+
// shows one astral character makes it 4.
|
|
439
|
+
#[test]
|
|
440
|
+
fn a_source_is_converted_only_while_it_may_fit() {
|
|
441
|
+
let sources = Sources::bounded(MAX_SOURCES, 5 << 20);
|
|
442
|
+
let read = script(50);
|
|
443
|
+
let still_read = sources.units(&key(&read), &read);
|
|
444
|
+
assert!(still_read.is_some());
|
|
445
|
+
let too_large = JsText::Latin1(vec![51; 4 << 20]);
|
|
446
|
+
assert!(sources.units(&key(&too_large), &too_large).is_none());
|
|
447
|
+
assert_eq!(sources.conversions(), 1);
|
|
448
|
+
let mut astral = vec![b'a'; 2 << 20];
|
|
449
|
+
astral.extend_from_slice("\u{1F600}".as_bytes());
|
|
450
|
+
let astral = JsText::Utf8(astral);
|
|
451
|
+
let astral_key = key(&astral);
|
|
452
|
+
for _ in 0..3 {
|
|
453
|
+
assert!(sources.units(&astral_key, &astral).is_none());
|
|
454
|
+
}
|
|
455
|
+
assert_eq!(sources.conversions(), 2);
|
|
456
|
+
drop(still_read);
|
|
457
|
+
assert!(two_byte(sources.units(&astral_key, &astral)).len() > 2 << 20);
|
|
458
|
+
assert_eq!((sources.conversions(), sources.made()), (3, 2));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#[test]
|
|
462
|
+
fn invalid_utf8_is_tried_once() {
|
|
463
|
+
let sources = Sources::default();
|
|
464
|
+
let mut invalid = vec![b'a'; 2 << 20];
|
|
465
|
+
invalid.push(0xFF);
|
|
466
|
+
let invalid = JsText::Utf8(invalid);
|
|
467
|
+
let invalid_key = key(&invalid);
|
|
468
|
+
for _ in 0..3 {
|
|
469
|
+
assert!(sources.units(&invalid_key, &invalid).is_none());
|
|
470
|
+
}
|
|
471
|
+
assert_eq!((sources.conversions(), sources.made()), (1, 0));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#[test]
|
|
475
|
+
fn a_child_reads_its_parents_copy_and_never_frees_it() {
|
|
476
|
+
let parent: &'static Sources = Box::leak(Box::default());
|
|
477
|
+
let text = script(2);
|
|
478
|
+
let key = key(&text);
|
|
479
|
+
let shared = parent.units(&key, &text);
|
|
480
|
+
let child = parent.inherited();
|
|
481
|
+
let in_child = child.units(&key, &text);
|
|
482
|
+
assert!(same(shared.as_ref(), in_child.as_ref()));
|
|
483
|
+
assert_eq!(child.reading(), 2);
|
|
484
|
+
let copy = Arc::downgrade(&one_byte(shared));
|
|
485
|
+
drop(in_child);
|
|
486
|
+
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
|
+
}
|
|
491
|
+
assert_eq!(copy.strong_count(), 1);
|
|
492
|
+
let again = one_byte(child.units(&key, &text));
|
|
493
|
+
assert!(
|
|
494
|
+
!copy
|
|
495
|
+
.upgrade()
|
|
496
|
+
.is_some_and(|copy| Arc::ptr_eq(©, &again))
|
|
497
|
+
);
|
|
498
|
+
let from_parent = one_byte(parent.units(&key, &text));
|
|
499
|
+
assert!(
|
|
500
|
+
copy.upgrade()
|
|
501
|
+
.is_some_and(|copy| Arc::ptr_eq(©, &from_parent))
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// A child's inherited copies stay read while its parent's strings hold
|
|
506
|
+
// them, so a child forked with a full list must not convert either.
|
|
507
|
+
#[test]
|
|
508
|
+
fn a_child_inheriting_a_full_list_converts_nothing() {
|
|
509
|
+
let parent = Sources::default();
|
|
510
|
+
let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
|
|
511
|
+
let read: Vec<Option<Units>> = (60..60 + count)
|
|
512
|
+
.map(|seed| {
|
|
513
|
+
let text = script(seed);
|
|
514
|
+
parent.units(&key(&text), &text)
|
|
515
|
+
})
|
|
516
|
+
.collect();
|
|
517
|
+
let child = parent.inherited();
|
|
518
|
+
let extra = script(110);
|
|
519
|
+
let extra_key = key(&extra);
|
|
520
|
+
for _ in 0..3 {
|
|
521
|
+
assert!(child.units(&extra_key, &extra).is_none());
|
|
522
|
+
}
|
|
523
|
+
assert_eq!(child.conversions(), 0);
|
|
524
|
+
drop(read);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#[test]
|
|
528
|
+
fn a_child_forked_while_the_sources_were_held_starts_without_them() {
|
|
529
|
+
let parent = Sources::default();
|
|
530
|
+
let text = script(3);
|
|
531
|
+
let key = key(&text);
|
|
532
|
+
let shared = one_byte(parent.units(&key, &text));
|
|
533
|
+
let held = parent.list.lock();
|
|
534
|
+
let child = parent.inherited();
|
|
535
|
+
drop(held);
|
|
536
|
+
assert!(!Arc::ptr_eq(&shared, &one_byte(child.units(&key, &text))));
|
|
537
|
+
assert_eq!(child.made(), 1);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
@@ -16,7 +16,7 @@ use crate::control::{
|
|
|
16
16
|
CallRequest, CallbackReply, ControlBlock, Depth, IsolateTerminator, JsText, Request,
|
|
17
17
|
};
|
|
18
18
|
use crate::fast_calls::{self, Hoisted, Shape};
|
|
19
|
-
use crate::process::code_cache::{Building, Plan};
|
|
19
|
+
use crate::process::code_cache::{Building, Key, Plan};
|
|
20
20
|
use crate::process::fork::Generation;
|
|
21
21
|
use crate::process::platform::{self, Mode};
|
|
22
22
|
use crate::process::watchdog;
|
|
@@ -775,10 +775,12 @@ fn evaluate(
|
|
|
775
775
|
.code_cache()
|
|
776
776
|
.plan(filename, source, runtime.snapshot)
|
|
777
777
|
} else {
|
|
778
|
-
Plan::Compile
|
|
778
|
+
Plan::Compile(None)
|
|
779
779
|
};
|
|
780
780
|
let too_large = || EngineError::Internal("source too large".to_owned());
|
|
781
|
-
let source =
|
|
781
|
+
let source = shared_source(tc, runtime, plan.key(), source)
|
|
782
|
+
.or_else(|| string(tc, source))
|
|
783
|
+
.ok_or_else(too_large)?;
|
|
782
784
|
let name = string(tc, filename).ok_or_else(too_large)?;
|
|
783
785
|
let origin = serialize::origin(tc, name.into());
|
|
784
786
|
let (script, make) = compile(tc, runtime, source, &origin, plan);
|
|
@@ -794,6 +796,25 @@ fn evaluate(
|
|
|
794
796
|
Ok(reply)
|
|
795
797
|
}
|
|
796
798
|
|
|
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.
|
|
803
|
+
fn shared_source<'s>(
|
|
804
|
+
scope: &v8::PinScope<'s, '_>,
|
|
805
|
+
runtime: &Runtime,
|
|
806
|
+
key: Option<&Key>,
|
|
807
|
+
source: &JsText,
|
|
808
|
+
) -> Option<v8::Local<'s, v8::String>> {
|
|
809
|
+
if runtime.block.config.max_memory.is_some() {
|
|
810
|
+
return None;
|
|
811
|
+
}
|
|
812
|
+
let sources = runtime.generation.sources();
|
|
813
|
+
let string = sys::external_string(scope, &sources.units(key?, source)?)?;
|
|
814
|
+
sources.read();
|
|
815
|
+
Some(string)
|
|
816
|
+
}
|
|
817
|
+
|
|
797
818
|
fn settle<'s>(
|
|
798
819
|
tc: &mut Tc<'s, '_, '_, '_>,
|
|
799
820
|
runtime: &Runtime,
|
|
@@ -1045,7 +1066,7 @@ fn compile<'s>(
|
|
|
1045
1066
|
plan: Plan,
|
|
1046
1067
|
) -> (Option<v8::Local<'s, v8::Script>>, Option<Building>) {
|
|
1047
1068
|
let (entry, key) = match plan {
|
|
1048
|
-
Plan::Compile => return (v8::Script::compile(scope, source, Some(origin)), None),
|
|
1069
|
+
Plan::Compile(_) => return (v8::Script::compile(scope, source, Some(origin)), None),
|
|
1049
1070
|
Plan::Make(building) => {
|
|
1050
1071
|
return (
|
|
1051
1072
|
v8::Script::compile(scope, source, Some(origin)),
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
use std::ffi::c_void;
|
|
1
|
+
use std::ffi::{c_char, c_void};
|
|
2
2
|
use std::ptr::NonNull;
|
|
3
|
+
use std::sync::Arc;
|
|
3
4
|
use std::time::Duration;
|
|
4
5
|
|
|
6
|
+
use crate::process::sources::Units;
|
|
7
|
+
|
|
5
8
|
// The prebuilt V8 archive marks every heap mapping MADV_DONTFORK, so a forked
|
|
6
9
|
// child would lose the isolate's memory. `-Wl,--wrap=madvise` (build.rs) routes
|
|
7
10
|
// V8's calls here; this drops that one advice and forwards the rest unchanged.
|
|
@@ -47,6 +50,77 @@ pub fn startup_data(blob: &crate::SnapshotBlob) -> v8::StartupData {
|
|
|
47
50
|
v8::StartupData::from(bytes)
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
/// A string V8 reads from `units` in place. It holds a reference to them,
|
|
54
|
+
/// which V8 hands back through `release_one_byte` or `release_two_byte` once,
|
|
55
|
+
/// on whatever thread it runs that on: when the string dies in a collection
|
|
56
|
+
/// or its isolate's teardown, or when V8 internalizes it while it lives and
|
|
57
|
+
/// gives the units to the internalized string, to release when that one dies,
|
|
58
|
+
/// or releases them at once if that string has its own (`MigrateExternalString`
|
|
59
|
+
/// in V8's `string.cc`). `None` past V8's length limit.
|
|
60
|
+
pub fn external_string<'s>(
|
|
61
|
+
scope: &v8::PinScope<'s, '_>,
|
|
62
|
+
units: &Units,
|
|
63
|
+
) -> Option<v8::Local<'s, v8::String>> {
|
|
64
|
+
// V8 refuses a resource past its limit without disposing of it, which
|
|
65
|
+
// would keep the reference for good, so it never gets one.
|
|
66
|
+
match units {
|
|
67
|
+
Units::OneByte(bytes) if bytes.len() <= v8::String::MAX_LENGTH => {
|
|
68
|
+
let data = Arc::into_raw(Arc::clone(bytes));
|
|
69
|
+
// SAFETY: the bytes stay where they are, unchanged, while the
|
|
70
|
+
// reference made here lives; V8 only reads them, and hands the
|
|
71
|
+
// reference back through `release_one_byte`, with this pointer and
|
|
72
|
+
// length, once the string dies.
|
|
73
|
+
unsafe {
|
|
74
|
+
v8::String::new_external_onebyte_raw(
|
|
75
|
+
scope,
|
|
76
|
+
data.cast::<c_char>().cast_mut(),
|
|
77
|
+
bytes.len(),
|
|
78
|
+
release_one_byte,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
Units::TwoByte(units) if units.len() <= v8::String::MAX_LENGTH => {
|
|
83
|
+
let data = Arc::into_raw(Arc::clone(units));
|
|
84
|
+
// SAFETY: as for one byte a unit, through `release_two_byte`.
|
|
85
|
+
unsafe {
|
|
86
|
+
v8::String::new_external_twobyte_raw(
|
|
87
|
+
scope,
|
|
88
|
+
data.cast::<u16>().cast_mut(),
|
|
89
|
+
units.len(),
|
|
90
|
+
release_two_byte,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
Units::OneByte(_) | Units::TwoByte(_) => None,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Each runs once per string `external_string` made, and only releases its
|
|
99
|
+
// reference: the list in `process::sources` holds another for as long as any
|
|
100
|
+
// string reads them, so the units are never freed here.
|
|
101
|
+
unsafe extern "C" fn release_one_byte(data: *mut c_char, length: usize) {
|
|
102
|
+
let released = std::panic::catch_unwind(|| {
|
|
103
|
+
let bytes = std::ptr::slice_from_raw_parts(data.cast::<u8>().cast_const(), length);
|
|
104
|
+
// SAFETY: V8 passes back the pointer and length `external_string`
|
|
105
|
+
// gave it, once, so this releases the one reference it made.
|
|
106
|
+
unsafe { Arc::decrement_strong_count(bytes) };
|
|
107
|
+
});
|
|
108
|
+
if released.is_err() {
|
|
109
|
+
std::process::abort();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
unsafe extern "C" fn release_two_byte(data: *mut u16, length: usize) {
|
|
114
|
+
let released = std::panic::catch_unwind(|| {
|
|
115
|
+
let units = std::ptr::slice_from_raw_parts(data.cast_const(), length);
|
|
116
|
+
// SAFETY: as in `release_one_byte`.
|
|
117
|
+
unsafe { Arc::decrement_strong_count(units) };
|
|
118
|
+
});
|
|
119
|
+
if released.is_err() {
|
|
120
|
+
std::process::abort();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
50
124
|
/// The address V8 names `isolate` by when it posts the isolate a task.
|
|
51
125
|
#[must_use]
|
|
52
126
|
pub fn isolate_key(isolate: &v8::Isolate) -> usize {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Aragon
|
|
@@ -54,6 +54,7 @@ files:
|
|
|
54
54
|
- crates/mini_racer_engine/src/process/code_cache.rs
|
|
55
55
|
- crates/mini_racer_engine/src/process/fork.rs
|
|
56
56
|
- crates/mini_racer_engine/src/process/platform.rs
|
|
57
|
+
- crates/mini_racer_engine/src/process/sources.rs
|
|
57
58
|
- crates/mini_racer_engine/src/process/tasks.rs
|
|
58
59
|
- crates/mini_racer_engine/src/process/watchdog.rs
|
|
59
60
|
- crates/mini_racer_engine/src/process/workers.rs
|