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,1560 @@
|
|
|
1
|
+
use std::collections::hash_map::RandomState;
|
|
2
|
+
use std::hash::BuildHasher;
|
|
3
|
+
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
4
|
+
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
|
5
|
+
use std::sync::{Arc, Mutex};
|
|
6
|
+
use std::time::Duration;
|
|
7
|
+
|
|
8
|
+
use mini_racer_sync::{
|
|
9
|
+
DispatchId, DispatchKind, Halt, Interrupt, Phase, Side, TerminationCause, ToV8, lock,
|
|
10
|
+
};
|
|
11
|
+
use mini_racer_wire::Writer;
|
|
12
|
+
use v8::ValueDeserializerHelper;
|
|
13
|
+
|
|
14
|
+
use crate::control::{CallbackReply, ControlBlock, Depth, IsolateTerminator, JsText, Request};
|
|
15
|
+
use crate::fast_calls::{self, Hoisted, Shape};
|
|
16
|
+
use crate::process::code_cache::{Building, Plan};
|
|
17
|
+
use crate::process::fork::Generation;
|
|
18
|
+
use crate::process::platform::{self, Mode};
|
|
19
|
+
use crate::process::watchdog;
|
|
20
|
+
use crate::serialize::{self, Filter, Tc};
|
|
21
|
+
use crate::spin::spin_until;
|
|
22
|
+
use crate::{EngineError, JsError, JsErrorKind};
|
|
23
|
+
use crate::{snapshot, sys};
|
|
24
|
+
|
|
25
|
+
// Spinning after each response catches a busy caller's next request without a
|
|
26
|
+
// futex wake on either side; an idle context then parks.
|
|
27
|
+
const IDLE_SPIN: Duration = Duration::from_micros(100);
|
|
28
|
+
|
|
29
|
+
// Closes the channel however the runner ends, so a waiting Ruby thread never
|
|
30
|
+
// parks on a runner that is gone.
|
|
31
|
+
struct Closing<'a>(&'a ControlBlock);
|
|
32
|
+
|
|
33
|
+
impl Drop for Closing<'_> {
|
|
34
|
+
fn drop(&mut self) {
|
|
35
|
+
self.0.term.uninstall();
|
|
36
|
+
self.0.channel.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Kept in an isolate slot: the callback trampoline gets only the isolate. A
|
|
41
|
+
// shared isolate's slots must be Send, as any thread may lock it.
|
|
42
|
+
struct Runtime {
|
|
43
|
+
block: Arc<ControlBlock>,
|
|
44
|
+
generation: &'static Generation,
|
|
45
|
+
// Whether the runner holds its isolate only while serving a request.
|
|
46
|
+
released: bool,
|
|
47
|
+
context: v8::Global<v8::Context>,
|
|
48
|
+
filter: Filter,
|
|
49
|
+
// The exception last thrown for a raising callback; mini_racer keeps one
|
|
50
|
+
// per isolate and compares what escapes a dispatch against it.
|
|
51
|
+
raised: Mutex<Option<v8::Global<v8::Value>>>,
|
|
52
|
+
// The address V8 posts this isolate's tasks under.
|
|
53
|
+
key: usize,
|
|
54
|
+
// Tasks running, one inside another when a task's script pumps.
|
|
55
|
+
running: AtomicU32,
|
|
56
|
+
// Set by a request, at any depth, that left memory worth handing back
|
|
57
|
+
// once the outermost response is out.
|
|
58
|
+
freed: AtomicBool,
|
|
59
|
+
// The startup snapshot this isolate began from, for the code cache's key.
|
|
60
|
+
snapshot: Option<u64>,
|
|
61
|
+
// Large scripts to cache once their dispatch is over, so making a cache
|
|
62
|
+
// never counts against a timeout.
|
|
63
|
+
uncached: Mutex<Vec<Uncached>>,
|
|
64
|
+
hoisting: Mutex<Hoisting>,
|
|
65
|
+
// The values of each fast call running, innermost last, for `hoisted`.
|
|
66
|
+
frames: Mutex<Vec<v8::Global<v8::Array>>>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
struct Uncached {
|
|
70
|
+
script: v8::Global<v8::UnboundScript>,
|
|
71
|
+
building: Building,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A context's fast calls: the binding their templates read values through,
|
|
75
|
+
// the templates compiled so far, most recently used last, and the last source
|
|
76
|
+
// a fast call took, up to `LARGE_SOURCE`.
|
|
77
|
+
#[derive(Default)]
|
|
78
|
+
struct Hoisting {
|
|
79
|
+
binding: Binding,
|
|
80
|
+
templates: Vec<Template>,
|
|
81
|
+
last: Vec<u8>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
struct Template {
|
|
85
|
+
text: Box<[u8]>,
|
|
86
|
+
// `None` for one refused: its code does not leave the call alone, or it
|
|
87
|
+
// does not compile.
|
|
88
|
+
script: Option<v8::Global<v8::Script>>,
|
|
89
|
+
// How much of its text comes before its first value and after its last.
|
|
90
|
+
ends: (usize, usize),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl Template {
|
|
94
|
+
// A source that starts and ends as this refused template does would make
|
|
95
|
+
// it again, so it takes the normal path without being read.
|
|
96
|
+
fn refuses(&self, source: &[u8]) -> bool {
|
|
97
|
+
let (head, tail) = self.ends;
|
|
98
|
+
self.script.is_none()
|
|
99
|
+
&& source.len() >= head + tail
|
|
100
|
+
&& self
|
|
101
|
+
.text
|
|
102
|
+
.get(..head)
|
|
103
|
+
.is_some_and(|head| source.starts_with(head))
|
|
104
|
+
&& self
|
|
105
|
+
.text
|
|
106
|
+
.len()
|
|
107
|
+
.checked_sub(tail)
|
|
108
|
+
.and_then(|at| self.text.get(at..))
|
|
109
|
+
.is_some_and(|tail| source.ends_with(tail))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[derive(Default)]
|
|
114
|
+
enum Binding {
|
|
115
|
+
#[default]
|
|
116
|
+
Unmade,
|
|
117
|
+
Made(String),
|
|
118
|
+
Failed,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const TEMPLATES: usize = 32;
|
|
122
|
+
|
|
123
|
+
impl Runtime {
|
|
124
|
+
fn new(
|
|
125
|
+
block: &Arc<ControlBlock>,
|
|
126
|
+
generation: &'static Generation,
|
|
127
|
+
(context, filter): (v8::Global<v8::Context>, Filter),
|
|
128
|
+
(key, snapshot): (usize, Option<u64>),
|
|
129
|
+
) -> Self {
|
|
130
|
+
Self {
|
|
131
|
+
block: Arc::clone(block),
|
|
132
|
+
generation,
|
|
133
|
+
released: platform::mode() == Mode::SingleThreaded,
|
|
134
|
+
context,
|
|
135
|
+
filter,
|
|
136
|
+
raised: Mutex::default(),
|
|
137
|
+
key,
|
|
138
|
+
running: AtomicU32::new(0),
|
|
139
|
+
freed: AtomicBool::new(false),
|
|
140
|
+
snapshot,
|
|
141
|
+
uncached: Mutex::default(),
|
|
142
|
+
hoisting: Mutex::default(),
|
|
143
|
+
frames: Mutex::default(),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
struct Running<'a>(&'a AtomicU32);
|
|
149
|
+
|
|
150
|
+
impl Drop for Running<'_> {
|
|
151
|
+
fn drop(&mut self) {
|
|
152
|
+
self.0.fetch_sub(1, Ordering::Relaxed);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// How a runner gets its isolate: made fresh, from the control block's
|
|
157
|
+
/// snapshot if it has one, or taken over in a forked child from an idle runner
|
|
158
|
+
/// of the parent.
|
|
159
|
+
#[derive(Clone, Copy)]
|
|
160
|
+
pub enum Start {
|
|
161
|
+
Fresh,
|
|
162
|
+
/// The isolate waits in the new control block's custody.
|
|
163
|
+
Adopted,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
pub fn run(block: &Arc<ControlBlock>, generation: &'static Generation, start: Start) {
|
|
167
|
+
let closing = Closing(block);
|
|
168
|
+
let disposed = catch_unwind(AssertUnwindSafe(|| match start {
|
|
169
|
+
Start::Fresh => serve(block, generation),
|
|
170
|
+
Start::Adopted => {
|
|
171
|
+
let isolate = lock(&block.custody).take();
|
|
172
|
+
isolate.map_or_else(
|
|
173
|
+
|| refuse(block, "no isolate to take over"),
|
|
174
|
+
|isolate| adopt(block, generation, isolate),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}))
|
|
178
|
+
.unwrap_or_else(|_| {
|
|
179
|
+
if block.channel.ready(Side::V8) {
|
|
180
|
+
let _ = block
|
|
181
|
+
.channel
|
|
182
|
+
.respond(Err(EngineError::Internal("the runner panicked".to_owned())));
|
|
183
|
+
}
|
|
184
|
+
false
|
|
185
|
+
});
|
|
186
|
+
// Answering only after the isolate is gone makes an explicit dispose return
|
|
187
|
+
// once V8 has released everything.
|
|
188
|
+
if disposed {
|
|
189
|
+
let _ = block.channel.respond(Ok(Vec::new()));
|
|
190
|
+
}
|
|
191
|
+
drop(closing);
|
|
192
|
+
block.phase.set(Phase::Exited);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// mini_racer's max_memory, checked after every garbage collection: a heap
|
|
196
|
+
// past it terminates the running script, or the next one.
|
|
197
|
+
pub fn check_heap(isolate: &mut v8::Isolate) {
|
|
198
|
+
let Some(runtime) = isolate.get_slot::<Arc<Runtime>>().cloned() else {
|
|
199
|
+
return;
|
|
200
|
+
};
|
|
201
|
+
let Some(max) = runtime.block.config.max_memory else {
|
|
202
|
+
return;
|
|
203
|
+
};
|
|
204
|
+
let used = isolate.get_heap_statistics().used_heap_size();
|
|
205
|
+
if u64::try_from(used).unwrap_or(u64::MAX) > max {
|
|
206
|
+
runtime.block.term.request(Interrupt::MaxMemory);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
pub fn park_for_fork(isolate: &v8::Isolate) {
|
|
211
|
+
if let Some(runtime) = isolate.get_slot::<Arc<Runtime>>() {
|
|
212
|
+
runtime.generation.pause(&runtime.block);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fn refuse(block: &ControlBlock, message: &str) -> bool {
|
|
217
|
+
// Holding no isolate, it never keeps a fork waiting.
|
|
218
|
+
block.phase.set(Phase::Deposited);
|
|
219
|
+
if receive(block).is_some() {
|
|
220
|
+
let _ = block
|
|
221
|
+
.channel
|
|
222
|
+
.respond(Err(EngineError::Internal(message.to_owned())));
|
|
223
|
+
}
|
|
224
|
+
false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Returns whether it stopped for an explicit dispose.
|
|
228
|
+
fn serve(block: &Arc<ControlBlock>, generation: &'static Generation) -> bool {
|
|
229
|
+
// No isolate is made while a fork waits for every runner to be idle.
|
|
230
|
+
generation.resume(block);
|
|
231
|
+
let snapshot = block.config.snapshot.as_ref();
|
|
232
|
+
let from_snapshot = snapshot.is_some();
|
|
233
|
+
let snapshot_id = snapshot.map(|blob| generation.code_cache().snapshot_id(blob.as_bytes()));
|
|
234
|
+
let params = v8::CreateParams::default();
|
|
235
|
+
let params = match snapshot {
|
|
236
|
+
Some(blob) => params.snapshot_blob(sys::startup_data(blob)),
|
|
237
|
+
None => params,
|
|
238
|
+
};
|
|
239
|
+
let mut isolate = v8::Isolate::new(params);
|
|
240
|
+
if block.config.max_memory.is_some() {
|
|
241
|
+
sys::watch_heap(&mut isolate);
|
|
242
|
+
}
|
|
243
|
+
let key = sys::isolate_key(&isolate);
|
|
244
|
+
generation.tasks().register(key, &block.tasks);
|
|
245
|
+
let contexts = {
|
|
246
|
+
v8::scope!(let scope, &mut isolate);
|
|
247
|
+
serialize::contexts(scope, from_snapshot)
|
|
248
|
+
};
|
|
249
|
+
let Some(contexts) = contexts else {
|
|
250
|
+
retire(block, generation, key, isolate);
|
|
251
|
+
return refuse(block, "context setup failed");
|
|
252
|
+
};
|
|
253
|
+
let runtime = Arc::new(Runtime::new(
|
|
254
|
+
block,
|
|
255
|
+
generation,
|
|
256
|
+
contexts,
|
|
257
|
+
(key, snapshot_id),
|
|
258
|
+
));
|
|
259
|
+
isolate.set_slot(Arc::clone(&runtime));
|
|
260
|
+
// Installed only once the context exists, so nothing terminates its setup.
|
|
261
|
+
block.term.install(IsolateTerminator::new(
|
|
262
|
+
isolate.thread_safe_handle(),
|
|
263
|
+
&block.tasks,
|
|
264
|
+
));
|
|
265
|
+
let shared = match sys::share(isolate) {
|
|
266
|
+
Ok(shared) => shared,
|
|
267
|
+
Err(refused) => {
|
|
268
|
+
retire(block, generation, key, refused);
|
|
269
|
+
return refuse(block, "isolate could not be shared");
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
let _ = block.isolate.set(shared.thread_safe_handle());
|
|
273
|
+
serve_isolate(block, shared, runtime)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The parent's runtime still names the parent's control block, so the child
|
|
277
|
+
// builds its own around the same contexts.
|
|
278
|
+
fn adopt(
|
|
279
|
+
block: &Arc<ControlBlock>,
|
|
280
|
+
generation: &'static Generation,
|
|
281
|
+
shared: v8::SharedIsolate,
|
|
282
|
+
) -> bool {
|
|
283
|
+
generation.resume(block);
|
|
284
|
+
let inherited = shared.lock().get_slot::<Arc<Runtime>>().cloned();
|
|
285
|
+
let Some(inherited) = inherited else {
|
|
286
|
+
// Disposed here, before the runner counts as idle to a fork.
|
|
287
|
+
drop(block.tasks.close());
|
|
288
|
+
drop(shared);
|
|
289
|
+
return refuse(block, "inherited isolate has no context");
|
|
290
|
+
};
|
|
291
|
+
let runtime = {
|
|
292
|
+
let mut locker = shared.lock();
|
|
293
|
+
let key = sys::isolate_key(&locker);
|
|
294
|
+
generation.tasks().register(key, &block.tasks);
|
|
295
|
+
let runtime = Arc::new(Runtime::new(
|
|
296
|
+
block,
|
|
297
|
+
generation,
|
|
298
|
+
(inherited.context.clone(), inherited.filter.clone()),
|
|
299
|
+
(key, inherited.snapshot),
|
|
300
|
+
));
|
|
301
|
+
locker.set_slot(Arc::clone(&runtime));
|
|
302
|
+
runtime
|
|
303
|
+
};
|
|
304
|
+
block.term.install(IsolateTerminator::new(
|
|
305
|
+
shared.thread_safe_handle(),
|
|
306
|
+
&block.tasks,
|
|
307
|
+
));
|
|
308
|
+
let _ = block.isolate.set(shared.thread_safe_handle());
|
|
309
|
+
serve_isolate(block, shared, runtime)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fn serve_isolate(block: &ControlBlock, shared: v8::SharedIsolate, runtime: Arc<Runtime>) -> bool {
|
|
313
|
+
let (generation, key) = (runtime.generation, runtime.key);
|
|
314
|
+
let (disposed, shared) = if runtime.released {
|
|
315
|
+
serve_released(block, shared, &runtime)
|
|
316
|
+
} else {
|
|
317
|
+
(serve_held(block, &shared, &runtime), Some(shared))
|
|
318
|
+
};
|
|
319
|
+
block.term.uninstall();
|
|
320
|
+
// Its handles are released under the lock, before the isolate goes.
|
|
321
|
+
match shared {
|
|
322
|
+
Some(shared) => {
|
|
323
|
+
let mut isolate = shared.lock();
|
|
324
|
+
drop(isolate.remove_slot::<Arc<Runtime>>());
|
|
325
|
+
drop(runtime);
|
|
326
|
+
drop(isolate);
|
|
327
|
+
retire(block, generation, key, shared);
|
|
328
|
+
}
|
|
329
|
+
None => retire(block, generation, key, ()),
|
|
330
|
+
}
|
|
331
|
+
disposed
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Queued tasks are dropped while their isolate exists. Its entry stays until
|
|
335
|
+
// the isolate is gone, so a task V8 posts during the teardown meets the closed
|
|
336
|
+
// queue and is dropped too, never kept for a later isolate at that address.
|
|
337
|
+
fn retire<I>(block: &ControlBlock, generation: &Generation, key: usize, isolate: I) {
|
|
338
|
+
drop(block.tasks.close());
|
|
339
|
+
drop(isolate);
|
|
340
|
+
generation.tasks().unregister(key, &block.tasks);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
enum Served {
|
|
344
|
+
Next,
|
|
345
|
+
Stop { disposed: bool },
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// With `gc_after_idle`, mini_racer's runner asks V8 for a full collection
|
|
349
|
+
// once it has waited that long after a request, and not again until the next.
|
|
350
|
+
fn serve_held(block: &ControlBlock, shared: &v8::SharedIsolate, runtime: &Arc<Runtime>) -> bool {
|
|
351
|
+
let mut isolate = shared.lock();
|
|
352
|
+
let mut collected = true;
|
|
353
|
+
loop {
|
|
354
|
+
// Between requests it holds its isolate but runs nothing in it, so a
|
|
355
|
+
// fork need not wait for it.
|
|
356
|
+
block.phase.set(Phase::Parked);
|
|
357
|
+
let received = match block.config.gc_after_idle {
|
|
358
|
+
Some(idle) if !collected => receive_within(block, idle),
|
|
359
|
+
_ => Received::Message(receive(block)),
|
|
360
|
+
};
|
|
361
|
+
runtime.generation.resume(block);
|
|
362
|
+
let message = match received {
|
|
363
|
+
Received::Message(message) => message,
|
|
364
|
+
// A collection for a context about to be torn down is wasted.
|
|
365
|
+
Received::Idle => {
|
|
366
|
+
if !block.term.is_disposing() {
|
|
367
|
+
isolate.low_memory_notification();
|
|
368
|
+
sys::release_free_memory();
|
|
369
|
+
}
|
|
370
|
+
collected = true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
// mini_racer's context has sent nothing before its first request.
|
|
375
|
+
if !matches!(message, Some(ToV8::Request(Request::Init { .. }))) {
|
|
376
|
+
collected = false;
|
|
377
|
+
}
|
|
378
|
+
if let Served::Stop { disposed } = serve_one(&mut isolate, block, runtime, message) {
|
|
379
|
+
return disposed;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Between requests a single-threaded runner leaves its isolate unlocked in the
|
|
385
|
+
// control block, as mini_racer's holds its isolate only for a request.
|
|
386
|
+
fn serve_released(
|
|
387
|
+
block: &ControlBlock,
|
|
388
|
+
shared: v8::SharedIsolate,
|
|
389
|
+
runtime: &Arc<Runtime>,
|
|
390
|
+
) -> (bool, Option<v8::SharedIsolate>) {
|
|
391
|
+
let mut held = Some(shared);
|
|
392
|
+
loop {
|
|
393
|
+
*lock(&block.custody) = held.take();
|
|
394
|
+
block.phase.set(Phase::Deposited);
|
|
395
|
+
let message = receive(block);
|
|
396
|
+
runtime.generation.resume(block);
|
|
397
|
+
let Some(shared) = lock(&block.custody).take() else {
|
|
398
|
+
return (false, None);
|
|
399
|
+
};
|
|
400
|
+
let mut locked = shared.lock();
|
|
401
|
+
block.term.relocked();
|
|
402
|
+
let served = serve_one(&mut locked, block, runtime, message);
|
|
403
|
+
drop(locked);
|
|
404
|
+
held = Some(shared);
|
|
405
|
+
if let Served::Stop { disposed } = served {
|
|
406
|
+
return (disposed, held);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
fn serve_one(
|
|
412
|
+
isolate: &mut v8::Isolate,
|
|
413
|
+
block: &ControlBlock,
|
|
414
|
+
runtime: &Arc<Runtime>,
|
|
415
|
+
message: Option<ToV8<crate::control::Wire>>,
|
|
416
|
+
) -> Served {
|
|
417
|
+
let request = match message {
|
|
418
|
+
Some(ToV8::Request(Request::Dispose)) => return Served::Stop { disposed: true },
|
|
419
|
+
// The snapshot's own isolate is entered with none of this
|
|
420
|
+
// runner's scopes open.
|
|
421
|
+
// A build the caller abandoned before it began is not started; once
|
|
422
|
+
// begun it runs to the end, as nothing can terminate its isolate.
|
|
423
|
+
Some(ToV8::Request(Request::Snapshot { code, base, depth })) => {
|
|
424
|
+
let response = match depth {
|
|
425
|
+
Depth::Outermost(dispatch) => {
|
|
426
|
+
let built = if block.term.check(DispatchKind::Script).is_some() {
|
|
427
|
+
Err(EngineError::Internal("halted without a cause".to_owned()))
|
|
428
|
+
} else {
|
|
429
|
+
let built = snapshot::create(&code, base.as_ref(), |isolate| {
|
|
430
|
+
isolate.set_slot(Arc::clone(runtime));
|
|
431
|
+
*lock(&block.building) = Some(isolate.thread_safe_handle());
|
|
432
|
+
});
|
|
433
|
+
lock(&block.building).take();
|
|
434
|
+
built
|
|
435
|
+
};
|
|
436
|
+
block
|
|
437
|
+
.term
|
|
438
|
+
.end(dispatch)
|
|
439
|
+
.map_or(built, |halt| Err(halted(halt)))
|
|
440
|
+
}
|
|
441
|
+
Depth::Nested => Err(EngineError::Internal(
|
|
442
|
+
"snapshot inside a callback".to_owned(),
|
|
443
|
+
)),
|
|
444
|
+
};
|
|
445
|
+
let served = respond(block, response);
|
|
446
|
+
if matches!(served, Served::Next) {
|
|
447
|
+
sys::release_free_memory();
|
|
448
|
+
}
|
|
449
|
+
return served;
|
|
450
|
+
}
|
|
451
|
+
Some(ToV8::Request(request)) => request,
|
|
452
|
+
// No callback is outstanding here, so there is nothing to reply to.
|
|
453
|
+
Some(ToV8::Reply(_)) => return Served::Next,
|
|
454
|
+
None => return Served::Stop { disposed: false },
|
|
455
|
+
};
|
|
456
|
+
let response = {
|
|
457
|
+
v8::scope_with_context!(let scope, isolate, &runtime.context);
|
|
458
|
+
handle(scope, runtime, request)
|
|
459
|
+
};
|
|
460
|
+
// Before the response, so a fork that follows finds this runner idle.
|
|
461
|
+
cache_uncached(isolate, runtime);
|
|
462
|
+
let served = respond(block, response);
|
|
463
|
+
// After the response, so the caller does not wait for it.
|
|
464
|
+
if matches!(served, Served::Next) && runtime.freed.swap(false, Ordering::Relaxed) {
|
|
465
|
+
sys::release_free_memory();
|
|
466
|
+
}
|
|
467
|
+
served
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
fn cache_uncached(isolate: &mut v8::Isolate, runtime: &Runtime) {
|
|
471
|
+
let uncached = std::mem::take(&mut *lock(&runtime.uncached));
|
|
472
|
+
if uncached.is_empty() || runtime.block.term.is_disposing() {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
let cache = runtime.generation.code_cache();
|
|
476
|
+
v8::scope!(let scope, isolate);
|
|
477
|
+
for Uncached { script, building } in uncached {
|
|
478
|
+
let script = v8::Local::new(scope, &script);
|
|
479
|
+
if let Some(data) = script.create_code_cache() {
|
|
480
|
+
cache.store(building, Box::from(&**data));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// A bundle, not a render: what parsing it frees is worth handing back.
|
|
486
|
+
const LARGE_SOURCE: usize = 1 << 20;
|
|
487
|
+
|
|
488
|
+
const fn large(source: &JsText) -> bool {
|
|
489
|
+
match source {
|
|
490
|
+
JsText::Utf8(bytes) | JsText::Latin1(bytes) => bytes.len() >= LARGE_SOURCE,
|
|
491
|
+
JsText::Utf16(units) => units.len() >= LARGE_SOURCE / 2,
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
fn respond(block: &ControlBlock, response: Result<Vec<u8>, EngineError>) -> Served {
|
|
496
|
+
if block.channel.respond(response).is_ok() {
|
|
497
|
+
Served::Next
|
|
498
|
+
} else {
|
|
499
|
+
Served::Stop { disposed: false }
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
enum Received {
|
|
504
|
+
Message(Option<ToV8<crate::control::Wire>>),
|
|
505
|
+
Idle,
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fn receive_within(block: &ControlBlock, idle: Duration) -> Received {
|
|
509
|
+
let deadline = std::time::Instant::now().checked_add(idle);
|
|
510
|
+
spin_until(IDLE_SPIN.min(idle), || block.channel.ready(Side::V8));
|
|
511
|
+
loop {
|
|
512
|
+
// As mini_racer's runner: once the deadline has passed it collects
|
|
513
|
+
// before it looks for a request, which then waits in the channel.
|
|
514
|
+
let left = deadline.map(|at| at.saturating_duration_since(std::time::Instant::now()));
|
|
515
|
+
if left.is_some_and(|left| left.is_zero()) {
|
|
516
|
+
return Received::Idle;
|
|
517
|
+
}
|
|
518
|
+
match block.channel.recv_v8() {
|
|
519
|
+
Ok(Some(message)) => return Received::Message(Some(message)),
|
|
520
|
+
Ok(None) => block.channel.park(Side::V8, left),
|
|
521
|
+
Err(_) => return Received::Message(None),
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
fn receive(block: &ControlBlock) -> Option<ToV8<crate::control::Wire>> {
|
|
527
|
+
spin_until(IDLE_SPIN, || block.channel.ready(Side::V8));
|
|
528
|
+
loop {
|
|
529
|
+
match block.channel.recv_v8() {
|
|
530
|
+
Ok(Some(message)) => return Some(message),
|
|
531
|
+
Ok(None) => block.channel.park(Side::V8, None),
|
|
532
|
+
Err(_) => return None,
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
fn handle(
|
|
538
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
539
|
+
runtime: &Runtime,
|
|
540
|
+
request: Request,
|
|
541
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
542
|
+
match request {
|
|
543
|
+
Request::Init { depth } => match depth {
|
|
544
|
+
Depth::Outermost(dispatch) => ended(runtime, dispatch),
|
|
545
|
+
Depth::Nested => Err(EngineError::Internal("setup inside a callback".to_owned())),
|
|
546
|
+
},
|
|
547
|
+
Request::Eval {
|
|
548
|
+
depth: Depth::Nested,
|
|
549
|
+
awaited: true,
|
|
550
|
+
..
|
|
551
|
+
}
|
|
552
|
+
| Request::Call {
|
|
553
|
+
depth: Depth::Nested,
|
|
554
|
+
awaited: true,
|
|
555
|
+
..
|
|
556
|
+
} => refuse_nested_await(scope, runtime),
|
|
557
|
+
Request::Eval {
|
|
558
|
+
filename,
|
|
559
|
+
source,
|
|
560
|
+
depth,
|
|
561
|
+
awaited,
|
|
562
|
+
} => script(scope, runtime, depth, DispatchKind::Script, |tc| {
|
|
563
|
+
evaluate(tc, runtime, (&filename, &source), (depth, awaited))
|
|
564
|
+
}),
|
|
565
|
+
Request::Call {
|
|
566
|
+
request,
|
|
567
|
+
depth,
|
|
568
|
+
awaited,
|
|
569
|
+
} => script(scope, runtime, depth, DispatchKind::Script, |tc| {
|
|
570
|
+
invoke(tc, runtime, &request, (depth, awaited))
|
|
571
|
+
}),
|
|
572
|
+
Request::Attach { request, depth } => {
|
|
573
|
+
script(scope, runtime, depth, DispatchKind::Maintenance, |tc| {
|
|
574
|
+
attach(tc, &request)
|
|
575
|
+
})
|
|
576
|
+
}
|
|
577
|
+
// A collection for a context about to be torn down is wasted.
|
|
578
|
+
Request::LowMemoryNotification { depth } => {
|
|
579
|
+
if !runtime.block.term.is_disposing() {
|
|
580
|
+
scope.low_memory_notification();
|
|
581
|
+
runtime.freed.store(true, Ordering::Relaxed);
|
|
582
|
+
}
|
|
583
|
+
match depth {
|
|
584
|
+
Depth::Outermost(dispatch) => ended(runtime, dispatch),
|
|
585
|
+
Depth::Nested => Ok(Vec::new()),
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// mini_racer leaves a termination this meets for the dispatch around
|
|
589
|
+
// it, or for the next script.
|
|
590
|
+
Request::MicrotaskCheckpoint { depth } => {
|
|
591
|
+
script(scope, runtime, depth, DispatchKind::Maintenance, |tc| {
|
|
592
|
+
tc.perform_microtask_checkpoint();
|
|
593
|
+
Ok(Vec::new())
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
Request::HeapStats { depth } => {
|
|
597
|
+
script(scope, runtime, depth, DispatchKind::Maintenance, |tc| {
|
|
598
|
+
Ok(heap_stats(tc))
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
Request::HeapSnapshot { depth } => {
|
|
602
|
+
script(scope, runtime, depth, DispatchKind::Maintenance, |tc| {
|
|
603
|
+
Ok(heap_snapshot(tc))
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
Request::PumpMessageLoop { depth } => {
|
|
607
|
+
script(scope, runtime, depth, DispatchKind::Pump, |tc| {
|
|
608
|
+
Ok(vec![u8::from(pump(tc, runtime))])
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
// The extension refuses a dispose from inside a callback before it
|
|
612
|
+
// gets here.
|
|
613
|
+
Request::Dispose => Err(EngineError::DisposeBusy),
|
|
614
|
+
// Snapshots are built on a context of their own, never nested.
|
|
615
|
+
Request::Snapshot { .. } => Err(EngineError::Internal(
|
|
616
|
+
"snapshot inside a callback".to_owned(),
|
|
617
|
+
)),
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// A maintenance dispatch reports only whether a dispose caught it.
|
|
622
|
+
fn ended(runtime: &Runtime, dispatch: DispatchId) -> Result<Vec<u8>, EngineError> {
|
|
623
|
+
match runtime.block.term.end(dispatch) {
|
|
624
|
+
Some(_) => Err(EngineError::Disposed),
|
|
625
|
+
None => Ok(Vec::new()),
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
fn halted(halt: Halt) -> EngineError {
|
|
630
|
+
match halt {
|
|
631
|
+
Halt::Terminated(cause) => EngineError::from(JsError::terminated(cause)),
|
|
632
|
+
// mini_racer cancels the wait of a dispatch a dispose catches in
|
|
633
|
+
// flight, which raises its disposed error.
|
|
634
|
+
Halt::Disposed => EngineError::Disposed,
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Runs a dispatch: nothing starts once it is halted. A script takes its
|
|
639
|
+
// verdict before serializing its result (`verdict`) and stays armed through
|
|
640
|
+
// it, as serializing runs getters: a cause that stops the serializing is
|
|
641
|
+
// reported, and one that lands without stopping it leaves the reply. A nested
|
|
642
|
+
// dispatch reports a halt but leaves its cause to the dispatch it runs in. An
|
|
643
|
+
// exception that escapes as the one a raising callback threw takes
|
|
644
|
+
// precedence, as in mini_racer.
|
|
645
|
+
fn script(
|
|
646
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
647
|
+
runtime: &Runtime,
|
|
648
|
+
depth: Depth,
|
|
649
|
+
kind: DispatchKind,
|
|
650
|
+
run: impl FnOnce(&mut Tc<'_, '_, '_, '_>) -> Result<Vec<u8>, EngineError>,
|
|
651
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
652
|
+
let term = &runtime.block.term;
|
|
653
|
+
v8::tc_scope!(let tc, scope);
|
|
654
|
+
tc.set_verbose(runtime.block.config.verbose_exceptions);
|
|
655
|
+
let result = if let Err(error) = arm_timeout(runtime, depth, kind) {
|
|
656
|
+
Err(error)
|
|
657
|
+
} else if term.refuses(kind) {
|
|
658
|
+
Err(EngineError::Internal("halted without a cause".to_owned()))
|
|
659
|
+
} else {
|
|
660
|
+
run(tc)
|
|
661
|
+
};
|
|
662
|
+
let since = term.mark();
|
|
663
|
+
// mini_racer's maintenance never cancels a termination it meets, so the
|
|
664
|
+
// interrupt is used up while the cause waits for the next script. Inside
|
|
665
|
+
// a callback, the termination caught here would otherwise unwind the
|
|
666
|
+
// script around it, which in mini_racer runs on and reports the cause
|
|
667
|
+
// when it ends.
|
|
668
|
+
if kind == DispatchKind::Maintenance
|
|
669
|
+
&& tc.has_terminated()
|
|
670
|
+
&& term.spend(since, matches!(depth, Depth::Nested))
|
|
671
|
+
{
|
|
672
|
+
// Lifted, the termination this caught must not be rethrown as the
|
|
673
|
+
// scope closes.
|
|
674
|
+
tc.reset();
|
|
675
|
+
}
|
|
676
|
+
let raised = result.is_err() && kind == DispatchKind::Script && escaped(tc, runtime);
|
|
677
|
+
let halt = match depth {
|
|
678
|
+
Depth::Outermost(dispatch) if result.is_ok() => term.finish(dispatch),
|
|
679
|
+
// mini_racer replies to a terminated script with its termination
|
|
680
|
+
// cancelled, and V8 runs the microtasks the script left then, still
|
|
681
|
+
// within the dispatch. A cause V8 acts on there is carried, spent, to
|
|
682
|
+
// the next script; one still pending when they finish fails the
|
|
683
|
+
// reply, and mini_racer's retry reports it with this script. When the
|
|
684
|
+
// script's own timeout ended it, mini_racer runs them with no limit;
|
|
685
|
+
// the timeout bounds them again here.
|
|
686
|
+
Depth::Outermost(dispatch) if kind == DispatchKind::Script && tc.has_terminated() => {
|
|
687
|
+
let verdict = term.settle(dispatch);
|
|
688
|
+
let drains = verdict.is_some_and(|halt| halt != Halt::Disposed)
|
|
689
|
+
&& rearm_timeout(runtime, dispatch).is_ok();
|
|
690
|
+
if drains {
|
|
691
|
+
tc.reset();
|
|
692
|
+
tc.perform_microtask_checkpoint();
|
|
693
|
+
}
|
|
694
|
+
if drains && (tc.has_terminated() || tc.is_execution_terminating()) {
|
|
695
|
+
term.spend(term.mark(), false);
|
|
696
|
+
match term.finish(dispatch) {
|
|
697
|
+
Some(Halt::Disposed) => Some(Halt::Disposed),
|
|
698
|
+
_ => verdict,
|
|
699
|
+
}
|
|
700
|
+
} else if drains {
|
|
701
|
+
term.end_drained(dispatch).or(verdict)
|
|
702
|
+
} else {
|
|
703
|
+
match term.end(dispatch) {
|
|
704
|
+
Some(Halt::Disposed) => Some(Halt::Disposed),
|
|
705
|
+
_ => verdict,
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
Depth::Outermost(dispatch) => term.end(dispatch),
|
|
710
|
+
// Its verdict came before it serialized, but a dispose still cancels
|
|
711
|
+
// the wait for it, as in mini_racer.
|
|
712
|
+
Depth::Nested if result.is_ok() && kind == DispatchKind::Script => {
|
|
713
|
+
term.is_disposing().then_some(Halt::Disposed)
|
|
714
|
+
}
|
|
715
|
+
Depth::Nested => {
|
|
716
|
+
let halt = term.check(kind);
|
|
717
|
+
if kind == DispatchKind::Script && matches!(halt, Some(Halt::Terminated(_))) {
|
|
718
|
+
term.renew();
|
|
719
|
+
}
|
|
720
|
+
halt
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
if raised {
|
|
724
|
+
return Err(EngineError::from(JsError {
|
|
725
|
+
kind: JsErrorKind::HostException,
|
|
726
|
+
message: String::new(),
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
729
|
+
halt.map_or(result, |halt| Err(halted(halt)))
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// mini_racer starts a script's watchdog on its V8 thread as it takes the
|
|
733
|
+
// request, so time spent waiting for the runner, such as behind a collection
|
|
734
|
+
// after `ensure_gc_after_idle`, never counts against the timeout. Setup and
|
|
735
|
+
// maintenance are never timed.
|
|
736
|
+
fn arm_timeout(runtime: &Runtime, depth: Depth, kind: DispatchKind) -> Result<(), EngineError> {
|
|
737
|
+
match (depth, kind, runtime.block.config.timeout) {
|
|
738
|
+
(Depth::Outermost(dispatch), DispatchKind::Script, Some(timeout)) => {
|
|
739
|
+
watchdog::schedule(timeout, &runtime.block, dispatch)
|
|
740
|
+
}
|
|
741
|
+
_ => Ok(()),
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
fn rearm_timeout(runtime: &Runtime, dispatch: DispatchId) -> Result<(), EngineError> {
|
|
746
|
+
runtime.block.config.timeout.map_or(Ok(()), |timeout| {
|
|
747
|
+
watchdog::rearm_if_fired(timeout, &runtime.block, dispatch)
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
fn escaped(tc: &Tc<'_, '_, '_, '_>, runtime: &Runtime) -> bool {
|
|
752
|
+
let Some(exception) = tc.exception() else {
|
|
753
|
+
return false;
|
|
754
|
+
};
|
|
755
|
+
lock(&runtime.raised)
|
|
756
|
+
.as_ref()
|
|
757
|
+
.is_some_and(|raised| v8::Local::new(tc, raised).same_value(exception))
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
fn evaluate(
|
|
761
|
+
tc: &mut Tc<'_, '_, '_, '_>,
|
|
762
|
+
runtime: &Runtime,
|
|
763
|
+
(filename, source): (&JsText, &JsText),
|
|
764
|
+
(depth, awaited): (Depth, bool),
|
|
765
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
766
|
+
if let Some(value) = fast_call(tc, runtime, filename, source)? {
|
|
767
|
+
return settle(tc, runtime, value, (depth, awaited));
|
|
768
|
+
}
|
|
769
|
+
let plan = if large(source) {
|
|
770
|
+
runtime.freed.store(true, Ordering::Relaxed);
|
|
771
|
+
runtime
|
|
772
|
+
.generation
|
|
773
|
+
.code_cache()
|
|
774
|
+
.plan(filename, source, runtime.snapshot)
|
|
775
|
+
} else {
|
|
776
|
+
Plan::Compile
|
|
777
|
+
};
|
|
778
|
+
let too_large = || EngineError::Internal("source too large".to_owned());
|
|
779
|
+
let source = string(tc, source).ok_or_else(too_large)?;
|
|
780
|
+
let name = string(tc, filename).ok_or_else(too_large)?;
|
|
781
|
+
let origin = serialize::origin(tc, name.into());
|
|
782
|
+
let (script, make) = compile(tc, runtime, source, &origin, plan);
|
|
783
|
+
let script = script.ok_or_else(|| failure(tc, JsErrorKind::Parse))?;
|
|
784
|
+
let value = script
|
|
785
|
+
.run(tc)
|
|
786
|
+
.ok_or_else(|| failure(tc, JsErrorKind::Runtime))?;
|
|
787
|
+
let reply = settle(tc, runtime, value, (depth, awaited))?;
|
|
788
|
+
if let Some(building) = make {
|
|
789
|
+
let script = v8::Global::new(tc, script.get_unbound_script(tc));
|
|
790
|
+
lock(&runtime.uncached).push(Uncached { script, building });
|
|
791
|
+
}
|
|
792
|
+
Ok(reply)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
fn settle<'s>(
|
|
796
|
+
tc: &mut Tc<'s, '_, '_, '_>,
|
|
797
|
+
runtime: &Runtime,
|
|
798
|
+
value: v8::Local<'s, v8::Value>,
|
|
799
|
+
(depth, awaited): (Depth, bool),
|
|
800
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
801
|
+
let value = if awaited {
|
|
802
|
+
await_promise(tc, runtime, value)?
|
|
803
|
+
} else {
|
|
804
|
+
value
|
|
805
|
+
};
|
|
806
|
+
let value = serialize::sanitize(tc, value);
|
|
807
|
+
verdict(runtime, depth)?;
|
|
808
|
+
serialize::reply(tc, &runtime.filter, value).ok_or_else(|| failure(tc, JsErrorKind::Runtime))
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// The evals ExecJS and react-rails send once per call run a template compiled
|
|
812
|
+
// once per context, a script like the source, so its frames and `this` are the
|
|
813
|
+
// source's. It reads the values the source carries, parsed as JSON instead of
|
|
814
|
+
// compiled, through a binding to `hoisted`. `None` leaves the eval to the
|
|
815
|
+
// normal path: another shape, values that are not JSON, or a template that is
|
|
816
|
+
// refused (`template`).
|
|
817
|
+
fn fast_call<'s>(
|
|
818
|
+
tc: &mut Tc<'s, '_, '_, '_>,
|
|
819
|
+
runtime: &Runtime,
|
|
820
|
+
filename: &JsText,
|
|
821
|
+
source: &JsText,
|
|
822
|
+
) -> Result<Option<v8::Local<'s, v8::Value>>, EngineError> {
|
|
823
|
+
// mini_racer's verbose `TryCatch` prints where an exception happened as
|
|
824
|
+
// its offset in the script, which a template moves.
|
|
825
|
+
if !crate::process::fast_calls() || runtime.block.config.verbose_exceptions {
|
|
826
|
+
return Ok(None);
|
|
827
|
+
}
|
|
828
|
+
let (JsText::Utf8(name), JsText::Utf8(source)) = (filename, source) else {
|
|
829
|
+
return Ok(None);
|
|
830
|
+
};
|
|
831
|
+
// A source sent again goes the normal way: from its second run there,
|
|
832
|
+
// V8's compilation cache runs it faster than a fast call parses its values
|
|
833
|
+
// again, and keeps no script per run.
|
|
834
|
+
if name != b"<eval>" || {
|
|
835
|
+
let hoisting = lock(&runtime.hoisting);
|
|
836
|
+
hoisting.last == *source
|
|
837
|
+
|| hoisting
|
|
838
|
+
.templates
|
|
839
|
+
.iter()
|
|
840
|
+
.any(|template| template.refuses(source))
|
|
841
|
+
} {
|
|
842
|
+
return Ok(None);
|
|
843
|
+
}
|
|
844
|
+
let Some(shape) = fast_calls::recognize(source) else {
|
|
845
|
+
return Ok(None);
|
|
846
|
+
};
|
|
847
|
+
remember(&mut lock(&runtime.hoisting).last, source);
|
|
848
|
+
if matches!(lock(&runtime.hoisting).binding, Binding::Unmade) {
|
|
849
|
+
bind_reader(tc, runtime);
|
|
850
|
+
}
|
|
851
|
+
let text = match &lock(&runtime.hoisting).binding {
|
|
852
|
+
Binding::Made(reader) => shape.template(reader.as_bytes()),
|
|
853
|
+
Binding::Unmade | Binding::Failed => None,
|
|
854
|
+
};
|
|
855
|
+
// Kept per context, as `last` is, so held to the same size.
|
|
856
|
+
let Some(text) = text.filter(|text| text.len() <= LARGE_SOURCE) else {
|
|
857
|
+
return declined(tc);
|
|
858
|
+
};
|
|
859
|
+
// Before parsing anything, so a render whose template is refused is never
|
|
860
|
+
// JSON-parsed.
|
|
861
|
+
let Some(script) = template(tc, runtime, text, &shape, filename) else {
|
|
862
|
+
return declined(tc);
|
|
863
|
+
};
|
|
864
|
+
let mut values = Vec::with_capacity(shape.values.len());
|
|
865
|
+
for value in &shape.values {
|
|
866
|
+
let (Hoisted::Json(literal) | Hoisted::Text(literal)) = *value;
|
|
867
|
+
let string = v8::String::new_from_utf8(tc, literal, v8::NewStringType::Normal);
|
|
868
|
+
let parsed = match value {
|
|
869
|
+
Hoisted::Json(_) => string.and_then(|string| v8::json::parse(tc, string)),
|
|
870
|
+
Hoisted::Text(_) => string.map(Into::into),
|
|
871
|
+
};
|
|
872
|
+
let Some(parsed) = parsed else {
|
|
873
|
+
return declined(tc);
|
|
874
|
+
};
|
|
875
|
+
values.push(parsed);
|
|
876
|
+
}
|
|
877
|
+
let frame = v8::Array::new_with_elements(tc, &values);
|
|
878
|
+
lock(&runtime.frames).push(v8::Global::new(tc, frame));
|
|
879
|
+
let value = script.run(tc);
|
|
880
|
+
lock(&runtime.frames).pop();
|
|
881
|
+
value
|
|
882
|
+
.map(Some)
|
|
883
|
+
.ok_or_else(|| failure(tc, JsErrorKind::Runtime))
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
fn remember(last: &mut Vec<u8>, source: &[u8]) {
|
|
887
|
+
if source.len() > LARGE_SOURCE {
|
|
888
|
+
*last = Vec::new();
|
|
889
|
+
} else {
|
|
890
|
+
last.clear();
|
|
891
|
+
last.extend_from_slice(source);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Declining leaves nothing it threw for the normal path, though a termination
|
|
896
|
+
// that arrived meanwhile still ends the dispatch.
|
|
897
|
+
fn declined<'s>(
|
|
898
|
+
tc: &mut Tc<'s, '_, '_, '_>,
|
|
899
|
+
) -> Result<Option<v8::Local<'s, v8::Value>>, EngineError> {
|
|
900
|
+
if tc.has_terminated() {
|
|
901
|
+
return Err(failure(tc, JsErrorKind::Runtime));
|
|
902
|
+
}
|
|
903
|
+
tc.reset();
|
|
904
|
+
Ok(None)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// A global `const` holding the function a template calls for its values: no
|
|
908
|
+
// property lists it, user code cannot reassign it, and its random name keeps
|
|
909
|
+
// it from meeting a global of the user's. The setup script takes the function
|
|
910
|
+
// from a global property of another random name, deleted once it has run.
|
|
911
|
+
// Made under the explicit microtask policy, so microtasks already queued run
|
|
912
|
+
// after the template's script, as they would after the source's.
|
|
913
|
+
fn bind_reader(tc: &mut Tc<'_, '_, '_, '_>, runtime: &Runtime) {
|
|
914
|
+
let [name, key] = [0, 1].map(|salt| {
|
|
915
|
+
let random = RandomState::new().hash_one((runtime.key, salt));
|
|
916
|
+
format!("__mini_racer_rs_{random:016x}")
|
|
917
|
+
});
|
|
918
|
+
let policy = tc.get_microtasks_policy();
|
|
919
|
+
tc.set_microtasks_policy(v8::MicrotasksPolicy::Explicit);
|
|
920
|
+
let global = tc.get_current_context().global(tc);
|
|
921
|
+
let made = v8::String::new(tc, &key).is_some_and(|property| {
|
|
922
|
+
let made = (|| {
|
|
923
|
+
let read = v8::Function::builder(hoisted).build(tc)?;
|
|
924
|
+
global
|
|
925
|
+
.create_data_property(tc, property.into(), read.into())
|
|
926
|
+
.filter(|&defined| defined)?;
|
|
927
|
+
let setup = v8::String::new(tc, &format!("const {name} = this[\"{key}\"];"))?;
|
|
928
|
+
v8::Script::compile(tc, setup, None)?.run(tc)
|
|
929
|
+
})()
|
|
930
|
+
.is_some();
|
|
931
|
+
global.delete(tc, property.into());
|
|
932
|
+
made
|
|
933
|
+
});
|
|
934
|
+
tc.set_microtasks_policy(policy);
|
|
935
|
+
// A termination may have cut the setup short; the next call makes another.
|
|
936
|
+
if made || !tc.has_terminated() {
|
|
937
|
+
lock(&runtime.hoisting).binding = if made {
|
|
938
|
+
Binding::Made(name)
|
|
939
|
+
} else {
|
|
940
|
+
Binding::Failed
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
fn template<'s>(
|
|
946
|
+
scope: &v8::PinScope<'s, '_>,
|
|
947
|
+
runtime: &Runtime,
|
|
948
|
+
text: Vec<u8>,
|
|
949
|
+
shape: &Shape<'_>,
|
|
950
|
+
filename: &JsText,
|
|
951
|
+
) -> Option<v8::Local<'s, v8::Script>> {
|
|
952
|
+
let mut hoisting = lock(&runtime.hoisting);
|
|
953
|
+
let kept = hoisting
|
|
954
|
+
.templates
|
|
955
|
+
.iter()
|
|
956
|
+
.position(|template| *template.text == *text);
|
|
957
|
+
if let Some(at) = kept {
|
|
958
|
+
let template = hoisting.templates.remove(at);
|
|
959
|
+
let script = template
|
|
960
|
+
.script
|
|
961
|
+
.as_ref()
|
|
962
|
+
.map(|script| v8::Local::new(scope, script));
|
|
963
|
+
hoisting.templates.push(template);
|
|
964
|
+
return script;
|
|
965
|
+
}
|
|
966
|
+
drop(hoisting);
|
|
967
|
+
let source = v8::String::new_from_utf8(scope, &text, v8::NewStringType::Normal)?;
|
|
968
|
+
let name = string(scope, filename)?;
|
|
969
|
+
let origin = serialize::origin(scope, name.into());
|
|
970
|
+
let script = shape
|
|
971
|
+
.code
|
|
972
|
+
.iter()
|
|
973
|
+
.all(|code| fast_calls::leaves_the_call_alone(code) && compiles_alone(scope, code))
|
|
974
|
+
.then(|| v8::Script::compile(scope, source, Some(&origin)))
|
|
975
|
+
.flatten();
|
|
976
|
+
let mut hoisting = lock(&runtime.hoisting);
|
|
977
|
+
if hoisting.templates.len() >= TEMPLATES {
|
|
978
|
+
hoisting.templates.remove(0);
|
|
979
|
+
}
|
|
980
|
+
let ends = (
|
|
981
|
+
shape.around.first().map_or(0, |head| head.len()),
|
|
982
|
+
shape.around.last().map_or(0, |tail| tail.len()),
|
|
983
|
+
);
|
|
984
|
+
hoisting.templates.push(Template {
|
|
985
|
+
text: text.into_boxed_slice(),
|
|
986
|
+
script: script.map(|script| v8::Global::new(scope, script)),
|
|
987
|
+
ends,
|
|
988
|
+
});
|
|
989
|
+
script
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// As a function body of its own, so its statements close before the call.
|
|
993
|
+
fn compiles_alone(scope: &v8::PinScope<'_, '_>, code: &[u8]) -> bool {
|
|
994
|
+
v8::String::new_from_utf8(scope, code, v8::NewStringType::Normal).is_some_and(|code| {
|
|
995
|
+
let mut source = v8::script_compiler::Source::new(code, None);
|
|
996
|
+
v8::script_compiler::compile_function(
|
|
997
|
+
scope,
|
|
998
|
+
&mut source,
|
|
999
|
+
&[],
|
|
1000
|
+
&[],
|
|
1001
|
+
v8::script_compiler::CompileOptions::NoCompileOptions,
|
|
1002
|
+
v8::script_compiler::NoCacheReason::NoReason,
|
|
1003
|
+
)
|
|
1004
|
+
.is_some()
|
|
1005
|
+
})
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// What a fast call's template reads in place of the literal at that index.
|
|
1009
|
+
#[expect(
|
|
1010
|
+
clippy::needless_pass_by_value,
|
|
1011
|
+
reason = "the signature V8 calls a function callback with"
|
|
1012
|
+
)]
|
|
1013
|
+
fn hoisted<'s>(
|
|
1014
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
1015
|
+
args: v8::FunctionCallbackArguments<'s>,
|
|
1016
|
+
mut rv: v8::ReturnValue<'s, v8::Value>,
|
|
1017
|
+
) {
|
|
1018
|
+
let _ = catch_unwind(AssertUnwindSafe(|| {
|
|
1019
|
+
let Some(runtime) = scope.get_slot::<Arc<Runtime>>().cloned() else {
|
|
1020
|
+
return;
|
|
1021
|
+
};
|
|
1022
|
+
let frame = lock(&runtime.frames)
|
|
1023
|
+
.last()
|
|
1024
|
+
.map(|frame| v8::Local::new(scope, frame));
|
|
1025
|
+
let index = args.get(0).uint32_value(scope);
|
|
1026
|
+
if let (Some(frame), Some(index)) = (frame, index)
|
|
1027
|
+
&& let Some(value) = frame.get_index(scope, index)
|
|
1028
|
+
{
|
|
1029
|
+
rv.set(value);
|
|
1030
|
+
}
|
|
1031
|
+
}));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// From this process's code cache when the plan has an entry. V8 refuses one
|
|
1035
|
+
// made under other flags, another V8 or another snapshot, and compiles from
|
|
1036
|
+
// source; the entry is then forgotten. Also returns the claim to cache the
|
|
1037
|
+
// script under once it has run.
|
|
1038
|
+
fn compile<'s>(
|
|
1039
|
+
scope: &v8::PinScope<'s, '_>,
|
|
1040
|
+
runtime: &Runtime,
|
|
1041
|
+
source: v8::Local<'s, v8::String>,
|
|
1042
|
+
origin: &v8::ScriptOrigin<'s>,
|
|
1043
|
+
plan: Plan,
|
|
1044
|
+
) -> (Option<v8::Local<'s, v8::Script>>, Option<Building>) {
|
|
1045
|
+
let (entry, key) = match plan {
|
|
1046
|
+
Plan::Compile => return (v8::Script::compile(scope, source, Some(origin)), None),
|
|
1047
|
+
Plan::Make(building) => {
|
|
1048
|
+
return (
|
|
1049
|
+
v8::Script::compile(scope, source, Some(origin)),
|
|
1050
|
+
Some(building),
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
Plan::Consume(entry, key) => (entry, key),
|
|
1054
|
+
};
|
|
1055
|
+
// `Source` keeps a pointer into the entry's bytes that the borrow checker
|
|
1056
|
+
// does not see, so it lives only inside this block, while `entry` does.
|
|
1057
|
+
let (script, rejected) = {
|
|
1058
|
+
let data = v8::script_compiler::CachedData::new(entry.data());
|
|
1059
|
+
let mut cached =
|
|
1060
|
+
v8::script_compiler::Source::new_with_cached_data(source, Some(origin), data);
|
|
1061
|
+
let script = v8::script_compiler::compile(
|
|
1062
|
+
scope,
|
|
1063
|
+
&mut cached,
|
|
1064
|
+
v8::script_compiler::CompileOptions::ConsumeCodeCache,
|
|
1065
|
+
v8::script_compiler::NoCacheReason::NoReason,
|
|
1066
|
+
);
|
|
1067
|
+
let rejected = cached
|
|
1068
|
+
.get_cached_data()
|
|
1069
|
+
.is_some_and(v8::CachedData::rejected);
|
|
1070
|
+
(script, rejected)
|
|
1071
|
+
};
|
|
1072
|
+
let cache = runtime.generation.code_cache();
|
|
1073
|
+
if rejected {
|
|
1074
|
+
cache.forget(&entry);
|
|
1075
|
+
return (script, cache.begin(key));
|
|
1076
|
+
}
|
|
1077
|
+
cache.hit();
|
|
1078
|
+
(script, None)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// mini_racer's last termination check, before a script's result is
|
|
1082
|
+
// serialized. A nested script reads the causes and leaves them to the
|
|
1083
|
+
// dispatch around it.
|
|
1084
|
+
fn verdict(runtime: &Runtime, depth: Depth) -> Result<(), EngineError> {
|
|
1085
|
+
let term = &runtime.block.term;
|
|
1086
|
+
let halt = match depth {
|
|
1087
|
+
Depth::Outermost(dispatch) => term.settle(dispatch),
|
|
1088
|
+
Depth::Nested => term.check(DispatchKind::Script),
|
|
1089
|
+
};
|
|
1090
|
+
halt.map_or(Ok(()), |halt| Err(halted(halt)))
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// V8 runs no microtask checkpoint inside another, so an await nested in a
|
|
1094
|
+
// callback could never settle; mini_racer refuses one before running anything.
|
|
1095
|
+
// With a stop or timeout pending, it reports the refusal as terminated, and the
|
|
1096
|
+
// refusal's text stays the message.
|
|
1097
|
+
fn refuse_nested_await(
|
|
1098
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
1099
|
+
runtime: &Runtime,
|
|
1100
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
1101
|
+
v8::tc_scope!(let tc, scope);
|
|
1102
|
+
tc.set_verbose(runtime.block.config.verbose_exceptions);
|
|
1103
|
+
let failed = |tc: &Tc<'_, '_, '_, '_>| failure(tc, JsErrorKind::Runtime);
|
|
1104
|
+
let message = v8::String::new(tc, "nested call_await/eval_await is not supported")
|
|
1105
|
+
.ok_or_else(|| failed(tc))?;
|
|
1106
|
+
let exception = v8::Exception::error(tc, message);
|
|
1107
|
+
tc.throw_exception(exception);
|
|
1108
|
+
let refused = failed(tc);
|
|
1109
|
+
let halt = runtime.block.term.check(DispatchKind::Script);
|
|
1110
|
+
if matches!(halt, Some(Halt::Terminated(_))) {
|
|
1111
|
+
runtime.block.term.renew();
|
|
1112
|
+
}
|
|
1113
|
+
let halt = halt.map(halted);
|
|
1114
|
+
Err(match (halt, refused) {
|
|
1115
|
+
(Some(EngineError::Js(halt)), EngineError::Js(refused)) => EngineError::from(JsError {
|
|
1116
|
+
kind: halt.kind,
|
|
1117
|
+
message: refused.message,
|
|
1118
|
+
}),
|
|
1119
|
+
(Some(disposed @ EngineError::Disposed), _) => disposed,
|
|
1120
|
+
(_, refused) => refused,
|
|
1121
|
+
})
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// mini_racer's await: run the microtasks, then look at the promise. While it
|
|
1125
|
+
// is pending, run due tasks one at a time, sleeping until one is due, until
|
|
1126
|
+
// the promise settles or the dispatch is halted.
|
|
1127
|
+
fn await_promise<'s>(
|
|
1128
|
+
tc: &mut Tc<'s, '_, '_, '_>,
|
|
1129
|
+
runtime: &Runtime,
|
|
1130
|
+
value: v8::Local<'s, v8::Value>,
|
|
1131
|
+
) -> Result<v8::Local<'s, v8::Value>, EngineError> {
|
|
1132
|
+
let Ok(promise) = v8::Local::<v8::Promise>::try_from(value) else {
|
|
1133
|
+
return Ok(value);
|
|
1134
|
+
};
|
|
1135
|
+
loop {
|
|
1136
|
+
tc.perform_microtask_checkpoint();
|
|
1137
|
+
match promise.state() {
|
|
1138
|
+
v8::PromiseState::Fulfilled => return Ok(promise.result(tc)),
|
|
1139
|
+
v8::PromiseState::Rejected => {
|
|
1140
|
+
let reason = promise.result(tc);
|
|
1141
|
+
tc.throw_exception(reason);
|
|
1142
|
+
return Err(failure(tc, JsErrorKind::Runtime));
|
|
1143
|
+
}
|
|
1144
|
+
v8::PromiseState::Pending => {}
|
|
1145
|
+
}
|
|
1146
|
+
if tc.is_execution_terminating() || runtime.block.term.check(DispatchKind::Script).is_some()
|
|
1147
|
+
{
|
|
1148
|
+
return Err(EngineError::from(JsError::terminated(
|
|
1149
|
+
TerminationCause::Stop,
|
|
1150
|
+
)));
|
|
1151
|
+
}
|
|
1152
|
+
match runtime.block.tasks.pop(false) {
|
|
1153
|
+
Some(task) => run_task(runtime, task),
|
|
1154
|
+
None => wait_for_task(runtime),
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Waiting, the runner holds its isolate but runs nothing in it, so a fork need
|
|
1160
|
+
// not wait for it.
|
|
1161
|
+
fn wait_for_task(runtime: &Runtime) {
|
|
1162
|
+
runtime.block.phase.set(Phase::Parked);
|
|
1163
|
+
runtime.block.tasks.wait();
|
|
1164
|
+
runtime.generation.resume(&runtime.block);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
fn run_task(runtime: &Runtime, task: v8::Task) {
|
|
1168
|
+
runtime.running.fetch_add(1, Ordering::Relaxed);
|
|
1169
|
+
let _running = Running(&runtime.running);
|
|
1170
|
+
task.run();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// mini_racer's heap statistics, in its order: the numbers V8 reports, which
|
|
1174
|
+
// it hands JavaScript as doubles.
|
|
1175
|
+
#[expect(
|
|
1176
|
+
clippy::cast_precision_loss,
|
|
1177
|
+
reason = "mini_racer passes each statistic to Ruby as a V8 number"
|
|
1178
|
+
)]
|
|
1179
|
+
fn heap_stats(isolate: &mut v8::Isolate) -> Vec<u8> {
|
|
1180
|
+
let stats = isolate.get_heap_statistics();
|
|
1181
|
+
let named = [
|
|
1182
|
+
("total_heap_size", stats.total_heap_size()),
|
|
1183
|
+
(
|
|
1184
|
+
"total_heap_size_executable",
|
|
1185
|
+
stats.total_heap_size_executable(),
|
|
1186
|
+
),
|
|
1187
|
+
("total_physical_size", stats.total_physical_size()),
|
|
1188
|
+
("total_available_size", stats.total_available_size()),
|
|
1189
|
+
(
|
|
1190
|
+
"total_global_handles_size",
|
|
1191
|
+
stats.total_global_handles_size(),
|
|
1192
|
+
),
|
|
1193
|
+
("used_global_handles_size", stats.used_global_handles_size()),
|
|
1194
|
+
("used_heap_size", stats.used_heap_size()),
|
|
1195
|
+
("heap_size_limit", stats.heap_size_limit()),
|
|
1196
|
+
("malloced_memory", stats.malloced_memory()),
|
|
1197
|
+
("external_memory", stats.external_memory()),
|
|
1198
|
+
("peak_malloced_memory", stats.peak_malloced_memory()),
|
|
1199
|
+
(
|
|
1200
|
+
"number_of_native_contexts",
|
|
1201
|
+
stats.number_of_native_contexts(),
|
|
1202
|
+
),
|
|
1203
|
+
(
|
|
1204
|
+
"number_of_detached_contexts",
|
|
1205
|
+
stats.number_of_detached_contexts(),
|
|
1206
|
+
),
|
|
1207
|
+
];
|
|
1208
|
+
let mut out = Vec::new();
|
|
1209
|
+
let mut writer = Writer::new(&mut out);
|
|
1210
|
+
writer.begin_object();
|
|
1211
|
+
for (name, value) in named {
|
|
1212
|
+
writer.utf8(name.as_bytes());
|
|
1213
|
+
writer.number(value as f64);
|
|
1214
|
+
}
|
|
1215
|
+
writer.end_object(u32::try_from(named.len()).unwrap_or(u32::MAX));
|
|
1216
|
+
out
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
fn heap_snapshot(isolate: &mut v8::Isolate) -> Vec<u8> {
|
|
1220
|
+
let mut json = Vec::new();
|
|
1221
|
+
isolate.take_heap_snapshot(|chunk| {
|
|
1222
|
+
json.extend_from_slice(chunk);
|
|
1223
|
+
true
|
|
1224
|
+
});
|
|
1225
|
+
json
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// mini_racer's pump: one due task, then a microtask checkpoint if it ran and
|
|
1229
|
+
// left nothing thrown or terminating. A pump from a running task's script
|
|
1230
|
+
// passes over non-nestable tasks, as V8's own runner does.
|
|
1231
|
+
fn pump(tc: &mut Tc<'_, '_, '_, '_>, runtime: &Runtime) -> bool {
|
|
1232
|
+
let nested = runtime.running.load(Ordering::Relaxed) > 0;
|
|
1233
|
+
let Some(task) = runtime.block.tasks.pop(nested) else {
|
|
1234
|
+
return false;
|
|
1235
|
+
};
|
|
1236
|
+
run_task(runtime, task);
|
|
1237
|
+
if !tc.is_execution_terminating() && !tc.has_caught() {
|
|
1238
|
+
tc.perform_microtask_checkpoint();
|
|
1239
|
+
}
|
|
1240
|
+
true
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
struct Reader;
|
|
1244
|
+
|
|
1245
|
+
impl v8::ValueDeserializerImpl for Reader {}
|
|
1246
|
+
|
|
1247
|
+
// `call` and `attach` requests are an array whose first element is a dotted
|
|
1248
|
+
// path; failing to read one is an internal error in mini_racer.
|
|
1249
|
+
fn read_request<'s>(
|
|
1250
|
+
tc: &Tc<'s, '_, '_, '_>,
|
|
1251
|
+
request: &[u8],
|
|
1252
|
+
) -> Option<(v8::Local<'s, v8::Object>, String)> {
|
|
1253
|
+
let context = tc.get_current_context();
|
|
1254
|
+
let deserializer = v8::ValueDeserializer::new(tc, Box::new(Reader), request);
|
|
1255
|
+
if deserializer.read_header(context) != Some(true) {
|
|
1256
|
+
return None;
|
|
1257
|
+
}
|
|
1258
|
+
let request = deserializer
|
|
1259
|
+
.read_value(context)
|
|
1260
|
+
.and_then(|request| request.to_object(tc))?;
|
|
1261
|
+
let path = request
|
|
1262
|
+
.get_index(tc, 0)
|
|
1263
|
+
.and_then(|name| name.to_string(tc))?
|
|
1264
|
+
.to_rust_string_lossy(tc);
|
|
1265
|
+
Some((request, path))
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// mini_racer's order: everything from the path walk on fails as a runtime
|
|
1269
|
+
// error. Each dotted segment but the last becomes an object, and the function
|
|
1270
|
+
// is called on its parent.
|
|
1271
|
+
fn invoke(
|
|
1272
|
+
tc: &mut Tc<'_, '_, '_, '_>,
|
|
1273
|
+
runtime: &Runtime,
|
|
1274
|
+
request: &[u8],
|
|
1275
|
+
(depth, awaited): (Depth, bool),
|
|
1276
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
1277
|
+
let context = tc.get_current_context();
|
|
1278
|
+
let failed = |tc: &Tc<'_, '_, '_, '_>| failure(tc, JsErrorKind::Runtime);
|
|
1279
|
+
let (request, path) =
|
|
1280
|
+
read_request(tc, request).ok_or_else(|| failure(tc, JsErrorKind::Internal))?;
|
|
1281
|
+
let mut receiver = context.global(tc);
|
|
1282
|
+
let mut keys = path.split('.').peekable();
|
|
1283
|
+
let mut function = None;
|
|
1284
|
+
while let Some(key) = keys.next() {
|
|
1285
|
+
let key = v8::String::new(tc, key).ok_or_else(|| failed(tc))?;
|
|
1286
|
+
let value = receiver.get(tc, key.into()).ok_or_else(|| failed(tc))?;
|
|
1287
|
+
if keys.peek().is_none() {
|
|
1288
|
+
function = Some(value);
|
|
1289
|
+
} else {
|
|
1290
|
+
receiver = value.to_object(tc).ok_or_else(|| failed(tc))?;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
let Some(Ok(function)) = function.map(v8::Local::<v8::Function>::try_from) else {
|
|
1294
|
+
let message = v8::String::new(tc, "not a function").ok_or_else(|| failed(tc))?;
|
|
1295
|
+
let exception = v8::Exception::type_error(tc, message);
|
|
1296
|
+
tc.throw_exception(exception);
|
|
1297
|
+
return Err(failed(tc));
|
|
1298
|
+
};
|
|
1299
|
+
let count = v8::Local::<v8::Array>::try_from(request).map_or(0, |array| array.length());
|
|
1300
|
+
let mut args = Vec::new();
|
|
1301
|
+
for index in 1..count {
|
|
1302
|
+
args.push(request.get_index(tc, index).ok_or_else(|| failed(tc))?);
|
|
1303
|
+
}
|
|
1304
|
+
let value = function
|
|
1305
|
+
.call(tc, receiver.into(), &args)
|
|
1306
|
+
.ok_or_else(|| failed(tc))?;
|
|
1307
|
+
let value = if awaited {
|
|
1308
|
+
await_promise(tc, runtime, value)?
|
|
1309
|
+
} else {
|
|
1310
|
+
value
|
|
1311
|
+
};
|
|
1312
|
+
let value = serialize::sanitize(tc, value);
|
|
1313
|
+
verdict(runtime, depth)?;
|
|
1314
|
+
serialize::reply(tc, &runtime.filter, value).ok_or_else(|| failed(tc))
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// mini_racer's attach: any failure is an internal error, even under a pending
|
|
1318
|
+
// termination, which attach never checks for; the message is then V8's
|
|
1319
|
+
// termination exception, "null". Each dotted segment but the last that is not
|
|
1320
|
+
// an object or function becomes a new object, and the callback is set on the
|
|
1321
|
+
// last.
|
|
1322
|
+
fn attach(tc: &Tc<'_, '_, '_, '_>, request: &[u8]) -> Result<Vec<u8>, EngineError> {
|
|
1323
|
+
let context = tc.get_current_context();
|
|
1324
|
+
let internal = |tc: &Tc<'_, '_, '_, '_>| described(tc, JsErrorKind::Internal);
|
|
1325
|
+
let (request, path) = read_request(tc, request).ok_or_else(|| internal(tc))?;
|
|
1326
|
+
let id = request
|
|
1327
|
+
.get_index(tc, 1)
|
|
1328
|
+
.filter(|id| id.is_int32())
|
|
1329
|
+
.ok_or_else(|| internal(tc))?;
|
|
1330
|
+
let mut object = context.global(tc);
|
|
1331
|
+
let mut keys = path.split('.').peekable();
|
|
1332
|
+
while let Some(key) = keys.next() {
|
|
1333
|
+
let key = v8::String::new(tc, key).ok_or_else(|| internal(tc))?;
|
|
1334
|
+
if keys.peek().is_none() {
|
|
1335
|
+
let function = v8::Function::builder(trampoline)
|
|
1336
|
+
.data(id)
|
|
1337
|
+
.build(tc)
|
|
1338
|
+
.ok_or_else(|| internal(tc))?;
|
|
1339
|
+
if object.set(tc, key.into(), function.into()) != Some(true) {
|
|
1340
|
+
return Err(internal(tc));
|
|
1341
|
+
}
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
let value = object.get(tc, key.into()).ok_or_else(|| internal(tc))?;
|
|
1345
|
+
object = if let Ok(inner) = v8::Local::<v8::Object>::try_from(value) {
|
|
1346
|
+
inner
|
|
1347
|
+
} else {
|
|
1348
|
+
let inner = v8::Object::new(tc);
|
|
1349
|
+
if object.set(tc, key.into(), inner.into()) != Some(true) {
|
|
1350
|
+
return Err(internal(tc));
|
|
1351
|
+
}
|
|
1352
|
+
inner
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
Ok(Vec::new())
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// An attached function: its arguments go to Ruby with its id, and requests
|
|
1359
|
+
// Ruby makes meanwhile are served here until the reply comes. A panic closes
|
|
1360
|
+
// the channel, as one in the runner loop does.
|
|
1361
|
+
#[expect(
|
|
1362
|
+
clippy::needless_pass_by_value,
|
|
1363
|
+
reason = "the signature V8 calls a function callback with"
|
|
1364
|
+
)]
|
|
1365
|
+
fn trampoline<'s>(
|
|
1366
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
1367
|
+
args: v8::FunctionCallbackArguments<'s>,
|
|
1368
|
+
rv: v8::ReturnValue<'s, v8::Value>,
|
|
1369
|
+
) {
|
|
1370
|
+
let Some(runtime) = scope.get_slot::<Arc<Runtime>>().cloned() else {
|
|
1371
|
+
return;
|
|
1372
|
+
};
|
|
1373
|
+
let outcome = catch_unwind(AssertUnwindSafe(|| callback(scope, &runtime, &args, rv)));
|
|
1374
|
+
if outcome.is_err() {
|
|
1375
|
+
scope.terminate_execution();
|
|
1376
|
+
runtime.block.channel.close();
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
fn callback<'s>(
|
|
1381
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
1382
|
+
runtime: &Runtime,
|
|
1383
|
+
args: &v8::FunctionCallbackArguments<'s>,
|
|
1384
|
+
mut rv: v8::ReturnValue<'s, v8::Value>,
|
|
1385
|
+
) {
|
|
1386
|
+
let count = args.length();
|
|
1387
|
+
// mini_racer builds the request in the safe context, out of reach of a
|
|
1388
|
+
// tampered Array.prototype.
|
|
1389
|
+
let request = {
|
|
1390
|
+
let safe = v8::Local::new(scope, &runtime.filter.context);
|
|
1391
|
+
let scope = &mut v8::ContextScope::new(scope, safe);
|
|
1392
|
+
v8::Array::new(scope, count.saturating_add(1))
|
|
1393
|
+
};
|
|
1394
|
+
let mut index = 0;
|
|
1395
|
+
for position in 0..count {
|
|
1396
|
+
let value = serialize::sanitize(scope, args.get(position));
|
|
1397
|
+
if request.set_index(scope, index, value).is_none() {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
index = index.saturating_add(1);
|
|
1401
|
+
}
|
|
1402
|
+
if request.set_index(scope, index, args.data()).is_none() {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
let Some(bytes) = serialize::write(scope, request.into()) else {
|
|
1406
|
+
return;
|
|
1407
|
+
};
|
|
1408
|
+
if runtime.block.channel.callback(bytes).is_err() {
|
|
1409
|
+
scope.terminate_execution();
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
loop {
|
|
1413
|
+
// Waiting for Ruby, V8 is still: a fork need not wait for this.
|
|
1414
|
+
runtime.block.phase.set(Phase::Parked);
|
|
1415
|
+
let message = receive(&runtime.block);
|
|
1416
|
+
runtime.generation.resume(&runtime.block);
|
|
1417
|
+
let reply = match message {
|
|
1418
|
+
Some(ToV8::Reply(reply)) => reply,
|
|
1419
|
+
Some(ToV8::Request(request)) => {
|
|
1420
|
+
// Each nested request gets its own handle scope, so its values
|
|
1421
|
+
// are released when it responds, not when the callback returns.
|
|
1422
|
+
let response = {
|
|
1423
|
+
v8::scope_with_context!(let scope, scope, &runtime.context);
|
|
1424
|
+
handle(scope, runtime, request)
|
|
1425
|
+
};
|
|
1426
|
+
if runtime.block.channel.respond(response).is_err() {
|
|
1427
|
+
scope.terminate_execution();
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
None => {
|
|
1433
|
+
scope.terminate_execution();
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
// A nested dispatch that was halted leaves its termination to unwind
|
|
1438
|
+
// this script too, as mini_racer restores it.
|
|
1439
|
+
let terminating = scope.is_execution_terminating();
|
|
1440
|
+
match reply {
|
|
1441
|
+
// As mini_racer answers a cancelled callback: the throw stops the
|
|
1442
|
+
// script at once, and the termination takes over at V8's next
|
|
1443
|
+
// interrupt check. Once the script's verdict is in, mini_racer
|
|
1444
|
+
// only throws to the microtasks it left.
|
|
1445
|
+
CallbackReply::Abort => {
|
|
1446
|
+
if !terminating {
|
|
1447
|
+
let exception = error(scope, &JsText::from("terminated"));
|
|
1448
|
+
scope.throw_exception(exception);
|
|
1449
|
+
}
|
|
1450
|
+
if !runtime.block.term.is_settled() {
|
|
1451
|
+
scope.terminate_execution();
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
// mini_racer throws a raising callback's exception even with a
|
|
1455
|
+
// cause pending, or a nested script's termination restored:
|
|
1456
|
+
// escaping the script, it takes precedence, and the termination
|
|
1457
|
+
// takes over if the script catches it.
|
|
1458
|
+
CallbackReply::Raised(message) => {
|
|
1459
|
+
if terminating {
|
|
1460
|
+
scope.cancel_terminate_execution();
|
|
1461
|
+
}
|
|
1462
|
+
let exception = error(scope, &message);
|
|
1463
|
+
*lock(&runtime.raised) = Some(v8::Global::new(scope, exception));
|
|
1464
|
+
scope.throw_exception(exception);
|
|
1465
|
+
if terminating {
|
|
1466
|
+
scope.terminate_execution();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
_ if terminating => {
|
|
1470
|
+
scope.terminate_execution();
|
|
1471
|
+
}
|
|
1472
|
+
// As mini_racer reads the reply: a termination pending for this
|
|
1473
|
+
// script makes the read throw it, and the script ends here.
|
|
1474
|
+
CallbackReply::Value(bytes) => {
|
|
1475
|
+
let context = scope.get_current_context();
|
|
1476
|
+
let deserializer = v8::ValueDeserializer::new(scope, Box::new(Reader), &bytes);
|
|
1477
|
+
if deserializer.read_header(context) == Some(true)
|
|
1478
|
+
&& let Some(value) = deserializer.read_value(context)
|
|
1479
|
+
{
|
|
1480
|
+
rv.set(value);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
fn error<'s>(scope: &v8::PinScope<'s, '_>, message: &JsText) -> v8::Local<'s, v8::Value> {
|
|
1489
|
+
let message = string(scope, message)
|
|
1490
|
+
.or_else(|| v8::String::new(scope, "Ruby exception"))
|
|
1491
|
+
.unwrap_or_else(|| v8::String::empty(scope));
|
|
1492
|
+
v8::Exception::error(scope, message)
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// mini_racer's suspend_termination: a failure under a pending termination is
|
|
1496
|
+
// reported as the termination.
|
|
1497
|
+
fn failure(tc: &Tc<'_, '_, '_, '_>, kind: JsErrorKind) -> EngineError {
|
|
1498
|
+
if tc.has_terminated() {
|
|
1499
|
+
return EngineError::from(JsError::terminated(TerminationCause::Stop));
|
|
1500
|
+
}
|
|
1501
|
+
described(tc, kind)
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// The message mini_racer builds for each kind: a parse error with where it
|
|
1505
|
+
// happened, a runtime error with its stack when the exception has one.
|
|
1506
|
+
fn described(tc: &Tc<'_, '_, '_, '_>, kind: JsErrorKind) -> EngineError {
|
|
1507
|
+
let message = match kind {
|
|
1508
|
+
JsErrorKind::Parse => parse_message(tc),
|
|
1509
|
+
JsErrorKind::Runtime
|
|
1510
|
+
| JsErrorKind::Internal
|
|
1511
|
+
| JsErrorKind::Terminated(_)
|
|
1512
|
+
| JsErrorKind::HostException => tc
|
|
1513
|
+
.stack_trace()
|
|
1514
|
+
.and_then(|trace| trace.to_string(tc))
|
|
1515
|
+
.map(|trace| trace.to_rust_string_lossy(tc)),
|
|
1516
|
+
};
|
|
1517
|
+
let message = message.unwrap_or_else(|| {
|
|
1518
|
+
tc.exception()
|
|
1519
|
+
.and_then(|exception| exception.to_string(tc))
|
|
1520
|
+
.map_or_else(
|
|
1521
|
+
|| "unexpected failure".to_owned(),
|
|
1522
|
+
|text| text.to_rust_string_lossy(tc),
|
|
1523
|
+
)
|
|
1524
|
+
});
|
|
1525
|
+
EngineError::from(JsError { kind, message })
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
pub fn string<'s>(
|
|
1529
|
+
scope: &v8::PinScope<'s, '_>,
|
|
1530
|
+
text: &JsText,
|
|
1531
|
+
) -> Option<v8::Local<'s, v8::String>> {
|
|
1532
|
+
match text {
|
|
1533
|
+
JsText::Utf8(bytes) => v8::String::new_from_utf8(scope, bytes, v8::NewStringType::Normal),
|
|
1534
|
+
JsText::Latin1(bytes) => {
|
|
1535
|
+
v8::String::new_from_one_byte(scope, bytes, v8::NewStringType::Normal)
|
|
1536
|
+
}
|
|
1537
|
+
JsText::Utf16(units) => {
|
|
1538
|
+
v8::String::new_from_two_byte(scope, units, v8::NewStringType::Normal)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
fn parse_message(tc: &Tc<'_, '_, '_, '_>) -> Option<String> {
|
|
1544
|
+
located(tc, " at ")
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/// The pending exception's message and where it happened, as `mini_racer`
|
|
1548
|
+
/// formats both, joined by `separator`.
|
|
1549
|
+
pub fn located(tc: &Tc<'_, '_, '_, '_>, separator: &str) -> Option<String> {
|
|
1550
|
+
let message = tc.message()?;
|
|
1551
|
+
let text = message.get(tc).to_rust_string_lossy(tc);
|
|
1552
|
+
let name = message
|
|
1553
|
+
.get_script_resource_name(tc)?
|
|
1554
|
+
.to_string(tc)?
|
|
1555
|
+
.to_rust_string_lossy(tc);
|
|
1556
|
+
let line = message.get_line_number(tc).unwrap_or(0);
|
|
1557
|
+
// V8's -1 for no column comes back as usize::MAX; mini_racer prints it signed.
|
|
1558
|
+
let column = message.get_start_column().cast_signed();
|
|
1559
|
+
Some(format!("{text}{separator}{name}:{line}:{column}"))
|
|
1560
|
+
}
|