mini_racer_rs 1.0.3 → 1.0.5
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/lib.rs +2 -0
- 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/request.rs +58 -22
- data/crates/mini_racer_engine/src/runner.rs +20 -10
- data/crates/mini_racer_engine/src/sys.rs +37 -0
- data/crates/mini_racer_engine/src/text.rs +182 -0
- data/crates/mini_racer_wire/src/write.rs +8 -3
- data/ext/mini_racer_rs/src/encode.rs +77 -15
- data/ext/mini_racer_rs/src/rb_raw.rs +13 -0
- data/lib/mini_racer_rs/version.rb +1 -1
- metadata +3 -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
|
}
|
|
@@ -4,6 +4,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
4
4
|
use mini_racer_wire::{WRITE_VERSION, tag};
|
|
5
5
|
use v8::ValueDeserializerHelper;
|
|
6
6
|
|
|
7
|
+
use crate::sys;
|
|
8
|
+
|
|
7
9
|
// Deeper values are left to V8, which reads as deep as its stack allows.
|
|
8
10
|
const MAX_DEPTH: u32 = 64;
|
|
9
11
|
// Far below V8's own limit on an array's length, which it enforces itself.
|
|
@@ -48,16 +50,16 @@ pub const fn worth_building(objects: usize, properties: usize) -> bool {
|
|
|
48
50
|
/// which its deserializer makes, element by element as a sparse array, so a
|
|
49
51
|
/// function returning an argument costs far more than it need.
|
|
50
52
|
///
|
|
51
|
-
/// A two-byte string is read by V8's own
|
|
52
|
-
///
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
///
|
|
57
|
-
///
|
|
58
|
-
///
|
|
59
|
-
///
|
|
60
|
-
/// (`TermCell::refuses`).
|
|
53
|
+
/// A two-byte string whose units all fit Latin-1 is read by V8's own
|
|
54
|
+
/// deserializer from its bytes alone, which keeps it two-byte as V8's read of
|
|
55
|
+
/// the whole would. The whole request is `Unbuilt` for anything else this would
|
|
56
|
+
/// not build as V8 does or that V8 would refuse (a binary, a key that is not a
|
|
57
|
+
/// string or an int32, a key given twice, an array inside itself, an object of
|
|
58
|
+
/// more than `MAX_KEYS` keys, nesting past `MAX_DEPTH`), decided from the bytes
|
|
59
|
+
/// before any value is made. A termination requested as it reads leaves it
|
|
60
|
+
/// `Unbuilt` too: V8 acts on one before each value it reads, an array or object
|
|
61
|
+
/// beginning as a value does, so this looks before the request and before each
|
|
62
|
+
/// value. One requested before the dispatch refused it (`TermCell::refuses`).
|
|
61
63
|
pub fn read<'s, 'b>(
|
|
62
64
|
scope: &v8::PinScope<'s, '_>,
|
|
63
65
|
bytes: &'b [u8],
|
|
@@ -74,6 +76,7 @@ pub fn read<'s, 'b>(
|
|
|
74
76
|
objects: Vec::new(),
|
|
75
77
|
elements: Vec::new(),
|
|
76
78
|
shapes: Vec::new(),
|
|
79
|
+
units: Vec::new(),
|
|
77
80
|
};
|
|
78
81
|
match builder.call() {
|
|
79
82
|
Some(call) => Read::Built(call),
|
|
@@ -119,6 +122,17 @@ impl<'b> Cursor<'b> {
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
// A value's tag, past the padding V8 skips, which aligns a two-byte
|
|
126
|
+
// string's units.
|
|
127
|
+
fn tag(&mut self) -> Option<u8> {
|
|
128
|
+
loop {
|
|
129
|
+
match self.byte()? {
|
|
130
|
+
tag::PADDING => {}
|
|
131
|
+
tag => return Some(tag),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
fn varint(&mut self) -> Option<u64> {
|
|
123
137
|
let mut value = 0u64;
|
|
124
138
|
for shift in (0..64).step_by(7) {
|
|
@@ -153,9 +167,9 @@ impl<'b> Cursor<'b> {
|
|
|
153
167
|
.filter(|text| text.len() <= v8::String::MAX_LENGTH)
|
|
154
168
|
}
|
|
155
169
|
|
|
156
|
-
fn two_byte(&mut self) -> Option
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
fn two_byte(&mut self) -> Option<&'b [u8]> {
|
|
171
|
+
self.text()
|
|
172
|
+
.filter(|text| text.len().is_multiple_of(2) && text.len() / 2 <= v8::String::MAX_LENGTH)
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
// The length of `[path, ...arguments]`, past V8's header.
|
|
@@ -312,7 +326,7 @@ impl Check<'_> {
|
|
|
312
326
|
}
|
|
313
327
|
|
|
314
328
|
fn value(&mut self, depth: u32) -> Option<()> {
|
|
315
|
-
match self.bytes.
|
|
329
|
+
match self.bytes.tag()? {
|
|
316
330
|
tag::INT32 => {
|
|
317
331
|
self.bytes.varint_u32()?;
|
|
318
332
|
}
|
|
@@ -323,7 +337,9 @@ impl Check<'_> {
|
|
|
323
337
|
tag::UTF8_STRING | tag::ONE_BYTE_STRING => {
|
|
324
338
|
self.bytes.string()?;
|
|
325
339
|
}
|
|
326
|
-
tag::TWO_BYTE_STRING =>
|
|
340
|
+
tag::TWO_BYTE_STRING => {
|
|
341
|
+
self.bytes.two_byte()?;
|
|
342
|
+
}
|
|
327
343
|
tag::BIGINT => {
|
|
328
344
|
self.bytes.bigint_bytes()?;
|
|
329
345
|
}
|
|
@@ -393,6 +409,9 @@ struct Builder<'s, 'b, 'p, 'i> {
|
|
|
393
409
|
// The names last made for each key position at each depth, so objects of
|
|
394
410
|
// one shape share them instead of looking each up again.
|
|
395
411
|
shapes: Vec<Vec<(Key<'b>, v8::Local<'s, v8::String>)>>,
|
|
412
|
+
// A two-byte string's units where the request's bytes hold them
|
|
413
|
+
// unaligned.
|
|
414
|
+
units: Vec<u16>,
|
|
396
415
|
}
|
|
397
416
|
|
|
398
417
|
// Reads again what `Check` accepted, so only a termination stops it, or V8's
|
|
@@ -419,7 +438,7 @@ impl<'s, 'b> Builder<'s, 'b, '_, '_> {
|
|
|
419
438
|
self.unterminated()?;
|
|
420
439
|
let scope = self.scope;
|
|
421
440
|
let at = self.bytes;
|
|
422
|
-
Some(match self.bytes.
|
|
441
|
+
Some(match self.bytes.tag()? {
|
|
423
442
|
tag::INT32 => v8::Integer::new(scope, zigzag(self.bytes.varint_u32()?)).into(),
|
|
424
443
|
tag::DOUBLE => v8::Number::new(scope, self.bytes.double()?).into(),
|
|
425
444
|
tag::NULL => v8::null(scope).into(),
|
|
@@ -459,17 +478,34 @@ impl<'s, 'b> Builder<'s, 'b, '_, '_> {
|
|
|
459
478
|
tag::BEGIN_DENSE_ARRAY => self.array(depth.checked_sub(1)?)?,
|
|
460
479
|
tag::BEGIN_OBJECT => self.object(depth.checked_sub(1)?)?,
|
|
461
480
|
tag::TWO_BYTE_STRING => {
|
|
462
|
-
self.bytes.two_byte()?;
|
|
463
|
-
self.two_byte(at)?
|
|
481
|
+
let text = self.bytes.two_byte()?;
|
|
482
|
+
self.two_byte(text, at)?
|
|
464
483
|
}
|
|
465
484
|
_ => return None,
|
|
466
485
|
})
|
|
467
486
|
}
|
|
468
487
|
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
488
|
+
// V8's API makes a one-byte string of units that all fit Latin-1, where
|
|
489
|
+
// its deserializer keeps them two-byte, so only a string with a wider
|
|
490
|
+
// unit is made through the API, from the request's own bytes where the
|
|
491
|
+
// padding before them aligned them.
|
|
492
|
+
fn two_byte(&mut self, text: &[u8], at: Cursor<'b>) -> Option<v8::Local<'s, v8::Value>> {
|
|
493
|
+
let pairs = text.as_chunks::<2>().0;
|
|
494
|
+
if !pairs.iter().any(|&[_, high]| high != 0) {
|
|
495
|
+
return self.deserialized(at);
|
|
496
|
+
}
|
|
497
|
+
let units = if let Some(units) = sys::units(text) {
|
|
498
|
+
units
|
|
499
|
+
} else {
|
|
500
|
+
self.units.clear();
|
|
501
|
+
self.units
|
|
502
|
+
.extend(pairs.iter().map(|&pair| u16::from_le_bytes(pair)));
|
|
503
|
+
&self.units
|
|
504
|
+
};
|
|
505
|
+
v8::String::new_from_two_byte(self.scope, units, v8::NewStringType::Normal).map(Into::into)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fn deserialized(&mut self, at: Cursor<'b>) -> Option<v8::Local<'s, v8::Value>> {
|
|
473
509
|
let len = at.rest.len().checked_sub(self.bytes.rest.len())?;
|
|
474
510
|
let mut bytes = vec![tag::HEADER, u8::try_from(WRITE_VERSION).ok()?];
|
|
475
511
|
bytes.extend_from_slice(at.rest.get(..len)?);
|