mini_racer_rs 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/COMPATIBILITY.md +1 -1
- data/Cargo.lock +4 -4
- data/Cargo.toml +1 -1
- data/README.md +19 -13
- data/crates/mini_racer_engine/src/process/code_cache.rs +61 -25
- data/crates/mini_racer_engine/src/process/sources.rs +244 -50
- data/crates/mini_racer_engine/src/runner.rs +20 -10
- data/crates/mini_racer_wire/src/decode.rs +24 -2
- data/crates/mini_racer_wire/src/text.rs +160 -49
- data/ext/mini_racer_rs/src/decode.rs +338 -35
- data/ext/mini_racer_rs/src/gc_root.rs +67 -2
- data/ext/mini_racer_rs/src/lib.rs +4 -0
- data/ext/mini_racer_rs/src/rb_raw.rs +39 -3
- data/lib/mini_racer_rs/version.rb +1 -1
- metadata +2 -2
|
@@ -9,6 +9,7 @@ use crate::process::code_cache::Key;
|
|
|
9
9
|
const MAX_SOURCES: usize = 8;
|
|
10
10
|
const MAX_BYTES: usize = 256 << 20;
|
|
11
11
|
const MAX_REFUSED: usize = 8;
|
|
12
|
+
const MAX_SWEPT: usize = 8;
|
|
12
13
|
|
|
13
14
|
/// A large script's source as V8 stores a string of it: a byte a unit when
|
|
14
15
|
/// every unit fits Latin-1, two otherwise. Never changed once made.
|
|
@@ -63,8 +64,9 @@ impl Units {
|
|
|
63
64
|
|
|
64
65
|
/// One copy of each large script's source, outside every V8 heap.
|
|
65
66
|
///
|
|
66
|
-
/// A script is kept
|
|
67
|
-
/// compiling it
|
|
67
|
+
/// A script is kept by the first context to compile it, and every context
|
|
68
|
+
/// compiling it, that one included, reads the kept copy in place of one on
|
|
69
|
+
/// its own heap.
|
|
68
70
|
/// Each string V8 makes from one holds a reference to it, which V8 hands back
|
|
69
71
|
/// when the string dies (`sys::external_string`). A source is dropped only
|
|
70
72
|
/// when no string holds one, so V8 never releases the last reference itself.
|
|
@@ -72,6 +74,8 @@ pub struct Sources {
|
|
|
72
74
|
list: Mutex<List>,
|
|
73
75
|
// How many sources, and how many bytes of them, may be kept and read.
|
|
74
76
|
bounds: (usize, usize),
|
|
77
|
+
// The number the next context's reads are told apart by.
|
|
78
|
+
next_reader: AtomicU64,
|
|
75
79
|
made: AtomicU64,
|
|
76
80
|
reads: AtomicU64,
|
|
77
81
|
conversions: AtomicU64,
|
|
@@ -91,6 +95,9 @@ struct List {
|
|
|
91
95
|
// left or, as invalid UTF-8, unbounded, so that while it is among the last
|
|
92
96
|
// `MAX_REFUSED` refused it is not converted again without room for it.
|
|
93
97
|
refused: Vec<(Key, usize)>,
|
|
98
|
+
// The last `MAX_SWEPT` sources swept, so one made again, which a second
|
|
99
|
+
// context has now read after all, is kept as such.
|
|
100
|
+
swept: Vec<Key>,
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
struct Kept {
|
|
@@ -99,23 +106,60 @@ struct Kept {
|
|
|
99
106
|
// The references from this process's list and every ancestor's, which a
|
|
100
107
|
// forked child never touches again; the strings reading it hold the rest.
|
|
101
108
|
held: usize,
|
|
109
|
+
readers: Readers,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The contexts that have read a kept source. One alone, as a script evaluated
|
|
113
|
+
// once has, gains nothing from a copy no string reads any more; a source more
|
|
114
|
+
// have read is likely to be read again, and an inherited one is only copied
|
|
115
|
+
// again if a child drops it, so both stay until a new source needs the room.
|
|
116
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
117
|
+
enum Readers {
|
|
118
|
+
One(u64),
|
|
119
|
+
Many,
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
impl Kept {
|
|
105
123
|
fn unread(&self) -> bool {
|
|
106
124
|
self.units.references() == self.held
|
|
107
125
|
}
|
|
126
|
+
|
|
127
|
+
fn spent(&self) -> bool {
|
|
128
|
+
matches!(self.readers, Readers::One(_)) && self.unread()
|
|
129
|
+
}
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
impl List {
|
|
111
|
-
fn find(&mut self, key: &Key) -> Option<Units> {
|
|
133
|
+
fn find(&mut self, key: &Key, reader: u64) -> Option<Units> {
|
|
112
134
|
let at = self.kept.iter().position(|kept| kept.key == *key)?;
|
|
113
|
-
let found = self.kept.remove(at);
|
|
135
|
+
let mut found = self.kept.remove(at);
|
|
136
|
+
if found.readers != Readers::One(reader) {
|
|
137
|
+
found.readers = Readers::Many;
|
|
138
|
+
}
|
|
114
139
|
let units = found.units.clone();
|
|
115
140
|
self.kept.push(found);
|
|
116
141
|
Some(units)
|
|
117
142
|
}
|
|
118
143
|
|
|
144
|
+
// V8's release of a string touches only the count, so a copy whose last
|
|
145
|
+
// string has gone waits for the next `Sources::units` call.
|
|
146
|
+
fn sweep(&mut self) -> Vec<Kept> {
|
|
147
|
+
let spent: Vec<Kept> = self.kept.extract_if(.., |kept| kept.spent()).collect();
|
|
148
|
+
for kept in &spent {
|
|
149
|
+
if self.swept.len() >= MAX_SWEPT {
|
|
150
|
+
self.swept.remove(0);
|
|
151
|
+
}
|
|
152
|
+
self.swept.push(kept.key.clone());
|
|
153
|
+
}
|
|
154
|
+
spent
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn unswept(&mut self, key: &Key) -> bool {
|
|
158
|
+
let before = self.swept.len();
|
|
159
|
+
self.swept.retain(|swept| swept != key);
|
|
160
|
+
self.swept.len() < before
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
// Whether `bytes` more fit beside what strings still read, which nothing
|
|
120
164
|
// can drop.
|
|
121
165
|
fn room(&self, bytes: usize, (sources, most): (usize, usize)) -> bool {
|
|
@@ -161,30 +205,43 @@ impl Sources {
|
|
|
161
205
|
Self {
|
|
162
206
|
list: Mutex::default(),
|
|
163
207
|
bounds: (sources, bytes),
|
|
208
|
+
next_reader: AtomicU64::new(0),
|
|
164
209
|
made: AtomicU64::new(0),
|
|
165
210
|
reads: AtomicU64::new(0),
|
|
166
211
|
conversions: AtomicU64::new(0),
|
|
167
212
|
}
|
|
168
213
|
}
|
|
169
214
|
|
|
215
|
+
/// A number for a new context to read shared sources as, told apart from
|
|
216
|
+
/// every other context of this process.
|
|
217
|
+
pub(crate) fn reader(&self) -> u64 {
|
|
218
|
+
self.next_reader.fetch_add(1, Ordering::Relaxed)
|
|
219
|
+
}
|
|
220
|
+
|
|
170
221
|
/// The shared source of the script `text` under `key`, kept from the first
|
|
171
|
-
/// context that asks for it
|
|
172
|
-
/// source is invalid UTF-8, or too large for
|
|
173
|
-
/// Only a source that may fit is converted,
|
|
174
|
-
|
|
222
|
+
/// context that asks for it, for the context `reader`. `None` leaves the
|
|
223
|
+
/// context its own copy: the source is invalid UTF-8, or too large for
|
|
224
|
+
/// what is kept and still read. Only a source that may fit is converted,
|
|
225
|
+
/// outside the lock. Every call looks up the requested source, then drops
|
|
226
|
+
/// each other copy that no string reads and that one context alone ever
|
|
227
|
+
/// read.
|
|
228
|
+
pub(crate) fn units(&self, key: &Key, text: &JsText, reader: u64) -> Option<Units> {
|
|
175
229
|
let mut list = lock(&self.list);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
230
|
+
// Found before the sweep, so a copy the context before this one left
|
|
231
|
+
// unread is read again, and kept, rather than copied again.
|
|
232
|
+
let found = list.find(key, reader);
|
|
233
|
+
let spent = list.sweep();
|
|
234
|
+
let room = found.is_none() && list.room(list.least_bytes(key, text), self.bounds);
|
|
182
235
|
drop(list);
|
|
236
|
+
drop(spent);
|
|
237
|
+
if !room {
|
|
238
|
+
return found;
|
|
239
|
+
}
|
|
183
240
|
self.conversions.fetch_add(1, Ordering::Relaxed);
|
|
184
241
|
let units = Units::of(text);
|
|
185
242
|
let mut list = lock(&self.list);
|
|
186
243
|
// Made meanwhile by a context loading the same script.
|
|
187
|
-
if let Some(found) = list.find(key) {
|
|
244
|
+
if let Some(found) = list.find(key, reader) {
|
|
188
245
|
return Some(found);
|
|
189
246
|
}
|
|
190
247
|
let bytes = units.as_ref().map_or(usize::MAX, Units::bytes);
|
|
@@ -211,13 +268,20 @@ impl Sources {
|
|
|
211
268
|
};
|
|
212
269
|
dropped.push(list.kept.remove(at));
|
|
213
270
|
}
|
|
271
|
+
let readers = if list.unswept(key) {
|
|
272
|
+
Readers::Many
|
|
273
|
+
} else {
|
|
274
|
+
Readers::One(reader)
|
|
275
|
+
};
|
|
214
276
|
list.kept.push(Kept {
|
|
215
277
|
key: key.clone(),
|
|
216
278
|
units: units.clone(),
|
|
217
279
|
held: 1,
|
|
280
|
+
readers,
|
|
218
281
|
});
|
|
219
282
|
drop(list);
|
|
220
|
-
// Freed once the lock is released, as a large source takes a while
|
|
283
|
+
// Freed once the lock is released, as a large source takes a while,
|
|
284
|
+
// like the spent copies above.
|
|
221
285
|
drop(dropped);
|
|
222
286
|
self.made.fetch_add(1, Ordering::Relaxed);
|
|
223
287
|
Some(units)
|
|
@@ -242,9 +306,11 @@ impl Sources {
|
|
|
242
306
|
key: kept.key.clone(),
|
|
243
307
|
units: kept.units.clone(),
|
|
244
308
|
held: kept.held + 1,
|
|
309
|
+
readers: Readers::Many,
|
|
245
310
|
})
|
|
246
311
|
.collect(),
|
|
247
312
|
refused: list.refused.clone(),
|
|
313
|
+
swept: list.swept.clone(),
|
|
248
314
|
})
|
|
249
315
|
.unwrap_or_default();
|
|
250
316
|
Self {
|
|
@@ -291,16 +357,14 @@ mod tests {
|
|
|
291
357
|
use crate::control::JsText;
|
|
292
358
|
use crate::process::code_cache::{CodeCache, Key};
|
|
293
359
|
|
|
294
|
-
// A script's key
|
|
360
|
+
// A script's key, as its first sighting in a context sharing it has it.
|
|
295
361
|
fn key(text: &JsText) -> Key {
|
|
296
362
|
let cache: &'static CodeCache = Box::leak(Box::default());
|
|
297
|
-
let name = JsText::from("bundle.js");
|
|
298
|
-
let _ = cache.plan(&name, text, None);
|
|
299
363
|
cache
|
|
300
|
-
.plan(&
|
|
364
|
+
.plan(&JsText::from("bundle.js"), text, None, true)
|
|
301
365
|
.key()
|
|
302
366
|
.cloned()
|
|
303
|
-
.unwrap_or_else(|| panic!("a
|
|
367
|
+
.unwrap_or_else(|| panic!("a shared first sighting has a key"))
|
|
304
368
|
}
|
|
305
369
|
|
|
306
370
|
fn script(seed: u8) -> JsText {
|
|
@@ -356,8 +420,8 @@ mod tests {
|
|
|
356
420
|
let sources = Sources::default();
|
|
357
421
|
let text = script(1);
|
|
358
422
|
let key = key(&text);
|
|
359
|
-
let first = one_byte(sources.units(&key, &text));
|
|
360
|
-
let second = one_byte(sources.units(&key, &text));
|
|
423
|
+
let first = one_byte(sources.units(&key, &text, sources.reader()));
|
|
424
|
+
let second = one_byte(sources.units(&key, &text, sources.reader()));
|
|
361
425
|
assert!(Arc::ptr_eq(&first, &second));
|
|
362
426
|
assert_eq!(sources.made(), 1);
|
|
363
427
|
assert_eq!(sources.reading(), 2);
|
|
@@ -377,6 +441,12 @@ mod tests {
|
|
|
377
441
|
}
|
|
378
442
|
}
|
|
379
443
|
|
|
444
|
+
// A copy two contexts have read, the second still reading it.
|
|
445
|
+
fn shared(sources: &Sources, key: &Key, text: &JsText) -> Option<Units> {
|
|
446
|
+
drop(sources.units(key, text, sources.reader()));
|
|
447
|
+
sources.units(key, text, sources.reader())
|
|
448
|
+
}
|
|
449
|
+
|
|
380
450
|
#[test]
|
|
381
451
|
fn a_source_still_read_is_kept_and_an_unread_one_makes_room() {
|
|
382
452
|
let sources = Sources::default();
|
|
@@ -386,31 +456,108 @@ mod tests {
|
|
|
386
456
|
let mut read: Vec<Option<Units>> = texts
|
|
387
457
|
.iter()
|
|
388
458
|
.zip(&keys)
|
|
389
|
-
.map(|(text, key)| sources
|
|
459
|
+
.map(|(text, key)| shared(&sources, key, text))
|
|
390
460
|
.collect();
|
|
391
461
|
let extra = script(100);
|
|
392
462
|
let extra_key = key(&extra);
|
|
393
|
-
assert!(
|
|
463
|
+
assert!(shared(&sources, &extra_key, &extra).is_none());
|
|
394
464
|
drop(read.remove(3));
|
|
395
465
|
drop(read.remove(0));
|
|
396
|
-
drop(sources.units(&keys[3], &texts[3]));
|
|
466
|
+
drop(sources.units(&keys[3], &texts[3], sources.reader()));
|
|
397
467
|
// Of the two unread, the least recently used makes room.
|
|
398
|
-
assert!(
|
|
468
|
+
assert!(shared(&sources, &extra_key, &extra).is_some());
|
|
399
469
|
let made = sources.made();
|
|
400
470
|
assert_eq!(made, u64::from(count) + 1);
|
|
401
|
-
drop(sources.units(&keys[3], &texts[3]));
|
|
471
|
+
drop(sources.units(&keys[3], &texts[3], sources.reader()));
|
|
402
472
|
assert_eq!(sources.made(), made);
|
|
403
|
-
drop(sources.units(&keys[0], &texts[0]));
|
|
473
|
+
drop(sources.units(&keys[0], &texts[0], sources.reader()));
|
|
404
474
|
assert_eq!(sources.made(), made + 1);
|
|
405
475
|
for (at, held) in [1, 2, 4, 5, 6, 7].into_iter().zip(&read) {
|
|
406
476
|
assert!(same(
|
|
407
|
-
sources
|
|
477
|
+
sources
|
|
478
|
+
.units(&keys[at], &texts[at], sources.reader())
|
|
479
|
+
.as_ref(),
|
|
408
480
|
held.as_ref()
|
|
409
481
|
));
|
|
410
482
|
}
|
|
411
483
|
assert_eq!(sources.made(), made + 1);
|
|
412
484
|
}
|
|
413
485
|
|
|
486
|
+
// One context's copy, however often it read it, waits for no new source
|
|
487
|
+
// once no string reads it: the next call drops and frees it.
|
|
488
|
+
#[test]
|
|
489
|
+
fn a_copy_one_context_read_goes_at_the_next_call_once_unread() {
|
|
490
|
+
let sources = Sources::default();
|
|
491
|
+
let (text, other) = (script(70), script(71));
|
|
492
|
+
let (key, other_key) = (key(&text), key(&other));
|
|
493
|
+
let reader = sources.reader();
|
|
494
|
+
let first = one_byte(sources.units(&key, &text, reader));
|
|
495
|
+
let again = one_byte(sources.units(&key, &text, reader));
|
|
496
|
+
assert!(Arc::ptr_eq(&first, &again));
|
|
497
|
+
let copy = Arc::downgrade(&first);
|
|
498
|
+
drop((first, again));
|
|
499
|
+
assert!(copy.upgrade().is_some());
|
|
500
|
+
drop(sources.units(&other_key, &other, sources.reader()));
|
|
501
|
+
assert!(copy.upgrade().is_none());
|
|
502
|
+
drop(sources.units(&key, &text, sources.reader()));
|
|
503
|
+
assert_eq!(sources.made(), 3);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[test]
|
|
507
|
+
fn a_copy_two_contexts_read_in_turn_stays_once_unread() {
|
|
508
|
+
let sources = Sources::default();
|
|
509
|
+
let (text, other) = (script(72), script(73));
|
|
510
|
+
let (key, other_key) = (key(&text), key(&other));
|
|
511
|
+
let copy = Arc::downgrade(&one_byte(shared(&sources, &key, &text)));
|
|
512
|
+
drop(sources.units(&other_key, &other, sources.reader()));
|
|
513
|
+
let again = one_byte(sources.units(&key, &text, sources.reader()));
|
|
514
|
+
assert!(
|
|
515
|
+
copy.upgrade()
|
|
516
|
+
.is_some_and(|copy| Arc::ptr_eq(©, &again))
|
|
517
|
+
);
|
|
518
|
+
assert_eq!(sources.made(), 2);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#[test]
|
|
522
|
+
fn a_copy_a_second_context_reads_while_the_first_does_stays_once_unread() {
|
|
523
|
+
let sources = Sources::default();
|
|
524
|
+
let (text, other) = (script(74), script(75));
|
|
525
|
+
let (key, other_key) = (key(&text), key(&other));
|
|
526
|
+
let first = one_byte(sources.units(&key, &text, sources.reader()));
|
|
527
|
+
let second = one_byte(sources.units(&key, &text, sources.reader()));
|
|
528
|
+
let copy = Arc::downgrade(&first);
|
|
529
|
+
drop((first, second));
|
|
530
|
+
drop(sources.units(&other_key, &other, sources.reader()));
|
|
531
|
+
let again = one_byte(sources.units(&key, &text, sources.reader()));
|
|
532
|
+
assert!(
|
|
533
|
+
copy.upgrade()
|
|
534
|
+
.is_some_and(|copy| Arc::ptr_eq(©, &again))
|
|
535
|
+
);
|
|
536
|
+
assert_eq!(sources.made(), 2);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Contexts loading two scripts in turn, each gone before the next, sweep
|
|
540
|
+
// each other's copy; a copy made again after its sweep is kept as one more
|
|
541
|
+
// than one context read, so each script is converted twice in all.
|
|
542
|
+
#[test]
|
|
543
|
+
fn scripts_loaded_in_turn_by_short_lived_contexts_stay_after_their_second_copy() {
|
|
544
|
+
let sources = Sources::default();
|
|
545
|
+
let texts = [script(78), script(79)];
|
|
546
|
+
let keys = texts.each_ref().map(key);
|
|
547
|
+
for _ in 0..2 {
|
|
548
|
+
for (key, text) in keys.iter().zip(&texts) {
|
|
549
|
+
drop(sources.units(key, text, sources.reader()));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
assert_eq!(sources.made(), 4);
|
|
553
|
+
for _ in 0..3 {
|
|
554
|
+
for (key, text) in keys.iter().zip(&texts) {
|
|
555
|
+
drop(sources.units(key, text, sources.reader()));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
assert_eq!(sources.made(), 4);
|
|
559
|
+
}
|
|
560
|
+
|
|
414
561
|
#[test]
|
|
415
562
|
fn a_full_list_converts_nothing() {
|
|
416
563
|
let sources = Sources::default();
|
|
@@ -418,18 +565,26 @@ mod tests {
|
|
|
418
565
|
let read: Vec<Option<Units>> = (40..40 + count)
|
|
419
566
|
.map(|seed| {
|
|
420
567
|
let text = script(seed);
|
|
421
|
-
sources.units(&key(&text), &text)
|
|
568
|
+
sources.units(&key(&text), &text, sources.reader())
|
|
422
569
|
})
|
|
423
570
|
.collect();
|
|
424
571
|
let converted = sources.conversions();
|
|
425
572
|
let extra = script(100);
|
|
426
573
|
let extra_key = key(&extra);
|
|
427
574
|
for _ in 0..3 {
|
|
428
|
-
assert!(
|
|
575
|
+
assert!(
|
|
576
|
+
sources
|
|
577
|
+
.units(&extra_key, &extra, sources.reader())
|
|
578
|
+
.is_none()
|
|
579
|
+
);
|
|
429
580
|
}
|
|
430
581
|
assert_eq!(sources.conversions(), converted);
|
|
431
582
|
drop(read);
|
|
432
|
-
assert!(
|
|
583
|
+
assert!(
|
|
584
|
+
sources
|
|
585
|
+
.units(&extra_key, &extra, sources.reader())
|
|
586
|
+
.is_some()
|
|
587
|
+
);
|
|
433
588
|
assert_eq!(sources.conversions(), converted + 1);
|
|
434
589
|
}
|
|
435
590
|
|
|
@@ -440,21 +595,29 @@ mod tests {
|
|
|
440
595
|
fn a_source_is_converted_only_while_it_may_fit() {
|
|
441
596
|
let sources = Sources::bounded(MAX_SOURCES, 5 << 20);
|
|
442
597
|
let read = script(50);
|
|
443
|
-
let still_read = sources.units(&key(&read), &read);
|
|
598
|
+
let still_read = sources.units(&key(&read), &read, sources.reader());
|
|
444
599
|
assert!(still_read.is_some());
|
|
445
600
|
let too_large = JsText::Latin1(vec![51; 4 << 20]);
|
|
446
|
-
assert!(
|
|
601
|
+
assert!(
|
|
602
|
+
sources
|
|
603
|
+
.units(&key(&too_large), &too_large, sources.reader())
|
|
604
|
+
.is_none()
|
|
605
|
+
);
|
|
447
606
|
assert_eq!(sources.conversions(), 1);
|
|
448
607
|
let mut astral = vec![b'a'; 2 << 20];
|
|
449
608
|
astral.extend_from_slice("\u{1F600}".as_bytes());
|
|
450
609
|
let astral = JsText::Utf8(astral);
|
|
451
610
|
let astral_key = key(&astral);
|
|
452
611
|
for _ in 0..3 {
|
|
453
|
-
assert!(
|
|
612
|
+
assert!(
|
|
613
|
+
sources
|
|
614
|
+
.units(&astral_key, &astral, sources.reader())
|
|
615
|
+
.is_none()
|
|
616
|
+
);
|
|
454
617
|
}
|
|
455
618
|
assert_eq!(sources.conversions(), 2);
|
|
456
619
|
drop(still_read);
|
|
457
|
-
assert!(two_byte(sources.units(&astral_key, &astral)).len() > 2 << 20);
|
|
620
|
+
assert!(two_byte(sources.units(&astral_key, &astral, sources.reader())).len() > 2 << 20);
|
|
458
621
|
assert_eq!((sources.conversions(), sources.made()), (3, 2));
|
|
459
622
|
}
|
|
460
623
|
|
|
@@ -466,7 +629,11 @@ mod tests {
|
|
|
466
629
|
let invalid = JsText::Utf8(invalid);
|
|
467
630
|
let invalid_key = key(&invalid);
|
|
468
631
|
for _ in 0..3 {
|
|
469
|
-
assert!(
|
|
632
|
+
assert!(
|
|
633
|
+
sources
|
|
634
|
+
.units(&invalid_key, &invalid, sources.reader())
|
|
635
|
+
.is_none()
|
|
636
|
+
);
|
|
470
637
|
}
|
|
471
638
|
assert_eq!((sources.conversions(), sources.made()), (1, 0));
|
|
472
639
|
}
|
|
@@ -476,32 +643,56 @@ mod tests {
|
|
|
476
643
|
let parent: &'static Sources = Box::leak(Box::default());
|
|
477
644
|
let text = script(2);
|
|
478
645
|
let key = key(&text);
|
|
479
|
-
let shared = parent.units(&key, &text);
|
|
646
|
+
let shared = parent.units(&key, &text, parent.reader());
|
|
480
647
|
let child = parent.inherited();
|
|
481
|
-
let in_child = child.units(&key, &text);
|
|
648
|
+
let in_child = child.units(&key, &text, child.reader());
|
|
482
649
|
assert!(same(shared.as_ref(), in_child.as_ref()));
|
|
483
650
|
assert_eq!(child.reading(), 2);
|
|
484
651
|
let copy = Arc::downgrade(&one_byte(shared));
|
|
485
652
|
drop(in_child);
|
|
653
|
+
// Sources the child still reads fill its list, so the unread
|
|
654
|
+
// inherited copy makes room.
|
|
486
655
|
let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
656
|
+
let others: Vec<Option<Units>> = (30..30 + count)
|
|
657
|
+
.map(|seed| {
|
|
658
|
+
let other = script(seed);
|
|
659
|
+
child.units(&self::key(&other), &other, child.reader())
|
|
660
|
+
})
|
|
661
|
+
.collect();
|
|
491
662
|
assert_eq!(copy.strong_count(), 1);
|
|
492
|
-
|
|
663
|
+
drop(others);
|
|
664
|
+
let again = one_byte(child.units(&key, &text, child.reader()));
|
|
493
665
|
assert!(
|
|
494
666
|
!copy
|
|
495
667
|
.upgrade()
|
|
496
668
|
.is_some_and(|copy| Arc::ptr_eq(©, &again))
|
|
497
669
|
);
|
|
498
|
-
let from_parent = one_byte(parent.units(&key, &text));
|
|
670
|
+
let from_parent = one_byte(parent.units(&key, &text, parent.reader()));
|
|
499
671
|
assert!(
|
|
500
672
|
copy.upgrade()
|
|
501
673
|
.is_some_and(|copy| Arc::ptr_eq(©, &from_parent))
|
|
502
674
|
);
|
|
503
675
|
}
|
|
504
676
|
|
|
677
|
+
// Dropping an inherited copy frees nothing in a child, and its next
|
|
678
|
+
// context loading the script would copy it again, so the child keeps
|
|
679
|
+
// one only its parent's context read.
|
|
680
|
+
#[test]
|
|
681
|
+
fn a_child_keeps_an_inherited_copy_one_context_read() {
|
|
682
|
+
let parent: &'static Sources = Box::leak(Box::default());
|
|
683
|
+
let (text, other) = (script(76), script(77));
|
|
684
|
+
let (key, other_key) = (key(&text), key(&other));
|
|
685
|
+
let copy = Arc::downgrade(&one_byte(parent.units(&key, &text, parent.reader())));
|
|
686
|
+
let child = parent.inherited();
|
|
687
|
+
drop(child.units(&other_key, &other, child.reader()));
|
|
688
|
+
let in_child = one_byte(child.units(&key, &text, child.reader()));
|
|
689
|
+
assert!(
|
|
690
|
+
copy.upgrade()
|
|
691
|
+
.is_some_and(|copy| Arc::ptr_eq(©, &in_child))
|
|
692
|
+
);
|
|
693
|
+
assert_eq!(child.made(), 1);
|
|
694
|
+
}
|
|
695
|
+
|
|
505
696
|
// A child's inherited copies stay read while its parent's strings hold
|
|
506
697
|
// them, so a child forked with a full list must not convert either.
|
|
507
698
|
#[test]
|
|
@@ -511,14 +702,14 @@ mod tests {
|
|
|
511
702
|
let read: Vec<Option<Units>> = (60..60 + count)
|
|
512
703
|
.map(|seed| {
|
|
513
704
|
let text = script(seed);
|
|
514
|
-
parent.units(&key(&text), &text)
|
|
705
|
+
parent.units(&key(&text), &text, parent.reader())
|
|
515
706
|
})
|
|
516
707
|
.collect();
|
|
517
708
|
let child = parent.inherited();
|
|
518
709
|
let extra = script(110);
|
|
519
710
|
let extra_key = key(&extra);
|
|
520
711
|
for _ in 0..3 {
|
|
521
|
-
assert!(child.units(&extra_key, &extra).is_none());
|
|
712
|
+
assert!(child.units(&extra_key, &extra, child.reader()).is_none());
|
|
522
713
|
}
|
|
523
714
|
assert_eq!(child.conversions(), 0);
|
|
524
715
|
drop(read);
|
|
@@ -529,11 +720,14 @@ mod tests {
|
|
|
529
720
|
let parent = Sources::default();
|
|
530
721
|
let text = script(3);
|
|
531
722
|
let key = key(&text);
|
|
532
|
-
let shared = one_byte(parent.units(&key, &text));
|
|
723
|
+
let shared = one_byte(parent.units(&key, &text, parent.reader()));
|
|
533
724
|
let held = parent.list.lock();
|
|
534
725
|
let child = parent.inherited();
|
|
535
726
|
drop(held);
|
|
536
|
-
assert!(!Arc::ptr_eq(
|
|
727
|
+
assert!(!Arc::ptr_eq(
|
|
728
|
+
&shared,
|
|
729
|
+
&one_byte(child.units(&key, &text, child.reader()))
|
|
730
|
+
));
|
|
537
731
|
assert_eq!(child.made(), 1);
|
|
538
732
|
}
|
|
539
733
|
}
|
|
@@ -62,6 +62,9 @@ struct Runtime {
|
|
|
62
62
|
freed: AtomicBool,
|
|
63
63
|
// The startup snapshot this isolate began from, for the code cache's key.
|
|
64
64
|
snapshot: Option<u64>,
|
|
65
|
+
// Tells this context's reads of a shared source from other contexts', as
|
|
66
|
+
// a copy only one context read is dropped once no string reads it.
|
|
67
|
+
reader: u64,
|
|
65
68
|
// Large scripts to cache once their dispatch is over, so making a cache
|
|
66
69
|
// never counts against a timeout.
|
|
67
70
|
uncached: Mutex<Vec<Uncached>>,
|
|
@@ -142,6 +145,7 @@ impl Runtime {
|
|
|
142
145
|
running: AtomicU32::new(0),
|
|
143
146
|
freed: AtomicBool::new(false),
|
|
144
147
|
snapshot,
|
|
148
|
+
reader: generation.sources().reader(),
|
|
145
149
|
uncached: Mutex::default(),
|
|
146
150
|
hoisting: Mutex::default(),
|
|
147
151
|
frames: Mutex::default(),
|
|
@@ -770,10 +774,12 @@ fn evaluate(
|
|
|
770
774
|
}
|
|
771
775
|
let plan = if large(source) {
|
|
772
776
|
runtime.freed.store(true, Ordering::Relaxed);
|
|
773
|
-
runtime
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
+
runtime.generation.code_cache().plan(
|
|
778
|
+
filename,
|
|
779
|
+
source,
|
|
780
|
+
runtime.snapshot,
|
|
781
|
+
shares_sources(runtime),
|
|
782
|
+
)
|
|
777
783
|
} else {
|
|
778
784
|
Plan::Compile(None)
|
|
779
785
|
};
|
|
@@ -796,21 +802,25 @@ fn evaluate(
|
|
|
796
802
|
Ok(reply)
|
|
797
803
|
}
|
|
798
804
|
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
805
|
+
// `max_memory:` measures the heap, where mini_racer keeps the source, so a
|
|
806
|
+
// context with it keeps its own copy.
|
|
807
|
+
fn shares_sources(runtime: &Runtime) -> bool {
|
|
808
|
+
runtime.block.config.max_memory.is_none()
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// A large script reads its source from the copy every context compiling it
|
|
812
|
+
// shares, outside the V8 heap, made by the first of them.
|
|
803
813
|
fn shared_source<'s>(
|
|
804
814
|
scope: &v8::PinScope<'s, '_>,
|
|
805
815
|
runtime: &Runtime,
|
|
806
816
|
key: Option<&Key>,
|
|
807
817
|
source: &JsText,
|
|
808
818
|
) -> Option<v8::Local<'s, v8::String>> {
|
|
809
|
-
if runtime
|
|
819
|
+
if !shares_sources(runtime) {
|
|
810
820
|
return None;
|
|
811
821
|
}
|
|
812
822
|
let sources = runtime.generation.sources();
|
|
813
|
-
let string = sys::external_string(scope, &sources.units(key?, source)?)?;
|
|
823
|
+
let string = sys::external_string(scope, &sources.units(key?, source, runtime.reader)?)?;
|
|
814
824
|
sources.read();
|
|
815
825
|
Some(string)
|
|
816
826
|
}
|
|
@@ -85,6 +85,12 @@ pub trait Visitor {
|
|
|
85
85
|
|
|
86
86
|
fn end(&mut self, frame: Self::Frame) -> Result<Self::Value, Self::Error>;
|
|
87
87
|
|
|
88
|
+
/// A key that is a string, for an entry of `frame` other than an error's
|
|
89
|
+
/// fields, which would otherwise reach `scalar` as text.
|
|
90
|
+
fn key(&mut self, _frame: &Self::Frame, text: Text<'_>) -> Result<Self::Value, Self::Error> {
|
|
91
|
+
self.scalar(Scalar::Text(text))
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
/// Remembers `value` as V8's object `id` for later back references. Ids
|
|
89
95
|
/// arrive in increasing order, except that a container whose `begin`
|
|
90
96
|
/// returned no value is registered when it ends.
|
|
@@ -650,11 +656,27 @@ impl<'a, 'v, V: Visitor> Decoder<'a, 'v, V> {
|
|
|
650
656
|
}
|
|
651
657
|
|
|
652
658
|
fn insert(&mut self, frame: &mut V::Frame, depth: u32) -> Result<(), DecodeError<V::Error>> {
|
|
653
|
-
let
|
|
654
|
-
let
|
|
659
|
+
let depth = Self::child(depth)?;
|
|
660
|
+
let key = self.entry_key(frame, depth)?;
|
|
661
|
+
let value = self.value(depth)?;
|
|
655
662
|
Self::visit(self.visitor.insert(frame, key, value))
|
|
656
663
|
}
|
|
657
664
|
|
|
665
|
+
// A key that is not a string is read again as a value: handing `value` its
|
|
666
|
+
// tag would split `value` in two, and the half with the containers takes
|
|
667
|
+
// them all inline. Out of line, so `insert` stays inside the container's
|
|
668
|
+
// frame; a deep result repeats every frame on the way down.
|
|
669
|
+
#[inline(never)]
|
|
670
|
+
fn entry_key(&mut self, frame: &V::Frame, depth: u32) -> Decoded<V> {
|
|
671
|
+
let start = self.reader;
|
|
672
|
+
let tag = self.tag()?;
|
|
673
|
+
if let Some(text) = self.text(tag)? {
|
|
674
|
+
return Self::visit(self.visitor.key(frame, text));
|
|
675
|
+
}
|
|
676
|
+
self.reader = start;
|
|
677
|
+
self.value(depth)
|
|
678
|
+
}
|
|
679
|
+
|
|
658
680
|
// Reads keyed entries up to `end`, which may follow padding only if a key does.
|
|
659
681
|
fn entries(
|
|
660
682
|
&mut self,
|