mini_racer_rs 1.0.0
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 +7 -0
- data/AI-DISCLOSURE.md +13 -0
- data/CHANGELOG.md +45 -0
- data/COMPATIBILITY.md +235 -0
- data/Cargo.lock +1263 -0
- data/Cargo.toml +62 -0
- data/LICENSE +21 -0
- data/README.md +254 -0
- data/THIRD_PARTY_NOTICES.md +4501 -0
- data/crates/mini_racer_engine/Cargo.toml +25 -0
- data/crates/mini_racer_engine/build.rs +13 -0
- data/crates/mini_racer_engine/src/context.rs +802 -0
- data/crates/mini_racer_engine/src/control.rs +239 -0
- data/crates/mini_racer_engine/src/error.rs +88 -0
- data/crates/mini_racer_engine/src/fast_calls.rs +681 -0
- data/crates/mini_racer_engine/src/lib.rs +20 -0
- data/crates/mini_racer_engine/src/notify.rs +107 -0
- data/crates/mini_racer_engine/src/process/code_cache.rs +412 -0
- data/crates/mini_racer_engine/src/process/fork.rs +261 -0
- data/crates/mini_racer_engine/src/process/platform.rs +194 -0
- data/crates/mini_racer_engine/src/process/tasks.rs +96 -0
- data/crates/mini_racer_engine/src/process/watchdog.rs +185 -0
- data/crates/mini_racer_engine/src/process/workers.rs +165 -0
- data/crates/mini_racer_engine/src/process.rs +69 -0
- data/crates/mini_racer_engine/src/runner.rs +1560 -0
- data/crates/mini_racer_engine/src/serialize.rs +253 -0
- data/crates/mini_racer_engine/src/snapshot.rs +221 -0
- data/crates/mini_racer_engine/src/spin.rs +23 -0
- data/crates/mini_racer_engine/src/sys.rs +566 -0
- data/crates/mini_racer_sync/Cargo.toml +18 -0
- data/crates/mini_racer_sync/src/channel.rs +621 -0
- data/crates/mini_racer_sync/src/fork.rs +116 -0
- data/crates/mini_racer_sync/src/lib.rs +20 -0
- data/crates/mini_racer_sync/src/lock.rs +49 -0
- data/crates/mini_racer_sync/src/parker.rs +44 -0
- data/crates/mini_racer_sync/src/primitives.rs +10 -0
- data/crates/mini_racer_sync/src/tasks.rs +345 -0
- data/crates/mini_racer_sync/src/termination.rs +1234 -0
- data/crates/mini_racer_sync/src/workers.rs +283 -0
- data/crates/mini_racer_wire/Cargo.toml +18 -0
- data/crates/mini_racer_wire/src/decode.rs +926 -0
- data/crates/mini_racer_wire/src/lib.rs +67 -0
- data/crates/mini_racer_wire/src/marker.rs +43 -0
- data/crates/mini_racer_wire/src/text.rs +106 -0
- data/crates/mini_racer_wire/src/write.rs +187 -0
- data/ext/mini_racer_rs/Cargo.toml +26 -0
- data/ext/mini_racer_rs/build.rs +16 -0
- data/ext/mini_racer_rs/extconf.rb +78 -0
- data/ext/mini_racer_rs/src/context.rs +485 -0
- data/ext/mini_racer_rs/src/decode.rs +450 -0
- data/ext/mini_racer_rs/src/encode.rs +333 -0
- data/ext/mini_racer_rs/src/errors.rs +156 -0
- data/ext/mini_racer_rs/src/fork.rs +31 -0
- data/ext/mini_racer_rs/src/gc_root.rs +117 -0
- data/ext/mini_racer_rs/src/guard.rs +7 -0
- data/ext/mini_racer_rs/src/host.rs +339 -0
- data/ext/mini_racer_rs/src/lib.rs +44 -0
- data/ext/mini_racer_rs/src/options.rs +269 -0
- data/ext/mini_racer_rs/src/platform.rs +42 -0
- data/ext/mini_racer_rs/src/rb_raw.rs +368 -0
- data/ext/mini_racer_rs/src/snapshot.rs +149 -0
- data/ext/mini_racer_rs/v8-archives.sha256 +8 -0
- data/lib/mini_racer/shared.rb +420 -0
- data/lib/mini_racer/truffleruby.rb +439 -0
- data/lib/mini_racer/version.rb +6 -0
- data/lib/mini_racer.rb +3 -0
- data/lib/mini_racer_rs/internal_error.rb +7 -0
- data/lib/mini_racer_rs/version.rb +5 -0
- data/lib/mini_racer_rs/vocabulary.rb +105 -0
- data/lib/mini_racer_rs.rb +33 -0
- data/licenses/mini_racer.txt +21 -0
- data/licenses/rust/SOURCES.tsv +6 -0
- data/licenses/rust/std/COPYRIGHT +34 -0
- data/licenses/rust/std/COPYRIGHT-library.html +29357 -0
- data/licenses/rust/std/LICENSE-APACHE +176 -0
- data/licenses/rust/std/LICENSE-MIT +25 -0
- data/licenses/v8/SOURCES.tsv +20 -0
- data/licenses/v8/abseil-cpp/LICENSE +203 -0
- data/licenses/v8/chromium-build/LICENSE +27 -0
- data/licenses/v8/components.tsv +29 -0
- data/licenses/v8/dragonbox/LICENSE-Boost +23 -0
- data/licenses/v8/fast_float/LICENSE-MIT +27 -0
- data/licenses/v8/highway/LICENSE +371 -0
- data/licenses/v8/icu/LICENSE +568 -0
- data/licenses/v8/inspector_protocol/LICENSE +27 -0
- data/licenses/v8/libc++/LICENSE.TXT +311 -0
- data/licenses/v8/libc++abi/LICENSE.TXT +311 -0
- data/licenses/v8/llvm-libc/LICENSE.TXT +278 -0
- data/licenses/v8/rapidhash/LICENSE +34 -0
- data/licenses/v8/simdutf/LICENSE +18 -0
- data/licenses/v8/siphash/LICENSE +116 -0
- data/licenses/v8/utf8-decoder/LICENSE +19 -0
- data/licenses/v8/v8/LICENSE +61 -0
- data/licenses/v8/v8/LICENSE.fdlibm +6 -0
- data/licenses/v8/v8/LICENSE.strongtalk +29 -0
- data/licenses/v8/v8/LICENSE.v8 +26 -0
- metadata +156 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
use std::io::{self, ErrorKind, Read, Write};
|
|
2
|
+
use std::os::fd::OwnedFd;
|
|
3
|
+
use std::os::unix::net::UnixStream;
|
|
4
|
+
use std::sync::Mutex;
|
|
5
|
+
|
|
6
|
+
use mini_racer_sync::{Channel, Notifier, Side, lock};
|
|
7
|
+
|
|
8
|
+
use crate::control::Wire;
|
|
9
|
+
|
|
10
|
+
/// A context's notifier for hosts that wait on a file descriptor: it becomes
|
|
11
|
+
/// readable whenever the runner rings the Ruby side. Made on first use, again
|
|
12
|
+
/// after a failed attempt, and released by `dispose`.
|
|
13
|
+
#[derive(Default)]
|
|
14
|
+
pub struct NotifyFd(Mutex<Option<UnixStream>>);
|
|
15
|
+
|
|
16
|
+
struct Doorbell(UnixStream);
|
|
17
|
+
|
|
18
|
+
impl Notifier for Doorbell {
|
|
19
|
+
fn notify(&self) {
|
|
20
|
+
// A full buffer already holds unread rings, so a failed write loses none.
|
|
21
|
+
(&self.0).write_all(&[1]).ok();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl NotifyFd {
|
|
26
|
+
/// Discards earlier rings.
|
|
27
|
+
pub fn drain(&self, channel: &Channel<Wire>) -> io::Result<()> {
|
|
28
|
+
let mut slot = lock(&self.0);
|
|
29
|
+
let read = Self::open(&mut slot, channel)?;
|
|
30
|
+
let mut buffer = [0; 64];
|
|
31
|
+
let drained = loop {
|
|
32
|
+
match (&*read).read(&mut buffer) {
|
|
33
|
+
Ok(0) => break Ok(()),
|
|
34
|
+
Ok(_) => {}
|
|
35
|
+
Err(error) if error.kind() == ErrorKind::Interrupted => {}
|
|
36
|
+
Err(error) if error.kind() == ErrorKind::WouldBlock => break Ok(()),
|
|
37
|
+
Err(error) => break Err(error),
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
drop(slot);
|
|
41
|
+
drained
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// A descriptor of its own for the host, readable whenever this one is.
|
|
45
|
+
pub fn duplicate(&self, channel: &Channel<Wire>) -> io::Result<OwnedFd> {
|
|
46
|
+
let mut slot = lock(&self.0);
|
|
47
|
+
let copy = Self::open(&mut slot, channel)?.try_clone();
|
|
48
|
+
drop(slot);
|
|
49
|
+
Ok(copy?.into())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn release(&self, channel: &Channel<Wire>) {
|
|
53
|
+
channel.set_notifier(Side::Ruby, None);
|
|
54
|
+
lock(&self.0).take();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn open<'s>(
|
|
58
|
+
slot: &'s mut Option<UnixStream>,
|
|
59
|
+
channel: &Channel<Wire>,
|
|
60
|
+
) -> io::Result<&'s UnixStream> {
|
|
61
|
+
if let Some(read) = slot {
|
|
62
|
+
return Ok(read);
|
|
63
|
+
}
|
|
64
|
+
let (read, write) = UnixStream::pair()?;
|
|
65
|
+
read.set_nonblocking(true)?;
|
|
66
|
+
write.set_nonblocking(true)?;
|
|
67
|
+
channel.set_notifier(Side::Ruby, Some(Box::new(Doorbell(write))));
|
|
68
|
+
Ok(slot.insert(read))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[cfg(test)]
|
|
73
|
+
mod tests {
|
|
74
|
+
use super::*;
|
|
75
|
+
|
|
76
|
+
fn readable(fd: &OwnedFd) -> bool {
|
|
77
|
+
let mut stream = UnixStream::from(fd.try_clone().expect("dup"));
|
|
78
|
+
stream.set_nonblocking(true).expect("nonblocking");
|
|
79
|
+
matches!(stream.read(&mut [0]), Ok(1))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn a_ring_is_readable_on_the_duplicate_until_drained() {
|
|
84
|
+
let channel = Channel::<Wire>::default();
|
|
85
|
+
let notify = NotifyFd::default();
|
|
86
|
+
let fd = notify.duplicate(&channel).expect("pair");
|
|
87
|
+
assert!(!readable(&fd));
|
|
88
|
+
channel.wake(Side::Ruby);
|
|
89
|
+
channel.wake(Side::Ruby);
|
|
90
|
+
assert!(readable(&fd));
|
|
91
|
+
notify.drain(&channel).expect("drain");
|
|
92
|
+
assert!(!readable(&fd));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn release_closes_the_pair_and_a_later_wait_makes_another() {
|
|
97
|
+
let channel = Channel::<Wire>::default();
|
|
98
|
+
let notify = NotifyFd::default();
|
|
99
|
+
let old = notify.duplicate(&channel).expect("pair");
|
|
100
|
+
notify.release(&channel);
|
|
101
|
+
channel.wake(Side::Ruby);
|
|
102
|
+
assert!(!readable(&old));
|
|
103
|
+
let new = notify.duplicate(&channel).expect("pair");
|
|
104
|
+
channel.wake(Side::Ruby);
|
|
105
|
+
assert!(readable(&new));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
use std::collections::hash_map::RandomState;
|
|
2
|
+
use std::hash::{BuildHasher, Hash, Hasher};
|
|
3
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
4
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
|
5
|
+
|
|
6
|
+
use mini_racer_sync::lock;
|
|
7
|
+
|
|
8
|
+
use crate::control::JsText;
|
|
9
|
+
|
|
10
|
+
const MAX_ENTRIES: usize = 8;
|
|
11
|
+
const MAX_BYTES: usize = 256 << 20;
|
|
12
|
+
const MAX_SIGHTINGS: usize = 64;
|
|
13
|
+
const MAX_REFUSED: usize = 8;
|
|
14
|
+
// Units read from each end of a source, and a smaller window from each of a
|
|
15
|
+
// few points between, to tell one sighting from another without reading it all.
|
|
16
|
+
const SAMPLE: usize = 4096;
|
|
17
|
+
const SAMPLE_POINTS: usize = 8;
|
|
18
|
+
|
|
19
|
+
/// V8 code caches for the large scripts one process's contexts have run, so
|
|
20
|
+
/// the next context to evaluate one reads its compiled code back instead of
|
|
21
|
+
/// compiling it again.
|
|
22
|
+
///
|
|
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.
|
|
26
|
+
#[derive(Default)]
|
|
27
|
+
pub struct CodeCache {
|
|
28
|
+
// Most recently used last.
|
|
29
|
+
entries: Mutex<Vec<Arc<Entry>>>,
|
|
30
|
+
sightings: Mutex<Vec<u64>>,
|
|
31
|
+
// Being built, so contexts loading a script at once build it once.
|
|
32
|
+
building: Mutex<Vec<Key>>,
|
|
33
|
+
// Built but over the size limit, so not built again.
|
|
34
|
+
refused: Mutex<Vec<Key>>,
|
|
35
|
+
// V8 checks a cache against the source's length alone, and runs whatever
|
|
36
|
+
// code it holds. Digests are keyed with this process's random key, so a
|
|
37
|
+
// script cannot be written to match another's.
|
|
38
|
+
hasher: RandomState,
|
|
39
|
+
hits: AtomicU64,
|
|
40
|
+
stored: AtomicU64,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// What a cached script is found by: its file name, the startup snapshot its
|
|
44
|
+
/// context began from (V8 refuses a cache made under another), and its
|
|
45
|
+
/// source's length and keyed digest.
|
|
46
|
+
#[derive(Clone, PartialEq, Eq)]
|
|
47
|
+
pub(crate) struct Key {
|
|
48
|
+
filename: JsText,
|
|
49
|
+
snapshot: Option<u64>,
|
|
50
|
+
units: usize,
|
|
51
|
+
digest: u64,
|
|
52
|
+
sighting: u64,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub(crate) struct Entry {
|
|
56
|
+
key: Key,
|
|
57
|
+
data: Box<[u8]>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl Entry {
|
|
61
|
+
pub(crate) fn data(&self) -> &[u8] {
|
|
62
|
+
&self.data
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// A key being built; dropped unbuilt, it lets another context build it.
|
|
67
|
+
pub(crate) struct Building {
|
|
68
|
+
cache: &'static CodeCache,
|
|
69
|
+
key: Key,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl Drop for Building {
|
|
73
|
+
fn drop(&mut self) {
|
|
74
|
+
lock(&self.cache.building).retain(|key| *key != self.key);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// How the runner compiles a large script.
|
|
79
|
+
pub(crate) enum Plan {
|
|
80
|
+
/// From source: a first sighting, a key being built elsewhere, a refused
|
|
81
|
+
/// cache, or the cache is off.
|
|
82
|
+
Compile,
|
|
83
|
+
/// From source, then a cache made once the dispatch is over.
|
|
84
|
+
Make(Building),
|
|
85
|
+
/// From the entry, falling back to source if V8 refuses it.
|
|
86
|
+
Consume(Arc<Entry>, Key),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
impl CodeCache {
|
|
90
|
+
pub(crate) fn plan(
|
|
91
|
+
&'static self,
|
|
92
|
+
filename: &JsText,
|
|
93
|
+
source: &JsText,
|
|
94
|
+
snapshot: Option<u64>,
|
|
95
|
+
) -> Plan {
|
|
96
|
+
if !enabled() {
|
|
97
|
+
return Plan::Compile;
|
|
98
|
+
}
|
|
99
|
+
let sighting = self.sighting(filename, source, snapshot);
|
|
100
|
+
let seen = touch(&self.sightings, sighting, MAX_SIGHTINGS);
|
|
101
|
+
// An entry counts as seen however many sightings have passed since.
|
|
102
|
+
let cached = lock(&self.entries)
|
|
103
|
+
.iter()
|
|
104
|
+
.any(|entry| entry.key.sighting == sighting);
|
|
105
|
+
if !seen && !cached {
|
|
106
|
+
return Plan::Compile;
|
|
107
|
+
}
|
|
108
|
+
let key = Key {
|
|
109
|
+
filename: filename.clone(),
|
|
110
|
+
snapshot,
|
|
111
|
+
units: source.units(),
|
|
112
|
+
digest: self.hasher.hash_one(source),
|
|
113
|
+
sighting,
|
|
114
|
+
};
|
|
115
|
+
let mut entries = lock(&self.entries);
|
|
116
|
+
if let Some(at) = entries.iter().position(|entry| entry.key == key) {
|
|
117
|
+
let entry = entries.remove(at);
|
|
118
|
+
entries.push(Arc::clone(&entry));
|
|
119
|
+
drop(entries);
|
|
120
|
+
return Plan::Consume(entry, key);
|
|
121
|
+
}
|
|
122
|
+
drop(entries);
|
|
123
|
+
if lock(&self.refused).contains(&key) {
|
|
124
|
+
return Plan::Compile;
|
|
125
|
+
}
|
|
126
|
+
self.begin(key).map_or(Plan::Compile, Plan::Make)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Claims `key` for this context to build, unless another is building it.
|
|
130
|
+
pub(crate) fn begin(&'static self, key: Key) -> Option<Building> {
|
|
131
|
+
let mut building = lock(&self.building);
|
|
132
|
+
if building.contains(&key) {
|
|
133
|
+
return None;
|
|
134
|
+
}
|
|
135
|
+
building.push(key.clone());
|
|
136
|
+
drop(building);
|
|
137
|
+
Some(Building { cache: self, key })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// A startup snapshot's identity for `Key`, from a sample of the blob:
|
|
141
|
+
/// one snapshot taken for another costs a refused cache, never wrong code.
|
|
142
|
+
pub(crate) fn snapshot_id(&self, blob: &[u8]) -> u64 {
|
|
143
|
+
let mut hasher = self.hasher.build_hasher();
|
|
144
|
+
sample(blob, &mut hasher);
|
|
145
|
+
hasher.finish()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub(crate) fn hit(&self) {
|
|
149
|
+
self.hits.fetch_add(1, Ordering::Relaxed);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Keeps `data` for the key built, dropping the least recently used
|
|
153
|
+
/// entries past the limits; a cache larger than all of them is refused.
|
|
154
|
+
pub(crate) fn store(&self, building: Building, data: Box<[u8]>) {
|
|
155
|
+
let key = building.key.clone();
|
|
156
|
+
drop(building);
|
|
157
|
+
if data.len() > MAX_BYTES {
|
|
158
|
+
touch(&self.refused, key, MAX_REFUSED);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let mut entries = lock(&self.entries);
|
|
162
|
+
if entries.iter().any(|entry| entry.key == key) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
entries.push(Arc::new(Entry { key, data }));
|
|
166
|
+
while entries.len() > MAX_ENTRIES
|
|
167
|
+
|| entries.iter().map(|entry| entry.data.len()).sum::<usize>() > MAX_BYTES
|
|
168
|
+
{
|
|
169
|
+
entries.remove(0);
|
|
170
|
+
}
|
|
171
|
+
drop(entries);
|
|
172
|
+
self.stored.fetch_add(1, Ordering::Relaxed);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Drops an entry V8 refused, so no later context tries it again.
|
|
176
|
+
pub(crate) fn forget(&self, entry: &Arc<Entry>) {
|
|
177
|
+
lock(&self.entries).retain(|kept| !Arc::ptr_eq(kept, entry));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// A forked child's cache: what its parent kept, had seen and refused,
|
|
181
|
+
/// unless a thread of the parent held it at the fork, under the same key.
|
|
182
|
+
pub(crate) fn inherited(&self) -> Self {
|
|
183
|
+
Self {
|
|
184
|
+
entries: Mutex::new(copied(&self.entries)),
|
|
185
|
+
sightings: Mutex::new(copied(&self.sightings)),
|
|
186
|
+
building: Mutex::default(),
|
|
187
|
+
refused: Mutex::new(copied(&self.refused)),
|
|
188
|
+
hasher: self.hasher.clone(),
|
|
189
|
+
hits: AtomicU64::new(0),
|
|
190
|
+
stored: AtomicU64::new(0),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Evaluations this process compiled from a cache V8 accepted.
|
|
195
|
+
#[must_use]
|
|
196
|
+
pub fn hits(&self) -> u64 {
|
|
197
|
+
self.hits.load(Ordering::Relaxed)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Caches this process made and kept.
|
|
201
|
+
#[must_use]
|
|
202
|
+
pub fn stored(&self) -> u64 {
|
|
203
|
+
self.stored.load(Ordering::Relaxed)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fn sighting(&self, filename: &JsText, source: &JsText, snapshot: Option<u64>) -> u64 {
|
|
207
|
+
let mut hasher = self.hasher.build_hasher();
|
|
208
|
+
filename.hash(&mut hasher);
|
|
209
|
+
snapshot.hash(&mut hasher);
|
|
210
|
+
let encoding: u8 = match source {
|
|
211
|
+
JsText::Utf8(bytes) => {
|
|
212
|
+
sample(bytes, &mut hasher);
|
|
213
|
+
0
|
|
214
|
+
}
|
|
215
|
+
JsText::Latin1(bytes) => {
|
|
216
|
+
sample(bytes, &mut hasher);
|
|
217
|
+
1
|
|
218
|
+
}
|
|
219
|
+
JsText::Utf16(units) => {
|
|
220
|
+
sample(units, &mut hasher);
|
|
221
|
+
2
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
encoding.hash(&mut hasher);
|
|
225
|
+
hasher.finish()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Moves `item` to the back of `list`, adding it if absent and dropping the
|
|
230
|
+
// front past `max`. Returns whether it was there.
|
|
231
|
+
fn touch<T: PartialEq>(list: &Mutex<Vec<T>>, item: T, max: usize) -> bool {
|
|
232
|
+
let mut list = lock(list);
|
|
233
|
+
let found = list.iter().position(|kept| *kept == item);
|
|
234
|
+
if let Some(at) = found {
|
|
235
|
+
list.remove(at);
|
|
236
|
+
} else if list.len() == max {
|
|
237
|
+
list.remove(0);
|
|
238
|
+
}
|
|
239
|
+
list.push(item);
|
|
240
|
+
drop(list);
|
|
241
|
+
found.is_some()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn copied<T: Clone>(list: &Mutex<Vec<T>>) -> Vec<T> {
|
|
245
|
+
list.try_lock().map(|list| list.clone()).unwrap_or_default()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn sample<T: Hash>(text: &[T], hasher: &mut impl Hasher) {
|
|
249
|
+
let len = text.len();
|
|
250
|
+
len.hash(hasher);
|
|
251
|
+
let span = SAMPLE.min(len);
|
|
252
|
+
text.get(..span).hash(hasher);
|
|
253
|
+
text.get(len - span..).hash(hasher);
|
|
254
|
+
for point in 1..SAMPLE_POINTS {
|
|
255
|
+
let at = len / SAMPLE_POINTS * point;
|
|
256
|
+
text.get(at..(at + SAMPLE / SAMPLE_POINTS).min(len))
|
|
257
|
+
.hash(hasher);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Whether the cache is on: `MINI_RACER_RS_CODE_CACHE=off` turns it off.
|
|
262
|
+
/// First read on a Ruby thread, by `ContextHandle::spawn`, where nothing in
|
|
263
|
+
/// Ruby can be changing the environment at the same time.
|
|
264
|
+
pub(crate) fn enabled() -> bool {
|
|
265
|
+
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
266
|
+
*ENABLED.get_or_init(|| {
|
|
267
|
+
std::env::var_os("MINI_RACER_RS_CODE_CACHE").is_none_or(|value| value != "off")
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[cfg(test)]
|
|
272
|
+
#[allow(clippy::panic, reason = "a test fails by panicking")]
|
|
273
|
+
mod tests {
|
|
274
|
+
use super::{CodeCache, Plan};
|
|
275
|
+
use crate::control::JsText;
|
|
276
|
+
|
|
277
|
+
fn cache() -> &'static CodeCache {
|
|
278
|
+
Box::leak(Box::default())
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn source(seed: u8) -> JsText {
|
|
282
|
+
JsText::Latin1(vec![seed; 2 << 20])
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn name() -> JsText {
|
|
286
|
+
JsText::from("bundle.js")
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Sees `text` twice and stores a cache for it.
|
|
290
|
+
fn cached(cache: &'static CodeCache, text: &JsText, data: Box<[u8]>) {
|
|
291
|
+
let _ = cache.plan(&name(), text, None);
|
|
292
|
+
let Plan::Make(building) = cache.plan(&name(), text, None) else {
|
|
293
|
+
panic!("a second sighting makes a cache");
|
|
294
|
+
};
|
|
295
|
+
cache.store(building, data);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn caches_a_script_the_second_time_it_is_seen() {
|
|
300
|
+
let cache = cache();
|
|
301
|
+
assert!(matches!(
|
|
302
|
+
cache.plan(&name(), &source(1), None),
|
|
303
|
+
Plan::Compile
|
|
304
|
+
));
|
|
305
|
+
cached(cache, &source(1), Box::from(&b"compiled"[..]));
|
|
306
|
+
assert!(matches!(
|
|
307
|
+
cache.plan(&name(), &source(1), None),
|
|
308
|
+
Plan::Consume(..)
|
|
309
|
+
));
|
|
310
|
+
assert!(matches!(
|
|
311
|
+
cache.plan(&name(), &source(1), Some(7)),
|
|
312
|
+
Plan::Compile
|
|
313
|
+
));
|
|
314
|
+
assert!(matches!(
|
|
315
|
+
cache.plan(&JsText::from("other.js"), &source(1), None),
|
|
316
|
+
Plan::Compile
|
|
317
|
+
));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Sources that match on every sampled unit and differ elsewhere share a
|
|
321
|
+
// sighting, and the full digest still tells them apart.
|
|
322
|
+
#[test]
|
|
323
|
+
fn a_source_alike_in_its_samples_is_not_taken_for_another() {
|
|
324
|
+
let cache = cache();
|
|
325
|
+
let mut alike = vec![5_u8; 2 << 20];
|
|
326
|
+
if let Some(unit) = alike.get_mut((2 << 20) / 16 + 4096) {
|
|
327
|
+
*unit = 6;
|
|
328
|
+
}
|
|
329
|
+
let (first, second) = (JsText::Latin1(vec![5; 2 << 20]), JsText::Latin1(alike));
|
|
330
|
+
assert_eq!(
|
|
331
|
+
cache.sighting(&name(), &first, None),
|
|
332
|
+
cache.sighting(&name(), &second, None)
|
|
333
|
+
);
|
|
334
|
+
cached(cache, &first, Box::from(&b"compiled"[..]));
|
|
335
|
+
assert!(matches!(cache.plan(&name(), &second, None), Plan::Make(..)));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#[test]
|
|
339
|
+
fn an_entry_outlives_any_number_of_other_sightings() {
|
|
340
|
+
let cache = cache();
|
|
341
|
+
cached(cache, &source(2), Box::from(&b"compiled"[..]));
|
|
342
|
+
// More sightings than the list keeps.
|
|
343
|
+
for seed in 10_u8..80 {
|
|
344
|
+
let _ = cache.plan(&name(), &source(seed), None);
|
|
345
|
+
}
|
|
346
|
+
assert!(matches!(
|
|
347
|
+
cache.plan(&name(), &source(2), None),
|
|
348
|
+
Plan::Consume(..)
|
|
349
|
+
));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn a_script_being_built_is_built_once() {
|
|
354
|
+
let cache = cache();
|
|
355
|
+
let _ = cache.plan(&name(), &source(3), None);
|
|
356
|
+
let first = cache.plan(&name(), &source(3), None);
|
|
357
|
+
assert!(matches!(first, Plan::Make(..)));
|
|
358
|
+
assert!(matches!(
|
|
359
|
+
cache.plan(&name(), &source(3), None),
|
|
360
|
+
Plan::Compile
|
|
361
|
+
));
|
|
362
|
+
drop(first);
|
|
363
|
+
assert!(matches!(
|
|
364
|
+
cache.plan(&name(), &source(3), None),
|
|
365
|
+
Plan::Make(..)
|
|
366
|
+
));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn a_child_starts_from_its_parents_entries() {
|
|
371
|
+
let cache = cache();
|
|
372
|
+
cached(cache, &source(4), Box::from(&b"compiled"[..]));
|
|
373
|
+
let child: &'static CodeCache = Box::leak(Box::new(cache.inherited()));
|
|
374
|
+
assert!(matches!(
|
|
375
|
+
child.plan(&name(), &source(4), None),
|
|
376
|
+
Plan::Consume(..)
|
|
377
|
+
));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[test]
|
|
381
|
+
fn a_child_forked_while_the_cache_was_held_starts_without_its_entries() {
|
|
382
|
+
let cache = cache();
|
|
383
|
+
cached(cache, &source(5), Box::from(&b"compiled"[..]));
|
|
384
|
+
let held = cache.entries.lock();
|
|
385
|
+
let child: &'static CodeCache = Box::leak(Box::new(cache.inherited()));
|
|
386
|
+
drop(held);
|
|
387
|
+
assert!(matches!(
|
|
388
|
+
child.plan(&name(), &source(5), None),
|
|
389
|
+
Plan::Make(..)
|
|
390
|
+
));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#[test]
|
|
394
|
+
fn a_cache_larger_than_the_limit_is_refused_and_not_rebuilt() {
|
|
395
|
+
let cache = cache();
|
|
396
|
+
cached(cache, &source(6), Box::from(&b"compiled"[..]));
|
|
397
|
+
cached(
|
|
398
|
+
cache,
|
|
399
|
+
&source(7),
|
|
400
|
+
vec![0; super::MAX_BYTES + 1].into_boxed_slice(),
|
|
401
|
+
);
|
|
402
|
+
assert_eq!(cache.stored(), 1);
|
|
403
|
+
assert!(matches!(
|
|
404
|
+
cache.plan(&name(), &source(7), None),
|
|
405
|
+
Plan::Compile
|
|
406
|
+
));
|
|
407
|
+
assert!(matches!(
|
|
408
|
+
cache.plan(&name(), &source(6), None),
|
|
409
|
+
Plan::Consume(..)
|
|
410
|
+
));
|
|
411
|
+
}
|
|
412
|
+
}
|