rusty_racer 0.1.3 → 0.1.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/README.md +10 -1
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +97 -63
- data/ext/rusty_racer/src/ops.rs +2 -2
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +16 -8
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23ab2fe2f98436d0c48cc380809611878f1a7fc0b367d8b4d677a822e718efd4
|
|
4
|
+
data.tar.gz: c23f8e67fb71a192331d63030d66d138dd9b1cdd136b264107b75456f762516b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f1144b81ef5f02bac43efe2790974f50e4cfcbbca924bea182602c2615419178f5f76376c14daee424e6c7dbbaee93152cf8969275ee61c5bf8891d1a8124b0
|
|
7
|
+
data.tar.gz: cea6a7fbf475ad7ae58cce4556ed1834d3a7aabb3bdf832fc82060b4e386e9ff0e9572dd32a4b759c51e66eedcb3478ed085e49c841958aa2fc20595ce9d55d2
|
data/README.md
CHANGED
|
@@ -23,7 +23,9 @@ Embed [V8](https://v8.dev/) in Ruby, built on [rusty_v8](https://crates.io/crate
|
|
|
23
23
|
- **Snapshots, realms (`Context`s), host callbacks, and a bytecode cache.**
|
|
24
24
|
- **Resource limits on both axes** — a `timeout_ms` (time) and a `memory_limit`
|
|
25
25
|
(space), each catchable: a runaway script fails just its own `eval`, leaving
|
|
26
|
-
the isolate usable, instead of aborting the process.
|
|
26
|
+
the isolate usable, instead of aborting the process. A heap runaway is caught
|
|
27
|
+
even with no explicit `memory_limit` — V8's default ceiling raises instead of
|
|
28
|
+
aborting.
|
|
27
29
|
- **Precompiled gems** bundle V8 for Linux/macOS × Ruby 3.3–4.0 — no V8 build,
|
|
28
30
|
no Rust toolchain.
|
|
29
31
|
|
|
@@ -106,6 +108,13 @@ iso.context.eval("a = []; for (;;) a.push(new Array(1e6))")
|
|
|
106
108
|
iso.context.eval("1 + 1") # => 2 (still usable)
|
|
107
109
|
```
|
|
108
110
|
|
|
111
|
+
Even without an explicit `memory_limit`, a heap runaway raises
|
|
112
|
+
`V8OutOfMemoryError` against V8's own default ceiling (~2 GB on 64-bit) rather
|
|
113
|
+
than aborting the process — pass `memory_limit:` for a tighter bound. (One
|
|
114
|
+
caveat: if the process's available memory — e.g. a container cgroup limit — sits
|
|
115
|
+
below the active ceiling, the OS may kill the process before V8's callback
|
|
116
|
+
fires; set an explicit `memory_limit` under that bound to keep it catchable.)
|
|
117
|
+
|
|
109
118
|
### Bytecode caching
|
|
110
119
|
|
|
111
120
|
V8 compiles lazily: the top level up front, each function body on first call.
|
data/ext/rusty_racer/Cargo.toml
CHANGED
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -190,25 +190,36 @@ enum VmError {
|
|
|
190
190
|
OutOfMemory, // memory_limit hit -> RustyRacer::V8OutOfMemoryError
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
// V8's near-heap-limit callback (registered
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
193
|
+
// V8's near-heap-limit callback (registered on EVERY isolate). V8 calls this,
|
|
194
|
+
// synchronously on the owner thread, when a GC still leaves the heap about to
|
|
195
|
+
// exceed the current ceiling — i.e. the script is running away on memory. That
|
|
196
|
+
// ceiling is the configured memory_limit when one was set, otherwise V8's own
|
|
197
|
+
// platform-derived default (the default-protection path), so a runaway raises a
|
|
198
|
+
// catchable error either way instead of aborting the process. `data` is the
|
|
199
|
+
// isolate ptr we registered (Core.iso_ptr). We flag the isolate and terminate the
|
|
200
|
+
// running JS so it unwinds with that catchable error. The return value becomes
|
|
201
|
+
// V8's new ceiling: hand it a DOUBLED limit so the unwind itself (and any pending
|
|
202
|
+
// finalizers) has room to allocate without tripping a hard OOM abort mid-unwind.
|
|
203
|
+
// Core::run, once the op has unwound, forces a GC to reclaim and resets the
|
|
204
|
+
// ceiling — see the OOM recovery there. The bump is a no-op-after-the-fact:
|
|
205
|
+
// doubling here, GC + reset after, so the limit keeps protecting later ops.
|
|
206
|
+
unsafe extern "C" fn near_heap_limit_cb(data: *mut c_void, current_heap_limit: usize, initial: usize) -> usize {
|
|
205
207
|
let isolate = unsafe { &mut *(data as *mut v8::Isolate) };
|
|
206
208
|
// get_slot_mut (not istate!): this runs as an extern "C" callback from V8's
|
|
207
209
|
// C++ allocator, where a panic would unwind across the FFI boundary. The slot
|
|
208
210
|
// is always present once an op can run (set in Isolate::new before any JS), but
|
|
209
211
|
// skip flagging rather than .expect()-panic in the impossible absent case.
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
+
// The closure releases the &mut borrow before terminate_execution (&self).
|
|
213
|
+
// Also record V8's `initial` ceiling so recovery can restore it when no
|
|
214
|
+
// explicit memory_limit was set (see oom_initial_limit).
|
|
215
|
+
let flagged = isolate
|
|
216
|
+
.get_slot_mut::<IsolateState>()
|
|
217
|
+
.map(|s| {
|
|
218
|
+
s.oom_fired = true;
|
|
219
|
+
s.oom_initial_limit = initial;
|
|
220
|
+
})
|
|
221
|
+
.is_some();
|
|
222
|
+
if flagged {
|
|
212
223
|
isolate.terminate_execution();
|
|
213
224
|
}
|
|
214
225
|
current_heap_limit.saturating_mul(2)
|
|
@@ -523,11 +534,6 @@ struct V8State {
|
|
|
523
534
|
promise_reject_handler: Option<(i32, v8::Global<v8::Function>)>,
|
|
524
535
|
}
|
|
525
536
|
|
|
526
|
-
// Context embedder-data slot holding the realm id (an Integer), stamped by
|
|
527
|
-
// new_realm so id_of_context is O(1). Slot 0 is the embedder's own first slot
|
|
528
|
-
// (the binding adds INTERNAL_SLOT_COUNT); nothing else here uses embedder data.
|
|
529
|
-
const REALM_ID_SLOT: i32 = 0;
|
|
530
|
-
|
|
531
537
|
// (STATE/MODULES/SCRIPTS/ACTIVE_REALMS/INSTANTIATING/WATCHDOG_FIRED/
|
|
532
538
|
// AUTO_MICROTASKS/DRAINING moved into IsolateState in the isolate slot, reached
|
|
533
539
|
// via istate!(scope). Their invariants are documented on IsolateState's fields.)
|
|
@@ -567,12 +573,18 @@ struct IsolateState {
|
|
|
567
573
|
instantiate_resolve: Option<RootedProc>,
|
|
568
574
|
instantiate_resolve_err: Option<BoxValue<Exception>>,
|
|
569
575
|
watchdog: Arc<WatchdogShared>,
|
|
570
|
-
// Set by near_heap_limit_cb when the
|
|
571
|
-
//
|
|
572
|
-
//
|
|
576
|
+
// Set by near_heap_limit_cb when the heap ceiling is hit: it terminates the
|
|
577
|
+
// running JS, and Core::run reads this after the op to relabel the terminate as
|
|
578
|
+
// OutOfMemory and recover the heap (GC + reset the ceiling).
|
|
573
579
|
// Plain bool (no atomic): the callback fires synchronously on the owner thread,
|
|
574
580
|
// never concurrently with the bracket that reads it.
|
|
575
581
|
oom_fired: bool,
|
|
582
|
+
// V8's original heap ceiling, captured from the callback's `initial` argument
|
|
583
|
+
// when it fires. Recovery resets the ceiling to memory_limit when one was set,
|
|
584
|
+
// but with no explicit limit (the default-protection path) the ceiling IS V8's
|
|
585
|
+
// platform-derived default, whose value we don't otherwise know — so we restore
|
|
586
|
+
// to this captured initial instead. 0 until the callback has fired at least once.
|
|
587
|
+
oom_initial_limit: usize,
|
|
576
588
|
}
|
|
577
589
|
|
|
578
590
|
impl IsolateState {
|
|
@@ -595,6 +607,7 @@ impl IsolateState {
|
|
|
595
607
|
instantiate_resolve_err: None,
|
|
596
608
|
watchdog: WatchdogShared::new(),
|
|
597
609
|
oom_fired: false,
|
|
610
|
+
oom_initial_limit: 0,
|
|
598
611
|
}
|
|
599
612
|
}
|
|
600
613
|
}
|
|
@@ -997,17 +1010,35 @@ fn finish_dynamic_import(
|
|
|
997
1010
|
}
|
|
998
1011
|
}
|
|
999
1012
|
|
|
1000
|
-
// The id of |context
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1013
|
+
// The id of |context| among this isolate's LIVE realms, or None when it isn't
|
|
1014
|
+
// one (e.g. a context that was reset or disposed away). Scans the realm table:
|
|
1015
|
+
// realm counts are small (a handful of frames) and this only runs off the hot
|
|
1016
|
+
// path (promise rejection, dynamic import, contextOf), so the scan is cheap.
|
|
1017
|
+
//
|
|
1018
|
+
// We deliberately do NOT stamp the id into the context's embedder data. Using a
|
|
1019
|
+
// context slot makes V8 attach a ContextAnnex carrying a guaranteed-finalizer
|
|
1020
|
+
// weak handle to every realm, and that finalizer has crashed the process at
|
|
1021
|
+
// isolate teardown (a first-pass weak callback re-fired during disposal,
|
|
1022
|
+
// panicking on an already-taken handle). Keeping realms free of context slots
|
|
1023
|
+
// keeps teardown clear of that hazard.
|
|
1004
1024
|
fn id_of_context(scope: &mut v8::PinScope<'_, '_>, context: v8::Local<v8::Context>) -> Option<i32> {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1025
|
+
// The main realm (id 0) is overwhelmingly the common case — check it first,
|
|
1026
|
+
// without cloning the whole table.
|
|
1027
|
+
let main = istate!(scope).realms.main_context.clone();
|
|
1028
|
+
if main.is_some_and(|g| v8::Local::new(scope, &g) == context) {
|
|
1029
|
+
return Some(0);
|
|
1030
|
+
}
|
|
1031
|
+
// Extra realms: snapshot the (cloned) Globals so no IsolateState borrow is
|
|
1032
|
+
// held while we mint Locals from `scope`.
|
|
1033
|
+
let extras: Vec<(i32, v8::Global<v8::Context>)> = istate!(scope)
|
|
1034
|
+
.realms
|
|
1035
|
+
.contexts
|
|
1036
|
+
.iter()
|
|
1037
|
+
.map(|(id, g)| (*id, g.clone()))
|
|
1038
|
+
.collect();
|
|
1039
|
+
extras
|
|
1040
|
+
.into_iter()
|
|
1041
|
+
.find_map(|(id, g)| (v8::Local::new(scope, &g) == context).then_some(id))
|
|
1011
1042
|
}
|
|
1012
1043
|
|
|
1013
1044
|
// Pick the Global context for a realm id: 0 = main, N = an extra realm (None
|
|
@@ -1085,10 +1116,12 @@ struct Core {
|
|
|
1085
1116
|
// Default per-eval/call timeout (ms); 0 = none. eval(timeout_ms:)'s explicit
|
|
1086
1117
|
// value overrides it. Guards against an in-V8 infinite loop without a watchdog.
|
|
1087
1118
|
default_timeout_ms: u64,
|
|
1088
|
-
// Per-isolate heap ceiling (bytes); 0 =
|
|
1089
|
-
// with this as V8's max heap
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1119
|
+
// Per-isolate heap ceiling (bytes); 0 = V8's default ceiling. When set, the
|
|
1120
|
+
// isolate is created with this as V8's max heap; near_heap_limit_cb is registered
|
|
1121
|
+
// either way (against this ceiling when set, V8's default otherwise), so a runaway
|
|
1122
|
+
// is always catchable rather than a process abort. Core::run's OOM recovery resets
|
|
1123
|
+
// the ceiling after each OOM (to this when set, else V8's captured default — see
|
|
1124
|
+
// oom_initial_limit). Space-axis twin of default_timeout_ms.
|
|
1092
1125
|
memory_limit: usize,
|
|
1093
1126
|
// Set by Context#dynamic_import_resolver=; called for a JS import() to map
|
|
1094
1127
|
// (specifier, referrer) to an already-loaded Module. GC-rooted like procs.
|
|
@@ -1343,18 +1376,11 @@ unsafe extern "C" fn promise_reject_cb(message: v8::PromiseRejectMessage) {
|
|
|
1343
1376
|
// Build a fresh v8::Context and install the host namespace (from STATE) into
|
|
1344
1377
|
// it — the single definition of "a realm of this isolate", shared by boot,
|
|
1345
1378
|
// reset and create_context so realms can't drift apart.
|
|
1346
|
-
fn new_realm(scope: &mut v8::PinScope<'_, '_, ()
|
|
1379
|
+
fn new_realm(scope: &mut v8::PinScope<'_, '_, ()>) -> v8::Global<v8::Context> {
|
|
1347
1380
|
let fresh = {
|
|
1348
1381
|
let context = v8::Context::new(scope, Default::default());
|
|
1349
1382
|
v8::Global::new(scope, context)
|
|
1350
1383
|
};
|
|
1351
|
-
// Stamp the realm id into the context so id_of_context is O(1) (it would
|
|
1352
|
-
// otherwise scan every realm on every promise rejection / contextOf call).
|
|
1353
|
-
{
|
|
1354
|
-
let context = v8::Local::new(scope, &fresh);
|
|
1355
|
-
let id_val: v8::Local<v8::Value> = v8::Integer::new(scope, id).into();
|
|
1356
|
-
context.set_embedder_data(REALM_ID_SLOT, id_val);
|
|
1357
|
-
}
|
|
1358
1384
|
// DESIGN DECISION: every realm of an isolate shares ONE security token, so
|
|
1359
1385
|
// they are all mutually same-origin — the model is "a group of same-origin
|
|
1360
1386
|
// frames sharing one heap", and NS.contextGlobal gives full cross-realm
|
|
@@ -1514,8 +1540,8 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
|
|
|
1514
1540
|
VmError::Parse(m) | VmError::Runtime(m) => m,
|
|
1515
1541
|
VmError::JsError { message, .. } => message,
|
|
1516
1542
|
VmError::Terminated => "snapshot code was terminated".to_string(),
|
|
1517
|
-
// Unreachable: the snapshot-creator
|
|
1518
|
-
//
|
|
1543
|
+
// Unreachable: the snapshot-creator is a separate isolate
|
|
1544
|
+
// that never registers near_heap_limit_cb.
|
|
1519
1545
|
VmError::OutOfMemory => "snapshot code ran out of memory".to_string(),
|
|
1520
1546
|
});
|
|
1521
1547
|
}
|
|
@@ -1565,6 +1591,8 @@ impl Isolate {
|
|
|
1565
1591
|
};
|
|
1566
1592
|
// Cap V8's heap at the configured limit so its near-heap-limit callback
|
|
1567
1593
|
// fires as the script approaches it (initial 0 = V8's default initial heap).
|
|
1594
|
+
// With no explicit limit the callback is still registered below, against
|
|
1595
|
+
// V8's own default ceiling, so a runaway raises instead of aborting.
|
|
1568
1596
|
if memory_limit > 0 {
|
|
1569
1597
|
create_params = create_params.heap_limits(0, memory_limit);
|
|
1570
1598
|
}
|
|
@@ -1598,7 +1626,7 @@ impl Isolate {
|
|
|
1598
1626
|
// namespace from the slot (seeded above).
|
|
1599
1627
|
{
|
|
1600
1628
|
v8::scope!(let scope, &mut isolate);
|
|
1601
|
-
let main_context = new_realm(scope
|
|
1629
|
+
let main_context = new_realm(scope);
|
|
1602
1630
|
istate!(scope).realms.main_context = Some(main_context);
|
|
1603
1631
|
}
|
|
1604
1632
|
// Box the OwnedIsolate so it has a STABLE address, then capture a raw ptr
|
|
@@ -1607,12 +1635,12 @@ impl Isolate {
|
|
|
1607
1635
|
// registry moves only the 8-byte pointer; the boxed OwnedIsolate stays put.
|
|
1608
1636
|
let mut boxed = Box::new(isolate);
|
|
1609
1637
|
let iso_ptr = IsoPtr(&mut **boxed as *mut v8::Isolate);
|
|
1610
|
-
// Arm the
|
|
1611
|
-
// this ptr (it reads the slot's oom_fired and terminates through
|
|
1612
|
-
// Core::run resets the ceiling through the same ptr on recovery.
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1638
|
+
// Arm the heap-limit callback now that iso_ptr is stable: the callback's
|
|
1639
|
+
// data IS this ptr (it reads the slot's oom_fired and terminates through
|
|
1640
|
+
// it), and Core::run resets the ceiling through the same ptr on recovery.
|
|
1641
|
+
// Registered unconditionally — with memory_limit it guards that ceiling,
|
|
1642
|
+
// without one it guards V8's default ceiling (catchable, not a process abort).
|
|
1643
|
+
boxed.add_near_heap_limit_callback(near_heap_limit_cb, iso_ptr.0 as *mut c_void);
|
|
1616
1644
|
let iso_id = NEXT_ISOLATE_ID.fetch_add(1, Ordering::SeqCst);
|
|
1617
1645
|
isolates().lock().unwrap().insert(iso_id, SendIso(boxed));
|
|
1618
1646
|
// Root the owner Thread VALUE so its address can't be reused while this
|
|
@@ -1735,19 +1763,26 @@ impl Core {
|
|
|
1735
1763
|
// OOM recovery. The near-heap-limit callback bumped the ceiling and
|
|
1736
1764
|
// terminated the op so it could unwind; the scope is closed and JS has
|
|
1737
1765
|
// stopped now. Reclaim the runaway allocation (a forced GC) and reset
|
|
1738
|
-
// the ceiling
|
|
1739
|
-
//
|
|
1740
|
-
//
|
|
1741
|
-
|
|
1742
|
-
if self.memory_limit > 0 && std::mem::take(&mut istate!(unsafe { &mut *iso }).oom_fired) {
|
|
1766
|
+
// the ceiling so the limit keeps protecting later ops, then relabel the
|
|
1767
|
+
// terminate as OutOfMemory. watchdog_fired stays false for an OOM, so
|
|
1768
|
+
// the request's end-sweep left the terminate flag set — cancel it here.
|
|
1769
|
+
if std::mem::take(&mut istate!(unsafe { &mut *iso }).oom_fired) {
|
|
1743
1770
|
let iso_ref = unsafe { &mut *iso };
|
|
1771
|
+
// The ceiling to restore: the configured memory_limit when one was
|
|
1772
|
+
// set, otherwise V8's default ceiling, which the callback captured
|
|
1773
|
+
// in oom_initial_limit (we don't otherwise know its value).
|
|
1774
|
+
let restore_to = if self.memory_limit > 0 {
|
|
1775
|
+
self.memory_limit
|
|
1776
|
+
} else {
|
|
1777
|
+
istate!(iso_ref).oom_initial_limit
|
|
1778
|
+
};
|
|
1744
1779
|
// Reclaim the runaway allocation, then reset the ceiling from the
|
|
1745
|
-
// doubled bump back to
|
|
1780
|
+
// doubled bump back to restore_to (V8 clamps it no lower than the
|
|
1746
1781
|
// live heap — a genuinely-retained set above the limit necessarily
|
|
1747
1782
|
// loosens it, inherent to recovering the isolate rather than
|
|
1748
1783
|
// discarding it), and re-arm the callback for the next op.
|
|
1749
1784
|
iso_ref.low_memory_notification();
|
|
1750
|
-
iso_ref.remove_near_heap_limit_callback(near_heap_limit_cb,
|
|
1785
|
+
iso_ref.remove_near_heap_limit_callback(near_heap_limit_cb, restore_to);
|
|
1751
1786
|
iso_ref.add_near_heap_limit_callback(near_heap_limit_cb, iso as *mut c_void);
|
|
1752
1787
|
// Clear the terminate the OOM set (the request end-sweep skips it —
|
|
1753
1788
|
// that only sweeps watchdog_fired). Do this AFTER the GC: the forced
|
|
@@ -2173,9 +2208,8 @@ impl Core {
|
|
|
2173
2208
|
// runs V8's teardown GC, which could otherwise re-invoke near_heap_limit_cb
|
|
2174
2209
|
// (touching the just-reset slot of an isolate being destroyed). The watchdog
|
|
2175
2210
|
// is already stopped above; this closes the matching space-axis hole.
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
}
|
|
2211
|
+
// Registered unconditionally (default protection), so always remove it.
|
|
2212
|
+
unsafe { &mut *self.iso_ptr.0 }.remove_near_heap_limit_callback(near_heap_limit_cb, 0);
|
|
2179
2213
|
// Remove (and drop) the OwnedIsolate — V8 disposal runs here — AFTER the
|
|
2180
2214
|
// watchdog joined and the Globals were cleared, while the isolate is
|
|
2181
2215
|
// entered (above). Drop outside the lock so V8 teardown can't deadlock on
|
data/ext/rusty_racer/src/ops.rs
CHANGED
|
@@ -574,7 +574,7 @@ fn op_reset(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32) -> VmReply {
|
|
|
574
574
|
.into(),
|
|
575
575
|
)))
|
|
576
576
|
} else {
|
|
577
|
-
let fresh = new_realm(scope
|
|
577
|
+
let fresh = new_realm(scope);
|
|
578
578
|
{
|
|
579
579
|
let realms = &mut istate!(scope).realms;
|
|
580
580
|
if context_id == 0 {
|
|
@@ -596,7 +596,7 @@ fn op_create_context(scope: &mut v8::PinScope<'_, '_, ()>) -> VmReply {
|
|
|
596
596
|
realms.next_context_id += 1;
|
|
597
597
|
id
|
|
598
598
|
};
|
|
599
|
-
let fresh = new_realm(scope
|
|
599
|
+
let fresh = new_realm(scope);
|
|
600
600
|
istate!(scope).realms.contexts.insert(id, fresh);
|
|
601
601
|
VmReply::Done(Ok(JsVal::Int(id as i64)))
|
|
602
602
|
}
|
data/lib/rusty_racer/version.rb
CHANGED
data/lib/rusty_racer.rb
CHANGED
|
@@ -27,8 +27,9 @@ module RustyRacer
|
|
|
27
27
|
class ParseError < EvalError; end
|
|
28
28
|
class RuntimeError < EvalError; end
|
|
29
29
|
class ScriptTerminatedError < EvalError; end
|
|
30
|
-
# Raised when JS allocation exceeds the isolate's
|
|
31
|
-
#
|
|
30
|
+
# Raised when JS allocation exceeds the isolate's heap ceiling (the configured
|
|
31
|
+
# memory_limit, or V8's default ceiling when none was set). Catchable like any
|
|
32
|
+
# eval error — a runaway script fails its own eval instead of aborting the
|
|
32
33
|
# process. The space-axis twin of ScriptTerminatedError (the time axis).
|
|
33
34
|
class V8OutOfMemoryError < EvalError; end
|
|
34
35
|
class SnapshotError < Error; end
|
|
@@ -45,12 +46,19 @@ module RustyRacer
|
|
|
45
46
|
# Keyword-arg constructor over the positional Rust primitive. A snapshot
|
|
46
47
|
# (RustyRacer::Snapshot) boots the isolate with its baked-in state;
|
|
47
48
|
# timeout_ms caps each eval/call (0 = no limit) against in-V8 infinite
|
|
48
|
-
# loops. memory_limit caps the V8 heap in bytes
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
49
|
+
# loops. memory_limit caps the V8 heap in bytes: a script that exceeds the
|
|
50
|
+
# ceiling is terminated and raises V8OutOfMemoryError rather than aborting
|
|
51
|
+
# the process, and the isolate stays usable afterward. 0 (the default) does
|
|
52
|
+
# NOT disable this — it leaves V8's own platform-derived default ceiling
|
|
53
|
+
# (typically ~2 GB on 64-bit) in place, so a runaway is still catchable
|
|
54
|
+
# instead of a fatal abort; pass a smaller value for a tighter bound. It is
|
|
55
|
+
# a soft limit — V8 enforces it at GC granularity, so usage may briefly
|
|
56
|
+
# overshoot, and an explicit limit must comfortably exceed the isolate's
|
|
57
|
+
# baseline (and any snapshot's baked-in heap), since it is only armed once
|
|
58
|
+
# the isolate has booted. Caveat: if the process's available memory (e.g. a
|
|
59
|
+
# container cgroup limit) is below the active ceiling, the OS may kill the
|
|
60
|
+
# process before V8's callback fires — set an explicit memory_limit under
|
|
61
|
+
# that bound to keep the error catchable.
|
|
54
62
|
# microtasks mirrors V8's kAuto/kExplicit: :auto (default) drains
|
|
55
63
|
# the microtask queue when the outermost eval/call/run/evaluate completes
|
|
56
64
|
# (the standard embedder contract); :explicit drains only on
|