rusty_racer 0.1.2 → 0.1.3
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 +26 -3
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +114 -4
- data/ext/rusty_racer/src/watchdog.rs +10 -0
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +13 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 746bc2e7e881d601b38f6052fb45f4f87d72d4580a46ea738e2c716af6641f82
|
|
4
|
+
data.tar.gz: 4fcc456589eabd593e506d7c941b7fe95635400225aac9807908a008f63e73ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20aeb8c39cc3f321b5015b78ff2c4ae54553babd54735bbb256aeaea16c2428cccc03141a55c179ebad34822243d2d39508517df33f34d6236a7a368cbe274b5
|
|
7
|
+
data.tar.gz: 5ff495f94287dfd8464855d5e20c84c4743b91a3d320760decf17244dad5efa67c503b74dd2994792059e0f7236b10992157bfd5c4cb5681b02fe162b47a8a4d
|
data/README.md
CHANGED
|
@@ -21,6 +21,9 @@ Embed [V8](https://v8.dev/) in Ruby, built on [rusty_v8](https://crates.io/crate
|
|
|
21
21
|
- **Drop-in [ExecJS](#execjs) runtime** — any ExecJS consumer switches with no
|
|
22
22
|
code change.
|
|
23
23
|
- **Snapshots, realms (`Context`s), host callbacks, and a bytecode cache.**
|
|
24
|
+
- **Resource limits on both axes** — a `timeout_ms` (time) and a `memory_limit`
|
|
25
|
+
(space), each catchable: a runaway script fails just its own `eval`, leaving
|
|
26
|
+
the isolate usable, instead of aborting the process.
|
|
24
27
|
- **Precompiled gems** bundle V8 for Linux/macOS × Ruby 3.3–4.0 — no V8 build,
|
|
25
28
|
no Rust toolchain.
|
|
26
29
|
|
|
@@ -33,9 +36,10 @@ dynamic import** (mini_racer is eval/classic-script oriented); **richer
|
|
|
33
36
|
marshalling** (the types above round-trip natively instead of through a
|
|
34
37
|
JSON-shaped projection); and **in-thread execution** with no per-op thread hop,
|
|
35
38
|
which is faster for overhead-dominated workloads (lots of tiny `eval`/`call`) and
|
|
36
|
-
at parity once the per-op JS work dominates.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
at parity once the per-op JS work dominates. Both axes of resource limiting are
|
|
40
|
+
covered — a `timeout_ms` (time) and a `memory_limit` (space), each catchable. It
|
|
41
|
+
is also younger and **experimental** — fewer miles, no Windows yet. Parity with
|
|
42
|
+
mini_racer is not a goal; the overlap is convergent evolution, not a port.
|
|
39
43
|
|
|
40
44
|
## What it can do
|
|
41
45
|
|
|
@@ -83,6 +87,25 @@ app.namespace["r"] # => 42
|
|
|
83
87
|
|
|
84
88
|
Classic `<script>`s work the same way: `ctx.compile("1 + 1").run` # => 2.
|
|
85
89
|
|
|
90
|
+
### Resource limits
|
|
91
|
+
|
|
92
|
+
An isolate can cap untrusted code on both axes. Each limit terminates the
|
|
93
|
+
offending op and raises a catchable error — the isolate stays usable afterward,
|
|
94
|
+
so one runaway script fails just its own `eval`, not the whole process.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# Time: timeout_ms caps each eval/call (per-call override on Context#eval).
|
|
98
|
+
iso = RustyRacer::Isolate.new(timeout_ms: 1000)
|
|
99
|
+
iso.context.eval("for (;;) {}") # raises RustyRacer::ScriptTerminatedError
|
|
100
|
+
|
|
101
|
+
# Space: memory_limit caps the V8 heap in bytes (a soft limit, enforced at GC
|
|
102
|
+
# granularity). The isolate forces a GC and resets the ceiling on recovery.
|
|
103
|
+
iso = RustyRacer::Isolate.new(memory_limit: 64 * 1024 * 1024)
|
|
104
|
+
iso.context.eval("a = []; for (;;) a.push(new Array(1e6))")
|
|
105
|
+
# raises RustyRacer::V8OutOfMemoryError
|
|
106
|
+
iso.context.eval("1 + 1") # => 2 (still usable)
|
|
107
|
+
```
|
|
108
|
+
|
|
86
109
|
### Bytecode caching
|
|
87
110
|
|
|
88
111
|
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
|
@@ -186,7 +186,52 @@ enum VmError {
|
|
|
186
186
|
message: String,
|
|
187
187
|
backtrace: Vec<String>,
|
|
188
188
|
},
|
|
189
|
-
Terminated,
|
|
189
|
+
Terminated, // watchdog/stop -> RustyRacer::ScriptTerminatedError
|
|
190
|
+
OutOfMemory, // memory_limit hit -> RustyRacer::V8OutOfMemoryError
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// V8's near-heap-limit callback (registered per isolate when memory_limit > 0).
|
|
194
|
+
// V8 calls this, synchronously on the owner thread, when a GC still leaves the
|
|
195
|
+
// heap about to exceed the configured ceiling — i.e. the script is running away
|
|
196
|
+
// on memory. `data` is the isolate ptr we registered (Core.iso_ptr). We flag the
|
|
197
|
+
// isolate and terminate the running JS so it unwinds with a catchable error
|
|
198
|
+
// rather than V8 aborting the process. The return value becomes V8's new ceiling:
|
|
199
|
+
// hand it a DOUBLED limit so the unwind itself (and any pending finalizers) has
|
|
200
|
+
// room to allocate without tripping a hard OOM abort mid-unwind. Core::run, once
|
|
201
|
+
// the op has unwound, forces a GC to reclaim and resets the ceiling — see the OOM
|
|
202
|
+
// recovery there. The bump is a no-op-after-the-fact: doubling here, GC + reset
|
|
203
|
+
// after, so the limit keeps protecting later ops.
|
|
204
|
+
unsafe extern "C" fn near_heap_limit_cb(data: *mut c_void, current_heap_limit: usize, _initial: usize) -> usize {
|
|
205
|
+
let isolate = unsafe { &mut *(data as *mut v8::Isolate) };
|
|
206
|
+
// get_slot_mut (not istate!): this runs as an extern "C" callback from V8's
|
|
207
|
+
// C++ allocator, where a panic would unwind across the FFI boundary. The slot
|
|
208
|
+
// is always present once an op can run (set in Isolate::new before any JS), but
|
|
209
|
+
// skip flagging rather than .expect()-panic in the impossible absent case.
|
|
210
|
+
// Setting the flag releases the &mut borrow before terminate_execution (&self).
|
|
211
|
+
if isolate.get_slot_mut::<IsolateState>().map(|s| s.oom_fired = true).is_some() {
|
|
212
|
+
isolate.terminate_execution();
|
|
213
|
+
}
|
|
214
|
+
current_heap_limit.saturating_mul(2)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// After an OOM the running op's outcome is a bare Terminated (the terminate the
|
|
218
|
+
// callback fired). Swap it for OutOfMemory so it surfaces as V8OutOfMemoryError,
|
|
219
|
+
// preserving the reply's variant (the caller dispatches on it). Only the error
|
|
220
|
+
// arm changes; a Terminated from a real timeout/stop never reaches here because
|
|
221
|
+
// this runs only when oom_fired was set.
|
|
222
|
+
fn relabel_oom(reply: VmReply) -> VmReply {
|
|
223
|
+
fn fix<T>(r: Result<T, VmError>) -> Result<T, VmError> {
|
|
224
|
+
match r {
|
|
225
|
+
Err(VmError::Terminated) => Err(VmError::OutOfMemory),
|
|
226
|
+
other => other,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
match reply {
|
|
230
|
+
VmReply::Done(r) => VmReply::Done(fix(r)),
|
|
231
|
+
VmReply::ModuleCompiled(r) => VmReply::ModuleCompiled(fix(r)),
|
|
232
|
+
VmReply::ScriptCompiled(r) => VmReply::ScriptCompiled(fix(r)),
|
|
233
|
+
VmReply::CodeCache(r) => VmReply::CodeCache(fix(r)),
|
|
234
|
+
}
|
|
190
235
|
}
|
|
191
236
|
|
|
192
237
|
// ---------------------------------------------------------------------------
|
|
@@ -522,6 +567,12 @@ struct IsolateState {
|
|
|
522
567
|
instantiate_resolve: Option<RootedProc>,
|
|
523
568
|
instantiate_resolve_err: Option<BoxValue<Exception>>,
|
|
524
569
|
watchdog: Arc<WatchdogShared>,
|
|
570
|
+
// Set by near_heap_limit_cb when the configured memory_limit is hit: it
|
|
571
|
+
// terminates the running JS, and Core::run reads this after the op to relabel
|
|
572
|
+
// the terminate as OutOfMemory and recover the heap (GC + reset the ceiling).
|
|
573
|
+
// Plain bool (no atomic): the callback fires synchronously on the owner thread,
|
|
574
|
+
// never concurrently with the bracket that reads it.
|
|
575
|
+
oom_fired: bool,
|
|
525
576
|
}
|
|
526
577
|
|
|
527
578
|
impl IsolateState {
|
|
@@ -543,6 +594,7 @@ impl IsolateState {
|
|
|
543
594
|
instantiate_resolve: None,
|
|
544
595
|
instantiate_resolve_err: None,
|
|
545
596
|
watchdog: WatchdogShared::new(),
|
|
597
|
+
oom_fired: false,
|
|
546
598
|
}
|
|
547
599
|
}
|
|
548
600
|
}
|
|
@@ -1033,6 +1085,11 @@ struct Core {
|
|
|
1033
1085
|
// Default per-eval/call timeout (ms); 0 = none. eval(timeout_ms:)'s explicit
|
|
1034
1086
|
// value overrides it. Guards against an in-V8 infinite loop without a watchdog.
|
|
1035
1087
|
default_timeout_ms: u64,
|
|
1088
|
+
// Per-isolate heap ceiling (bytes); 0 = none. When set, the isolate is created
|
|
1089
|
+
// with this as V8's max heap and near_heap_limit_cb is registered; Core::run's
|
|
1090
|
+
// OOM recovery resets the ceiling back to this after each OOM. Space-axis twin
|
|
1091
|
+
// of default_timeout_ms.
|
|
1092
|
+
memory_limit: usize,
|
|
1036
1093
|
// Set by Context#dynamic_import_resolver=; called for a JS import() to map
|
|
1037
1094
|
// (specifier, referrer) to an already-loaded Module. GC-rooted like procs.
|
|
1038
1095
|
dynamic_import_resolver: Mutex<Option<RootedProc>>,
|
|
@@ -1457,6 +1514,9 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
|
|
|
1457
1514
|
VmError::Parse(m) | VmError::Runtime(m) => m,
|
|
1458
1515
|
VmError::JsError { message, .. } => message,
|
|
1459
1516
|
VmError::Terminated => "snapshot code was terminated".to_string(),
|
|
1517
|
+
// Unreachable: the snapshot-creator isolate carries no
|
|
1518
|
+
// memory_limit, so near_heap_limit_cb is never registered.
|
|
1519
|
+
VmError::OutOfMemory => "snapshot code ran out of memory".to_string(),
|
|
1460
1520
|
});
|
|
1461
1521
|
}
|
|
1462
1522
|
}
|
|
@@ -1492,16 +1552,22 @@ impl Isolate {
|
|
|
1492
1552
|
host_namespace: Option<String>,
|
|
1493
1553
|
snapshot: Option<magnus::typed_data::Obj<Snapshot>>,
|
|
1494
1554
|
timeout_ms: u64,
|
|
1555
|
+
memory_limit: usize,
|
|
1495
1556
|
explicit_microtasks: bool,
|
|
1496
1557
|
) -> Result<Self, Error> {
|
|
1497
1558
|
init_v8();
|
|
1498
1559
|
// A snapshot blob bakes globalThis state in: the first Context::new (in
|
|
1499
1560
|
// new_realm below) deserializes that default context for free.
|
|
1500
1561
|
let snapshot_bytes = snapshot.map(|s| s.blob.borrow().clone());
|
|
1501
|
-
let create_params = match snapshot_bytes {
|
|
1562
|
+
let mut create_params = match snapshot_bytes {
|
|
1502
1563
|
Some(bytes) => v8::CreateParams::default().snapshot_blob(v8::StartupData::from(bytes)),
|
|
1503
1564
|
None => Default::default(),
|
|
1504
1565
|
};
|
|
1566
|
+
// Cap V8's heap at the configured limit so its near-heap-limit callback
|
|
1567
|
+
// fires as the script approaches it (initial 0 = V8's default initial heap).
|
|
1568
|
+
if memory_limit > 0 {
|
|
1569
|
+
create_params = create_params.heap_limits(0, memory_limit);
|
|
1570
|
+
}
|
|
1505
1571
|
let mut isolate = v8::Isolate::new(create_params);
|
|
1506
1572
|
// Always Explicit at the V8 level; the binding performs the kAuto
|
|
1507
1573
|
// end-of-script drain itself (auto_drain) so it stays inside the
|
|
@@ -1541,6 +1607,12 @@ impl Isolate {
|
|
|
1541
1607
|
// registry moves only the 8-byte pointer; the boxed OwnedIsolate stays put.
|
|
1542
1608
|
let mut boxed = Box::new(isolate);
|
|
1543
1609
|
let iso_ptr = IsoPtr(&mut **boxed as *mut v8::Isolate);
|
|
1610
|
+
// Arm the memory limit now that iso_ptr is stable: the callback's data IS
|
|
1611
|
+
// this ptr (it reads the slot's oom_fired and terminates through it), and
|
|
1612
|
+
// Core::run resets the ceiling through the same ptr on recovery.
|
|
1613
|
+
if memory_limit > 0 {
|
|
1614
|
+
boxed.add_near_heap_limit_callback(near_heap_limit_cb, iso_ptr.0 as *mut c_void);
|
|
1615
|
+
}
|
|
1544
1616
|
let iso_id = NEXT_ISOLATE_ID.fetch_add(1, Ordering::SeqCst);
|
|
1545
1617
|
isolates().lock().unwrap().insert(iso_id, SendIso(boxed));
|
|
1546
1618
|
// Root the owner Thread VALUE so its address can't be reused while this
|
|
@@ -1559,6 +1631,7 @@ impl Isolate {
|
|
|
1559
1631
|
depth: std::sync::atomic::AtomicU32::new(0),
|
|
1560
1632
|
procs: Mutex::new(ProcTable::default()),
|
|
1561
1633
|
default_timeout_ms: timeout_ms,
|
|
1634
|
+
memory_limit,
|
|
1562
1635
|
dynamic_import_resolver: Mutex::new(None),
|
|
1563
1636
|
watchdog,
|
|
1564
1637
|
watchdog_join: Mutex::new(Some(watchdog_join)),
|
|
@@ -1654,11 +1727,37 @@ impl Core {
|
|
|
1654
1727
|
self.scan_start_field.load(Ordering::Relaxed),
|
|
1655
1728
|
stack_top,
|
|
1656
1729
|
);
|
|
1657
|
-
let reply = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
|
1730
|
+
let mut reply = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
|
1658
1731
|
v8::scope!(let scope, unsafe { &mut *iso });
|
|
1659
1732
|
service_request(scope, request, true)
|
|
1660
1733
|
}))
|
|
1661
1734
|
.ok();
|
|
1735
|
+
// OOM recovery. The near-heap-limit callback bumped the ceiling and
|
|
1736
|
+
// terminated the op so it could unwind; the scope is closed and JS has
|
|
1737
|
+
// stopped now. Reclaim the runaway allocation (a forced GC) and reset
|
|
1738
|
+
// the ceiling back to memory_limit so the limit keeps protecting later
|
|
1739
|
+
// ops, then relabel the terminate as OutOfMemory. watchdog_fired stays
|
|
1740
|
+
// false for an OOM, so the request's end-sweep left the terminate flag
|
|
1741
|
+
// set — cancel it here.
|
|
1742
|
+
if self.memory_limit > 0 && std::mem::take(&mut istate!(unsafe { &mut *iso }).oom_fired) {
|
|
1743
|
+
let iso_ref = unsafe { &mut *iso };
|
|
1744
|
+
// Reclaim the runaway allocation, then reset the ceiling from the
|
|
1745
|
+
// doubled bump back to memory_limit (V8 clamps it no lower than the
|
|
1746
|
+
// live heap — a genuinely-retained set above the limit necessarily
|
|
1747
|
+
// loosens it, inherent to recovering the isolate rather than
|
|
1748
|
+
// discarding it), and re-arm the callback for the next op.
|
|
1749
|
+
iso_ref.low_memory_notification();
|
|
1750
|
+
iso_ref.remove_near_heap_limit_callback(near_heap_limit_cb, self.memory_limit);
|
|
1751
|
+
iso_ref.add_near_heap_limit_callback(near_heap_limit_cb, iso as *mut c_void);
|
|
1752
|
+
// Clear the terminate the OOM set (the request end-sweep skips it —
|
|
1753
|
+
// that only sweeps watchdog_fired). Do this AFTER the GC: the forced
|
|
1754
|
+
// GC above runs with the callback still armed, so a still-huge live
|
|
1755
|
+
// set can re-fire it mid-GC, re-setting both terminate and oom_fired;
|
|
1756
|
+
// clearing both here keeps either from leaking into the next op.
|
|
1757
|
+
iso_ref.cancel_terminate_execution();
|
|
1758
|
+
istate!(iso_ref).oom_fired = false;
|
|
1759
|
+
reply = reply.map(relabel_oom);
|
|
1760
|
+
}
|
|
1662
1761
|
unsafe { (*iso).exit() };
|
|
1663
1762
|
reply
|
|
1664
1763
|
} else {
|
|
@@ -2070,6 +2169,13 @@ impl Core {
|
|
|
2070
2169
|
st.scripts = ScriptReg::default();
|
|
2071
2170
|
st.instantiate_resolve = None;
|
|
2072
2171
|
}
|
|
2172
|
+
// Unregister the near-heap-limit callback before disposal: dropping the box
|
|
2173
|
+
// runs V8's teardown GC, which could otherwise re-invoke near_heap_limit_cb
|
|
2174
|
+
// (touching the just-reset slot of an isolate being destroyed). The watchdog
|
|
2175
|
+
// is already stopped above; this closes the matching space-axis hole.
|
|
2176
|
+
if self.memory_limit > 0 {
|
|
2177
|
+
unsafe { &mut *self.iso_ptr.0 }.remove_near_heap_limit_callback(near_heap_limit_cb, 0);
|
|
2178
|
+
}
|
|
2073
2179
|
// Remove (and drop) the OwnedIsolate — V8 disposal runs here — AFTER the
|
|
2074
2180
|
// watchdog joined and the Globals were cleared, while the isolate is
|
|
2075
2181
|
// entered (above). Drop outside the lock so V8 teardown can't deadlock on
|
|
@@ -2524,6 +2630,10 @@ fn vm_err(ruby: &Ruby, e: VmError) -> Error {
|
|
|
2524
2630
|
err_class(ruby, "ScriptTerminatedError"),
|
|
2525
2631
|
"JavaScript was terminated (timeout or stop)",
|
|
2526
2632
|
),
|
|
2633
|
+
VmError::OutOfMemory => Error::new(
|
|
2634
|
+
err_class(ruby, "V8OutOfMemoryError"),
|
|
2635
|
+
"JavaScript exceeded the isolate memory_limit",
|
|
2636
|
+
),
|
|
2527
2637
|
}
|
|
2528
2638
|
}
|
|
2529
2639
|
|
|
@@ -2601,7 +2711,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
2601
2711
|
// The isolate (VM) + its isolate-level ops; hands out Contexts.
|
|
2602
2712
|
let isolate = module.define_class("Isolate", ruby.class_object())?;
|
|
2603
2713
|
// keyword-arg wrapper Isolate.new(snapshot:, ...) lives in lib/rusty_racer.rb
|
|
2604
|
-
isolate.define_singleton_method("_new", function!(Isolate::new,
|
|
2714
|
+
isolate.define_singleton_method("_new", function!(Isolate::new, 5))?;
|
|
2605
2715
|
isolate.define_method("context", method!(Isolate::context, 0))?;
|
|
2606
2716
|
isolate.define_method("create_context", method!(Isolate::create_context, 0))?;
|
|
2607
2717
|
isolate.define_method("terminate", method!(Isolate::terminate, 0))?;
|
|
@@ -185,6 +185,7 @@ pub(crate) fn run_js_bracketed(
|
|
|
185
185
|
&& ran_js
|
|
186
186
|
&& !fired
|
|
187
187
|
&& matches!(outcome, Err(VmError::Terminated))
|
|
188
|
+
&& !istate!(scope).oom_fired
|
|
188
189
|
{
|
|
189
190
|
report_watchdog_anomaly(scope, label, watchdog, timeout_ms, started.elapsed());
|
|
190
191
|
}
|
|
@@ -193,6 +194,15 @@ pub(crate) fn run_js_bracketed(
|
|
|
193
194
|
if ran_js {
|
|
194
195
|
outcome = Err(VmError::Terminated);
|
|
195
196
|
}
|
|
197
|
+
} else if ran_js && istate!(scope).oom_fired {
|
|
198
|
+
// The memory_limit callback fired TerminateExecution during this op. body
|
|
199
|
+
// may not have noticed it — a microtask/TLA drain that was interrupted
|
|
200
|
+
// leaves a pending promise and returns Ok (auto_drain just stops at the
|
|
201
|
+
// terminate) — so force the terminated outcome rather than let an OOM
|
|
202
|
+
// surface as a bogus success. Core::run relabels it to OutOfMemory and
|
|
203
|
+
// recovers the heap. (No watchdog_fired here: the OOM terminate is swept by
|
|
204
|
+
// Core::run's recovery, not the request end-sweep.)
|
|
205
|
+
outcome = Err(VmError::Terminated);
|
|
196
206
|
}
|
|
197
207
|
outcome
|
|
198
208
|
}
|
data/lib/rusty_racer/version.rb
CHANGED
data/lib/rusty_racer.rb
CHANGED
|
@@ -27,6 +27,10 @@ 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 memory_limit. Catchable like
|
|
31
|
+
# any eval error — a runaway script fails its own eval instead of aborting the
|
|
32
|
+
# process. The space-axis twin of ScriptTerminatedError (the time axis).
|
|
33
|
+
class V8OutOfMemoryError < EvalError; end
|
|
30
34
|
class SnapshotError < Error; end
|
|
31
35
|
class PlatformAlreadyInitialized < Error; end
|
|
32
36
|
|
|
@@ -41,16 +45,22 @@ module RustyRacer
|
|
|
41
45
|
# Keyword-arg constructor over the positional Rust primitive. A snapshot
|
|
42
46
|
# (RustyRacer::Snapshot) boots the isolate with its baked-in state;
|
|
43
47
|
# timeout_ms caps each eval/call (0 = no limit) against in-V8 infinite
|
|
44
|
-
# loops.
|
|
48
|
+
# loops. memory_limit caps the V8 heap in bytes (0 = no limit): a script
|
|
49
|
+
# that exceeds it is terminated and raises V8OutOfMemoryError rather than
|
|
50
|
+
# aborting the process, and the isolate stays usable afterward. It is a soft
|
|
51
|
+
# limit — V8 enforces it at GC granularity, so usage may briefly overshoot,
|
|
52
|
+
# and it must comfortably exceed the isolate's baseline (and any snapshot's
|
|
53
|
+
# baked-in heap), since the limit is only armed once the isolate has booted.
|
|
54
|
+
# microtasks mirrors V8's kAuto/kExplicit: :auto (default) drains
|
|
45
55
|
# the microtask queue when the outermost eval/call/run/evaluate completes
|
|
46
56
|
# (the standard embedder contract); :explicit drains only on
|
|
47
57
|
# #perform_microtask_checkpoint.
|
|
48
|
-
def self.new(host_namespace: nil, snapshot: nil, timeout_ms: 0, microtasks: :auto)
|
|
58
|
+
def self.new(host_namespace: nil, snapshot: nil, timeout_ms: 0, memory_limit: 0, microtasks: :auto)
|
|
49
59
|
unless %i[auto explicit].include?(microtasks)
|
|
50
60
|
raise ArgumentError, "microtasks must be :auto or :explicit, got #{microtasks.inspect}"
|
|
51
61
|
end
|
|
52
62
|
|
|
53
|
-
_new(host_namespace, snapshot, timeout_ms, microtasks == :explicit)
|
|
63
|
+
_new(host_namespace, snapshot, timeout_ms, memory_limit, microtasks == :explicit)
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
# ->(specifier, referrer_url, context) { Module } for JS import(). |context|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rusty_racer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keita Urashima
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|