mini_racer_rs 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/COMPATIBILITY.md +7 -1
- data/Cargo.lock +4 -4
- data/Cargo.toml +1 -1
- data/README.md +3 -5
- data/crates/mini_racer_engine/src/context.rs +11 -6
- data/crates/mini_racer_engine/src/control.rs +24 -3
- data/crates/mini_racer_engine/src/lib.rs +2 -1
- data/crates/mini_racer_engine/src/process/code_cache.rs +26 -15
- data/crates/mini_racer_engine/src/process/fork.rs +12 -2
- data/crates/mini_racer_engine/src/process/sources.rs +539 -0
- data/crates/mini_racer_engine/src/process.rs +1 -0
- data/crates/mini_racer_engine/src/request.rs +552 -0
- data/crates/mini_racer_engine/src/runner.rs +84 -28
- data/crates/mini_racer_engine/src/serialize.rs +65 -4
- data/crates/mini_racer_engine/src/sys.rs +91 -1
- data/crates/mini_racer_wire/src/lib.rs +1 -1
- data/crates/mini_racer_wire/src/write.rs +33 -1
- data/ext/mini_racer_rs/src/context.rs +7 -5
- data/ext/mini_racer_rs/src/decode.rs +11 -4
- data/ext/mini_racer_rs/src/encode.rs +221 -69
- data/lib/mini_racer_rs/version.rb +1 -1
- metadata +3 -1
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
2
|
+
use std::sync::{Arc, Mutex};
|
|
3
|
+
|
|
4
|
+
use mini_racer_sync::lock;
|
|
5
|
+
|
|
6
|
+
use crate::control::JsText;
|
|
7
|
+
use crate::process::code_cache::Key;
|
|
8
|
+
|
|
9
|
+
const MAX_SOURCES: usize = 8;
|
|
10
|
+
const MAX_BYTES: usize = 256 << 20;
|
|
11
|
+
const MAX_REFUSED: usize = 8;
|
|
12
|
+
|
|
13
|
+
/// A large script's source as V8 stores a string of it: a byte a unit when
|
|
14
|
+
/// every unit fits Latin-1, two otherwise. Never changed once made.
|
|
15
|
+
#[derive(Clone)]
|
|
16
|
+
pub(crate) enum Units {
|
|
17
|
+
OneByte(Arc<[u8]>),
|
|
18
|
+
TwoByte(Arc<[u16]>),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl Units {
|
|
22
|
+
// V8 decodes valid UTF-8 to the same units; invalid UTF-8 is left to its
|
|
23
|
+
// own decoder, whose replacements this does not repeat.
|
|
24
|
+
fn of(text: &JsText) -> Option<Self> {
|
|
25
|
+
Some(match text {
|
|
26
|
+
JsText::Latin1(bytes) => Self::OneByte(Arc::from(bytes.as_slice())),
|
|
27
|
+
JsText::Utf8(bytes) if bytes.is_ascii() => Self::OneByte(Arc::from(bytes.as_slice())),
|
|
28
|
+
JsText::Utf8(bytes) => {
|
|
29
|
+
let text = std::str::from_utf8(bytes).ok()?;
|
|
30
|
+
text.chars()
|
|
31
|
+
.map(u8::try_from)
|
|
32
|
+
.collect::<Result<_, _>>()
|
|
33
|
+
.map_or_else(
|
|
34
|
+
|_| Self::TwoByte(text.encode_utf16().collect()),
|
|
35
|
+
Self::OneByte,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
JsText::Utf16(units) => units
|
|
39
|
+
.iter()
|
|
40
|
+
.map(|&unit| u8::try_from(unit))
|
|
41
|
+
.collect::<Result<_, _>>()
|
|
42
|
+
.map_or_else(
|
|
43
|
+
|_| Self::TwoByte(Arc::from(units.as_slice())),
|
|
44
|
+
Self::OneByte,
|
|
45
|
+
),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn bytes(&self) -> usize {
|
|
50
|
+
match self {
|
|
51
|
+
Self::OneByte(bytes) => bytes.len(),
|
|
52
|
+
Self::TwoByte(units) => units.len() * 2,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn references(&self) -> usize {
|
|
57
|
+
match self {
|
|
58
|
+
Self::OneByte(bytes) => Arc::strong_count(bytes),
|
|
59
|
+
Self::TwoByte(units) => Arc::strong_count(units),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// One copy of each large script's source, outside every V8 heap.
|
|
65
|
+
///
|
|
66
|
+
/// A script is kept once the process has seen it before, and every context
|
|
67
|
+
/// compiling it then reads the kept copy in place of one on its own heap.
|
|
68
|
+
/// Each string V8 makes from one holds a reference to it, which V8 hands back
|
|
69
|
+
/// when the string dies (`sys::external_string`). A source is dropped only
|
|
70
|
+
/// when no string holds one, so V8 never releases the last reference itself.
|
|
71
|
+
pub struct Sources {
|
|
72
|
+
list: Mutex<List>,
|
|
73
|
+
// How many sources, and how many bytes of them, may be kept and read.
|
|
74
|
+
bounds: (usize, usize),
|
|
75
|
+
made: AtomicU64,
|
|
76
|
+
reads: AtomicU64,
|
|
77
|
+
conversions: AtomicU64,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Default for Sources {
|
|
81
|
+
fn default() -> Self {
|
|
82
|
+
Self::bounded(MAX_SOURCES, MAX_BYTES)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[derive(Default)]
|
|
87
|
+
struct List {
|
|
88
|
+
// Most recently used last.
|
|
89
|
+
kept: Vec<Kept>,
|
|
90
|
+
// What each source converted and left unkept takes, too much for the room
|
|
91
|
+
// left or, as invalid UTF-8, unbounded, so that while it is among the last
|
|
92
|
+
// `MAX_REFUSED` refused it is not converted again without room for it.
|
|
93
|
+
refused: Vec<(Key, usize)>,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
struct Kept {
|
|
97
|
+
key: Key,
|
|
98
|
+
units: Units,
|
|
99
|
+
// The references from this process's list and every ancestor's, which a
|
|
100
|
+
// forked child never touches again; the strings reading it hold the rest.
|
|
101
|
+
held: usize,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl Kept {
|
|
105
|
+
fn unread(&self) -> bool {
|
|
106
|
+
self.units.references() == self.held
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl List {
|
|
111
|
+
fn find(&mut self, key: &Key) -> Option<Units> {
|
|
112
|
+
let at = self.kept.iter().position(|kept| kept.key == *key)?;
|
|
113
|
+
let found = self.kept.remove(at);
|
|
114
|
+
let units = found.units.clone();
|
|
115
|
+
self.kept.push(found);
|
|
116
|
+
Some(units)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Whether `bytes` more fit beside what strings still read, which nothing
|
|
120
|
+
// can drop.
|
|
121
|
+
fn room(&self, bytes: usize, (sources, most): (usize, usize)) -> bool {
|
|
122
|
+
let (read, read_bytes) = self
|
|
123
|
+
.kept
|
|
124
|
+
.iter()
|
|
125
|
+
.filter(|kept| !kept.unread())
|
|
126
|
+
.fold((0, 0), |(count, total), kept| {
|
|
127
|
+
(count + 1, total + kept.units.bytes())
|
|
128
|
+
});
|
|
129
|
+
read < sources && read_bytes.saturating_add(bytes) <= most
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn refuse(&mut self, key: &Key, bytes: usize) {
|
|
133
|
+
self.refused.retain(|(refused, _)| refused != key);
|
|
134
|
+
if self.refused.len() >= MAX_REFUSED {
|
|
135
|
+
self.refused.remove(0);
|
|
136
|
+
}
|
|
137
|
+
self.refused.push((key.clone(), bytes));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn least_bytes(&self, key: &Key, text: &JsText) -> usize {
|
|
141
|
+
self.refused
|
|
142
|
+
.iter()
|
|
143
|
+
.find(|(refused, _)| refused == key)
|
|
144
|
+
.map_or_else(|| least_bytes(text), |&(_, bytes)| bytes)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The fewest bytes `text` can take as units, so a source that cannot fit is
|
|
149
|
+
// never converted: UTF-8 takes at least a byte for every two, as Latin-1
|
|
150
|
+
// written in UTF-8 does.
|
|
151
|
+
const fn least_bytes(text: &JsText) -> usize {
|
|
152
|
+
match text {
|
|
153
|
+
JsText::Latin1(bytes) => bytes.len(),
|
|
154
|
+
JsText::Utf8(bytes) => bytes.len().div_ceil(2),
|
|
155
|
+
JsText::Utf16(units) => units.len(),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
impl Sources {
|
|
160
|
+
fn bounded(sources: usize, bytes: usize) -> Self {
|
|
161
|
+
Self {
|
|
162
|
+
list: Mutex::default(),
|
|
163
|
+
bounds: (sources, bytes),
|
|
164
|
+
made: AtomicU64::new(0),
|
|
165
|
+
reads: AtomicU64::new(0),
|
|
166
|
+
conversions: AtomicU64::new(0),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// The shared source of the script `text` under `key`, kept from the first
|
|
171
|
+
/// context that asks for it. `None` leaves the context its own copy: the
|
|
172
|
+
/// source is invalid UTF-8, or too large for what is kept and still read.
|
|
173
|
+
/// Only a source that may fit is converted, outside the lock.
|
|
174
|
+
pub(crate) fn units(&self, key: &Key, text: &JsText) -> Option<Units> {
|
|
175
|
+
let mut list = lock(&self.list);
|
|
176
|
+
if let Some(units) = list.find(key) {
|
|
177
|
+
return Some(units);
|
|
178
|
+
}
|
|
179
|
+
if !list.room(list.least_bytes(key, text), self.bounds) {
|
|
180
|
+
return None;
|
|
181
|
+
}
|
|
182
|
+
drop(list);
|
|
183
|
+
self.conversions.fetch_add(1, Ordering::Relaxed);
|
|
184
|
+
let units = Units::of(text);
|
|
185
|
+
let mut list = lock(&self.list);
|
|
186
|
+
// Made meanwhile by a context loading the same script.
|
|
187
|
+
if let Some(found) = list.find(key) {
|
|
188
|
+
return Some(found);
|
|
189
|
+
}
|
|
190
|
+
let bytes = units.as_ref().map_or(usize::MAX, Units::bytes);
|
|
191
|
+
if !list.room(bytes, self.bounds) {
|
|
192
|
+
list.refuse(key, bytes);
|
|
193
|
+
drop(list);
|
|
194
|
+
return None;
|
|
195
|
+
}
|
|
196
|
+
let units = units?;
|
|
197
|
+
list.refused.retain(|(refused, _)| refused != key);
|
|
198
|
+
let (sources, most) = self.bounds;
|
|
199
|
+
let mut dropped = Vec::new();
|
|
200
|
+
while list.kept.len() >= sources
|
|
201
|
+
|| list
|
|
202
|
+
.kept
|
|
203
|
+
.iter()
|
|
204
|
+
.map(|kept| kept.units.bytes())
|
|
205
|
+
.sum::<usize>()
|
|
206
|
+
+ bytes
|
|
207
|
+
> most
|
|
208
|
+
{
|
|
209
|
+
let Some(at) = list.kept.iter().position(Kept::unread) else {
|
|
210
|
+
break;
|
|
211
|
+
};
|
|
212
|
+
dropped.push(list.kept.remove(at));
|
|
213
|
+
}
|
|
214
|
+
list.kept.push(Kept {
|
|
215
|
+
key: key.clone(),
|
|
216
|
+
units: units.clone(),
|
|
217
|
+
held: 1,
|
|
218
|
+
});
|
|
219
|
+
drop(list);
|
|
220
|
+
// Freed once the lock is released, as a large source takes a while.
|
|
221
|
+
drop(dropped);
|
|
222
|
+
self.made.fetch_add(1, Ordering::Relaxed);
|
|
223
|
+
Some(units)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pub(crate) fn read(&self) {
|
|
227
|
+
self.reads.fetch_add(1, Ordering::Relaxed);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// A forked child's sources: what its parent kept, unless a thread of the
|
|
231
|
+
/// parent held them at the fork. The parent's list keeps its references
|
|
232
|
+
/// in the child, so the child frees none of these.
|
|
233
|
+
pub(crate) fn inherited(&self) -> Self {
|
|
234
|
+
let list = self
|
|
235
|
+
.list
|
|
236
|
+
.try_lock()
|
|
237
|
+
.map(|list| List {
|
|
238
|
+
kept: list
|
|
239
|
+
.kept
|
|
240
|
+
.iter()
|
|
241
|
+
.map(|kept| Kept {
|
|
242
|
+
key: kept.key.clone(),
|
|
243
|
+
units: kept.units.clone(),
|
|
244
|
+
held: kept.held + 1,
|
|
245
|
+
})
|
|
246
|
+
.collect(),
|
|
247
|
+
refused: list.refused.clone(),
|
|
248
|
+
})
|
|
249
|
+
.unwrap_or_default();
|
|
250
|
+
Self {
|
|
251
|
+
list: Mutex::new(list),
|
|
252
|
+
..Self::bounded(self.bounds.0, self.bounds.1)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Sources this process made and kept.
|
|
257
|
+
#[must_use]
|
|
258
|
+
pub fn made(&self) -> u64 {
|
|
259
|
+
self.made.load(Ordering::Relaxed)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Strings this process's contexts made from a kept source.
|
|
263
|
+
#[must_use]
|
|
264
|
+
pub fn reads(&self) -> u64 {
|
|
265
|
+
self.reads.load(Ordering::Relaxed)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Sources this process converted to the units V8 reads, kept or not.
|
|
269
|
+
#[must_use]
|
|
270
|
+
pub fn conversions(&self) -> u64 {
|
|
271
|
+
self.conversions.load(Ordering::Relaxed)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Strings alive now that read a kept source.
|
|
275
|
+
#[must_use]
|
|
276
|
+
pub fn reading(&self) -> usize {
|
|
277
|
+
lock(&self.list)
|
|
278
|
+
.kept
|
|
279
|
+
.iter()
|
|
280
|
+
.map(|kept| kept.units.references().saturating_sub(kept.held))
|
|
281
|
+
.sum()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[cfg(test)]
|
|
286
|
+
#[allow(clippy::panic, reason = "a test fails by panicking")]
|
|
287
|
+
mod tests {
|
|
288
|
+
use std::sync::Arc;
|
|
289
|
+
|
|
290
|
+
use super::{MAX_SOURCES, Sources, Units};
|
|
291
|
+
use crate::control::JsText;
|
|
292
|
+
use crate::process::code_cache::{CodeCache, Key};
|
|
293
|
+
|
|
294
|
+
// A script's key once the code cache has seen it twice.
|
|
295
|
+
fn key(text: &JsText) -> Key {
|
|
296
|
+
let cache: &'static CodeCache = Box::leak(Box::default());
|
|
297
|
+
let name = JsText::from("bundle.js");
|
|
298
|
+
let _ = cache.plan(&name, text, None);
|
|
299
|
+
cache
|
|
300
|
+
.plan(&name, text, None)
|
|
301
|
+
.key()
|
|
302
|
+
.cloned()
|
|
303
|
+
.unwrap_or_else(|| panic!("a second sighting has a key"))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fn script(seed: u8) -> JsText {
|
|
307
|
+
JsText::Latin1(vec![seed; 2 << 20])
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn one_byte(units: Option<Units>) -> Arc<[u8]> {
|
|
311
|
+
match units {
|
|
312
|
+
Some(Units::OneByte(bytes)) => bytes,
|
|
313
|
+
_ => panic!("one byte a unit"),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn two_byte(units: Option<Units>) -> Arc<[u16]> {
|
|
318
|
+
match units {
|
|
319
|
+
Some(Units::TwoByte(units)) => units,
|
|
320
|
+
_ => panic!("two bytes a unit"),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[test]
|
|
325
|
+
fn keeps_a_source_in_the_form_v8_stores_a_string_of_it() {
|
|
326
|
+
assert_eq!(
|
|
327
|
+
*one_byte(Units::of(&JsText::Latin1(vec![b'a', 0xE9]))),
|
|
328
|
+
[b'a', 0xE9]
|
|
329
|
+
);
|
|
330
|
+
assert_eq!(*one_byte(Units::of(&JsText::from("a\0b"))), *b"a\0b");
|
|
331
|
+
assert_eq!(
|
|
332
|
+
*two_byte(Units::of(&JsText::from("\u{FEFF}a"))),
|
|
333
|
+
[0xFEFF, 0x61]
|
|
334
|
+
);
|
|
335
|
+
assert_eq!(
|
|
336
|
+
*one_byte(Units::of(&JsText::from("caf\u{E9}\u{FF}"))),
|
|
337
|
+
*b"caf\xE9\xFF"
|
|
338
|
+
);
|
|
339
|
+
assert_eq!(
|
|
340
|
+
*two_byte(Units::of(&JsText::from("a\u{100}\u{1F600}"))),
|
|
341
|
+
[0x61, 0x100, 0xD83D, 0xDE00]
|
|
342
|
+
);
|
|
343
|
+
assert_eq!(
|
|
344
|
+
*one_byte(Units::of(&JsText::Utf16(vec![0x61, 0xE9]))),
|
|
345
|
+
[0x61, 0xE9]
|
|
346
|
+
);
|
|
347
|
+
assert_eq!(
|
|
348
|
+
*two_byte(Units::of(&JsText::Utf16(vec![0xD800, 0x61, 0xDC00]))),
|
|
349
|
+
[0xD800, 0x61, 0xDC00]
|
|
350
|
+
);
|
|
351
|
+
assert!(Units::of(&JsText::Utf8(vec![b'a', 0xFF])).is_none());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn every_context_compiling_a_script_reads_one_copy() {
|
|
356
|
+
let sources = Sources::default();
|
|
357
|
+
let text = script(1);
|
|
358
|
+
let key = key(&text);
|
|
359
|
+
let first = one_byte(sources.units(&key, &text));
|
|
360
|
+
let second = one_byte(sources.units(&key, &text));
|
|
361
|
+
assert!(Arc::ptr_eq(&first, &second));
|
|
362
|
+
assert_eq!(sources.made(), 1);
|
|
363
|
+
assert_eq!(sources.reading(), 2);
|
|
364
|
+
drop((first, second));
|
|
365
|
+
assert_eq!(sources.reading(), 0);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn same(first: Option<&Units>, second: Option<&Units>) -> bool {
|
|
369
|
+
match (first, second) {
|
|
370
|
+
(Some(Units::OneByte(first)), Some(Units::OneByte(second))) => {
|
|
371
|
+
Arc::ptr_eq(first, second)
|
|
372
|
+
}
|
|
373
|
+
(Some(Units::TwoByte(first)), Some(Units::TwoByte(second))) => {
|
|
374
|
+
Arc::ptr_eq(first, second)
|
|
375
|
+
}
|
|
376
|
+
_ => false,
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[test]
|
|
381
|
+
fn a_source_still_read_is_kept_and_an_unread_one_makes_room() {
|
|
382
|
+
let sources = Sources::default();
|
|
383
|
+
let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
|
|
384
|
+
let texts: Vec<JsText> = (10..10 + count).map(script).collect();
|
|
385
|
+
let keys: Vec<Key> = texts.iter().map(key).collect();
|
|
386
|
+
let mut read: Vec<Option<Units>> = texts
|
|
387
|
+
.iter()
|
|
388
|
+
.zip(&keys)
|
|
389
|
+
.map(|(text, key)| sources.units(key, text))
|
|
390
|
+
.collect();
|
|
391
|
+
let extra = script(100);
|
|
392
|
+
let extra_key = key(&extra);
|
|
393
|
+
assert!(sources.units(&extra_key, &extra).is_none());
|
|
394
|
+
drop(read.remove(3));
|
|
395
|
+
drop(read.remove(0));
|
|
396
|
+
drop(sources.units(&keys[3], &texts[3]));
|
|
397
|
+
// Of the two unread, the least recently used makes room.
|
|
398
|
+
assert!(sources.units(&extra_key, &extra).is_some());
|
|
399
|
+
let made = sources.made();
|
|
400
|
+
assert_eq!(made, u64::from(count) + 1);
|
|
401
|
+
drop(sources.units(&keys[3], &texts[3]));
|
|
402
|
+
assert_eq!(sources.made(), made);
|
|
403
|
+
drop(sources.units(&keys[0], &texts[0]));
|
|
404
|
+
assert_eq!(sources.made(), made + 1);
|
|
405
|
+
for (at, held) in [1, 2, 4, 5, 6, 7].into_iter().zip(&read) {
|
|
406
|
+
assert!(same(
|
|
407
|
+
sources.units(&keys[at], &texts[at]).as_ref(),
|
|
408
|
+
held.as_ref()
|
|
409
|
+
));
|
|
410
|
+
}
|
|
411
|
+
assert_eq!(sources.made(), made + 1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[test]
|
|
415
|
+
fn a_full_list_converts_nothing() {
|
|
416
|
+
let sources = Sources::default();
|
|
417
|
+
let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
|
|
418
|
+
let read: Vec<Option<Units>> = (40..40 + count)
|
|
419
|
+
.map(|seed| {
|
|
420
|
+
let text = script(seed);
|
|
421
|
+
sources.units(&key(&text), &text)
|
|
422
|
+
})
|
|
423
|
+
.collect();
|
|
424
|
+
let converted = sources.conversions();
|
|
425
|
+
let extra = script(100);
|
|
426
|
+
let extra_key = key(&extra);
|
|
427
|
+
for _ in 0..3 {
|
|
428
|
+
assert!(sources.units(&extra_key, &extra).is_none());
|
|
429
|
+
}
|
|
430
|
+
assert_eq!(sources.conversions(), converted);
|
|
431
|
+
drop(read);
|
|
432
|
+
assert!(sources.units(&extra_key, &extra).is_some());
|
|
433
|
+
assert_eq!(sources.conversions(), converted + 1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Under a 5 MiB bound with 2 MiB still read, 4 MiB of Latin-1 cannot fit
|
|
437
|
+
// and is never converted; 2 MiB of UTF-8 may fit, until converting it
|
|
438
|
+
// shows one astral character makes it 4.
|
|
439
|
+
#[test]
|
|
440
|
+
fn a_source_is_converted_only_while_it_may_fit() {
|
|
441
|
+
let sources = Sources::bounded(MAX_SOURCES, 5 << 20);
|
|
442
|
+
let read = script(50);
|
|
443
|
+
let still_read = sources.units(&key(&read), &read);
|
|
444
|
+
assert!(still_read.is_some());
|
|
445
|
+
let too_large = JsText::Latin1(vec![51; 4 << 20]);
|
|
446
|
+
assert!(sources.units(&key(&too_large), &too_large).is_none());
|
|
447
|
+
assert_eq!(sources.conversions(), 1);
|
|
448
|
+
let mut astral = vec![b'a'; 2 << 20];
|
|
449
|
+
astral.extend_from_slice("\u{1F600}".as_bytes());
|
|
450
|
+
let astral = JsText::Utf8(astral);
|
|
451
|
+
let astral_key = key(&astral);
|
|
452
|
+
for _ in 0..3 {
|
|
453
|
+
assert!(sources.units(&astral_key, &astral).is_none());
|
|
454
|
+
}
|
|
455
|
+
assert_eq!(sources.conversions(), 2);
|
|
456
|
+
drop(still_read);
|
|
457
|
+
assert!(two_byte(sources.units(&astral_key, &astral)).len() > 2 << 20);
|
|
458
|
+
assert_eq!((sources.conversions(), sources.made()), (3, 2));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#[test]
|
|
462
|
+
fn invalid_utf8_is_tried_once() {
|
|
463
|
+
let sources = Sources::default();
|
|
464
|
+
let mut invalid = vec![b'a'; 2 << 20];
|
|
465
|
+
invalid.push(0xFF);
|
|
466
|
+
let invalid = JsText::Utf8(invalid);
|
|
467
|
+
let invalid_key = key(&invalid);
|
|
468
|
+
for _ in 0..3 {
|
|
469
|
+
assert!(sources.units(&invalid_key, &invalid).is_none());
|
|
470
|
+
}
|
|
471
|
+
assert_eq!((sources.conversions(), sources.made()), (1, 0));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#[test]
|
|
475
|
+
fn a_child_reads_its_parents_copy_and_never_frees_it() {
|
|
476
|
+
let parent: &'static Sources = Box::leak(Box::default());
|
|
477
|
+
let text = script(2);
|
|
478
|
+
let key = key(&text);
|
|
479
|
+
let shared = parent.units(&key, &text);
|
|
480
|
+
let child = parent.inherited();
|
|
481
|
+
let in_child = child.units(&key, &text);
|
|
482
|
+
assert!(same(shared.as_ref(), in_child.as_ref()));
|
|
483
|
+
assert_eq!(child.reading(), 2);
|
|
484
|
+
let copy = Arc::downgrade(&one_byte(shared));
|
|
485
|
+
drop(in_child);
|
|
486
|
+
let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
|
|
487
|
+
for seed in 30..30 + count {
|
|
488
|
+
let other = script(seed);
|
|
489
|
+
drop(child.units(&self::key(&other), &other));
|
|
490
|
+
}
|
|
491
|
+
assert_eq!(copy.strong_count(), 1);
|
|
492
|
+
let again = one_byte(child.units(&key, &text));
|
|
493
|
+
assert!(
|
|
494
|
+
!copy
|
|
495
|
+
.upgrade()
|
|
496
|
+
.is_some_and(|copy| Arc::ptr_eq(©, &again))
|
|
497
|
+
);
|
|
498
|
+
let from_parent = one_byte(parent.units(&key, &text));
|
|
499
|
+
assert!(
|
|
500
|
+
copy.upgrade()
|
|
501
|
+
.is_some_and(|copy| Arc::ptr_eq(©, &from_parent))
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// A child's inherited copies stay read while its parent's strings hold
|
|
506
|
+
// them, so a child forked with a full list must not convert either.
|
|
507
|
+
#[test]
|
|
508
|
+
fn a_child_inheriting_a_full_list_converts_nothing() {
|
|
509
|
+
let parent = Sources::default();
|
|
510
|
+
let count = u8::try_from(MAX_SOURCES).unwrap_or(u8::MAX);
|
|
511
|
+
let read: Vec<Option<Units>> = (60..60 + count)
|
|
512
|
+
.map(|seed| {
|
|
513
|
+
let text = script(seed);
|
|
514
|
+
parent.units(&key(&text), &text)
|
|
515
|
+
})
|
|
516
|
+
.collect();
|
|
517
|
+
let child = parent.inherited();
|
|
518
|
+
let extra = script(110);
|
|
519
|
+
let extra_key = key(&extra);
|
|
520
|
+
for _ in 0..3 {
|
|
521
|
+
assert!(child.units(&extra_key, &extra).is_none());
|
|
522
|
+
}
|
|
523
|
+
assert_eq!(child.conversions(), 0);
|
|
524
|
+
drop(read);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#[test]
|
|
528
|
+
fn a_child_forked_while_the_sources_were_held_starts_without_them() {
|
|
529
|
+
let parent = Sources::default();
|
|
530
|
+
let text = script(3);
|
|
531
|
+
let key = key(&text);
|
|
532
|
+
let shared = one_byte(parent.units(&key, &text));
|
|
533
|
+
let held = parent.list.lock();
|
|
534
|
+
let child = parent.inherited();
|
|
535
|
+
drop(held);
|
|
536
|
+
assert!(!Arc::ptr_eq(&shared, &one_byte(child.units(&key, &text))));
|
|
537
|
+
assert_eq!(child.made(), 1);
|
|
538
|
+
}
|
|
539
|
+
}
|