rusty_racer 0.1.14 → 0.1.16
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/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +281 -144
- data/ext/rusty_racer/src/marshal.rs +155 -124
- data/ext/rusty_racer/src/ops.rs +410 -272
- data/ext/rusty_racer/src/stack.rs +238 -50
- data/ext/rusty_racer/src/watchdog.rs +8 -2
- data/lib/rusty_racer/version.rb +1 -1
- metadata +1 -1
|
@@ -151,40 +151,141 @@ fn native_stack_bounds() -> (usize, usize) {
|
|
|
151
151
|
(0, 0)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Lower bound (and upper, for
|
|
155
|
-
// — i.e. the BOTTOM of the stack `addr` is on. Used for a Ruby Fiber,
|
|
156
|
-
// mmap'd stack pthread can't see: V8's limit must sit ABOVE this bottom or
|
|
157
|
-
// deep fiber recursion overflows the real stack and SEGVs the unmapped guard.
|
|
158
|
-
//
|
|
159
|
-
//
|
|
154
|
+
// Lower bound (and upper, for the cache test) of the memory region containing
|
|
155
|
+
// `addr` — i.e. the BOTTOM of the stack `addr` is on. Used for a Ruby Fiber,
|
|
156
|
+
// whose mmap'd stack pthread can't see: V8's limit must sit ABOVE this bottom or
|
|
157
|
+
// a deep fiber recursion overflows the real stack and SEGVs the unmapped guard.
|
|
158
|
+
// (0, 0) if unknown.
|
|
159
|
+
//
|
|
160
|
+
// Cached per native thread, several regions at a time, because a miss asks the
|
|
161
|
+
// OS for the whole memory map and costs far more than the op it serves: with ONE
|
|
162
|
+
// slot
|
|
163
|
+
// a workload that alternates between fibers missed on every single op, and an
|
|
164
|
+
// eval went from 1.2us to 31us as soon as two fibers took turns (measured on a
|
|
165
|
+
// 212-line maps; a Rails process has many times that). Ruby pools fiber stacks,
|
|
166
|
+
// so the live set is small and stable — one Enumerator per Capybara result, say
|
|
167
|
+
// — and a handful of slots turns that back into hits.
|
|
168
|
+
//
|
|
169
|
+
// Eviction is RANDOM, not round-robin (or LRU, which behaves the same here):
|
|
170
|
+
// ops cycling through more fibers than there are slots are exactly the pattern
|
|
171
|
+
// those policies handle worst, since each eviction takes the entry needed next
|
|
172
|
+
// and the hit rate collapses to zero — one fiber over the size measured as slow
|
|
173
|
+
// as having no cache at all. Evicting at random keeps roughly
|
|
174
|
+
// slots/live-fibers of the accesses, so going over the size degrades gradually
|
|
175
|
+
// instead of falling off a cliff. Empty slots are filled before anything is
|
|
176
|
+
// evicted. Entries are never invalidated: a dead fiber's keeps its slot until
|
|
177
|
+
// some miss draws it, and with a live set that fits there are no misses at all
|
|
178
|
+
// — which is why a hit has to be self-justifying rather than merely plausible.
|
|
179
|
+
//
|
|
180
|
+
// So the mapping's top is never used, or even kept, as an upper bound. A fiber
|
|
181
|
+
// stack's mapping can extend far above the stack itself, where the kernel merged
|
|
182
|
+
// it with an adjacent anonymous mapping of the same protection that has nothing
|
|
183
|
+
// to do with the fiber pool (measured: a 644KB stack inside an 8840KB mapping,
|
|
184
|
+
// with an unrelated 400MB mapping directly above). An entry that answered on the
|
|
185
|
+
// strength of that top would let a later allocation in the neighbour's range
|
|
186
|
+
// inherit a bottom megabytes below its own, and V8 would be told it has that
|
|
187
|
+
// much headroom on a 644KB stack.
|
|
188
|
+
//
|
|
189
|
+
// Each entry instead carries `seen`: the highest address a V8 op has ACTUALLY
|
|
190
|
+
// run at on that stack. It is both the top of the matching window and the bound
|
|
191
|
+
// returned to callers, so the scan-start test in StackScope::enter — "is this
|
|
192
|
+
// enclosing op's marker on the stack I am on?" — rests on an address we watched
|
|
193
|
+
// an op occupy rather than on the kernel's idea of where the mapping ends. A
|
|
194
|
+
// frame higher than anything seen so far misses once and widens the window in
|
|
195
|
+
// place, and a miss always re-queries, so it is only HITS that this has to keep
|
|
196
|
+
// honest.
|
|
197
|
+
//
|
|
198
|
+
// What it does not do is tell two stacks apart that genuinely share one mapping,
|
|
199
|
+
// since entries are keyed by the mapping's bottom: an op running higher up such
|
|
200
|
+
// a mapping re-queries, gets the same bottom back, and widens that one entry.
|
|
201
|
+
// Both then get the lower bottom and a window that spans both. That stays
|
|
202
|
+
// SAFE — one mapping means no guard page in between, so neither the limit nor a
|
|
203
|
+
// scan can walk into an unmapped page — but the upper stack is handed more
|
|
204
|
+
// headroom than it owns. It needs a stack with no guard page below it, which
|
|
205
|
+
// Ruby's fiber pool and pthread both rule out for the stacks we run on.
|
|
206
|
+
//
|
|
207
|
+
// Within that window a cached bottom is the fiber's real bottom: Ruby brackets
|
|
208
|
+
// every fiber stack with PROT_NONE guard pages, so the mapping cannot start
|
|
209
|
+
// below the stack, and a pooled slot is handed to the next fiber with identical
|
|
210
|
+
// bounds.
|
|
211
|
+
const FIBER_REGION_SLOTS: usize = 16;
|
|
212
|
+
|
|
213
|
+
#[derive(Clone, Copy)]
|
|
214
|
+
struct FiberRegion {
|
|
215
|
+
// The mapping's bottom, and the key: 0 marks the slot unused.
|
|
216
|
+
lo: usize,
|
|
217
|
+
// Highest address a V8 op has run at on this stack.
|
|
218
|
+
seen: usize,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#[derive(Clone, Copy)]
|
|
222
|
+
struct FiberRegions {
|
|
223
|
+
slots: [FiberRegion; FIBER_REGION_SLOTS],
|
|
224
|
+
// xorshift64 state for picking a victim. Seeded to a fixed nonzero constant
|
|
225
|
+
// (the golden-ratio one): nothing here needs unpredictability, only that the
|
|
226
|
+
// eviction order doesn't line up with the caller's rotation through fibers.
|
|
227
|
+
rng: u64,
|
|
228
|
+
}
|
|
229
|
+
|
|
160
230
|
thread_local! {
|
|
161
|
-
static
|
|
231
|
+
static FIBER_REGIONS: std::cell::RefCell<FiberRegions> = const {
|
|
232
|
+
std::cell::RefCell::new(FiberRegions {
|
|
233
|
+
slots: [FiberRegion { lo: 0, seen: 0 }; FIBER_REGION_SLOTS],
|
|
234
|
+
rng: 0x9E37_79B9_7F4A_7C15,
|
|
235
|
+
})
|
|
236
|
+
};
|
|
162
237
|
}
|
|
163
238
|
|
|
239
|
+
// (bottom, highest-address-seen) of the stack `addr` is on. `addr` must be an
|
|
240
|
+
// address the CALLER knows is on that stack and as high up it as it can offer —
|
|
241
|
+
// it both answers the lookup and becomes the entry's `seen` on a miss, so a
|
|
242
|
+
// caller that passes its topmost frame gets the widest sound window. A RefCell
|
|
243
|
+
// rather than a Cell so a hit reads the slots in place instead of copying the
|
|
244
|
+
// whole table out on every fiber op.
|
|
164
245
|
fn current_region_bounds_cached(addr: usize) -> (usize, usize) {
|
|
165
|
-
|
|
166
|
-
let (
|
|
167
|
-
|
|
168
|
-
|
|
246
|
+
FIBER_REGIONS.with(|c| {
|
|
247
|
+
if let Some(r) = c
|
|
248
|
+
.borrow()
|
|
249
|
+
.slots
|
|
250
|
+
.iter()
|
|
251
|
+
.find(|r| r.lo != 0 && addr >= r.lo && addr <= r.seen)
|
|
252
|
+
{
|
|
253
|
+
return (r.lo, r.seen);
|
|
169
254
|
}
|
|
170
|
-
let
|
|
171
|
-
if
|
|
172
|
-
|
|
255
|
+
let (lo, _) = query_region_bounds(addr);
|
|
256
|
+
if lo == 0 {
|
|
257
|
+
return (0, 0);
|
|
173
258
|
}
|
|
174
|
-
|
|
259
|
+
let mut cache = c.borrow_mut();
|
|
260
|
+
// Prefer the entry for a stack we already know (a frame above anything
|
|
261
|
+
// seen on it so far, which just widens the window), then an empty slot,
|
|
262
|
+
// and only then evict.
|
|
263
|
+
let slot = cache
|
|
264
|
+
.slots
|
|
265
|
+
.iter()
|
|
266
|
+
.position(|r| r.lo == lo)
|
|
267
|
+
.or_else(|| cache.slots.iter().position(|r| r.lo == 0))
|
|
268
|
+
.unwrap_or_else(|| {
|
|
269
|
+
cache.rng ^= cache.rng << 13;
|
|
270
|
+
cache.rng ^= cache.rng >> 7;
|
|
271
|
+
cache.rng ^= cache.rng << 17;
|
|
272
|
+
(cache.rng % FIBER_REGION_SLOTS as u64) as usize
|
|
273
|
+
});
|
|
274
|
+
cache.slots[slot] = FiberRegion { lo, seen: addr };
|
|
275
|
+
(lo, addr)
|
|
175
276
|
})
|
|
176
277
|
}
|
|
177
278
|
|
|
178
|
-
// The [start, end) of the
|
|
179
|
-
//
|
|
180
|
-
// only called on a cache miss (a new fiber).
|
|
279
|
+
// The [start, end) of the mapping containing `addr`, or (0, 0) when it can't be
|
|
280
|
+
// established. Only called on a cache miss (a new fiber), because it is slow.
|
|
181
281
|
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
282
|
+
// (0, 0) is the fail-safe answer, not an error: every caller falls back to a
|
|
283
|
+
// fixed budget below the SP, which is what this gem did on every platform before
|
|
284
|
+
// any of these lookups existed. So a platform without an implementation, or a
|
|
285
|
+
// lookup that returns something we can't vouch for, degrades to a smaller JS
|
|
286
|
+
// stack rather than to a wrong one.
|
|
287
|
+
//
|
|
288
|
+
// Linux reads /proc/self/maps fresh each time.
|
|
188
289
|
#[cfg(target_os = "linux")]
|
|
189
290
|
fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
190
291
|
use std::io::Read;
|
|
@@ -203,19 +304,84 @@ fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
|
203
304
|
let Some((lo, hi)) = range.split_once('-') else {
|
|
204
305
|
continue;
|
|
205
306
|
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
307
|
+
let (Ok(lo), Ok(hi)) = (usize::from_str_radix(lo, 16), usize::from_str_radix(hi, 16))
|
|
308
|
+
else {
|
|
309
|
+
continue;
|
|
310
|
+
};
|
|
311
|
+
if addr >= lo && addr < hi {
|
|
312
|
+
return (lo, hi);
|
|
213
313
|
}
|
|
214
314
|
}
|
|
215
315
|
(0, 0)
|
|
216
316
|
}
|
|
217
317
|
|
|
218
|
-
|
|
318
|
+
// macOS has no /proc, so ask the Mach VM directly. mach_vm_region reports the
|
|
319
|
+
// region containing `addr` — or, when `addr` sits in a hole, the next one ABOVE
|
|
320
|
+
// it — so the answer is checked for containment rather than trusted. It is also
|
|
321
|
+
// checked for being readable and writable: a stack always is, and a protection
|
|
322
|
+
// that isn't tells us we either walked into something that is not a stack or
|
|
323
|
+
// misread the reply, in which case the fixed-budget fallback is the safer answer.
|
|
324
|
+
#[cfg(target_os = "macos")]
|
|
325
|
+
fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
326
|
+
// Not in libc, which points at the `mach2` crate instead; these are the only
|
|
327
|
+
// entry points we need, so declare them here rather than take the dependency.
|
|
328
|
+
// (libc does carry mach_task_self_, but deprecated for the same reason.)
|
|
329
|
+
unsafe extern "C" {
|
|
330
|
+
static mach_task_self_: libc::mach_port_t;
|
|
331
|
+
fn mach_vm_region(
|
|
332
|
+
target_task: libc::vm_map_t,
|
|
333
|
+
address: *mut libc::mach_vm_address_t,
|
|
334
|
+
size: *mut libc::mach_vm_size_t,
|
|
335
|
+
flavor: libc::c_int,
|
|
336
|
+
info: *mut u32,
|
|
337
|
+
info_count: *mut libc::mach_msg_type_number_t,
|
|
338
|
+
object_name: *mut libc::mach_port_t,
|
|
339
|
+
) -> libc::kern_return_t;
|
|
340
|
+
fn mach_port_deallocate(
|
|
341
|
+
task: libc::mach_port_t,
|
|
342
|
+
name: libc::mach_port_t,
|
|
343
|
+
) -> libc::kern_return_t;
|
|
344
|
+
}
|
|
345
|
+
// VM_REGION_BASIC_INFO_64, and the width of vm_region_basic_info_data_64_t
|
|
346
|
+
// in u32s. Only `protection` (its first field) is read.
|
|
347
|
+
const VM_REGION_BASIC_INFO_64: libc::c_int = 9;
|
|
348
|
+
const VM_REGION_BASIC_INFO_COUNT_64: libc::mach_msg_type_number_t = 9;
|
|
349
|
+
const KERN_SUCCESS: libc::kern_return_t = 0;
|
|
350
|
+
const VM_PROT_READ_WRITE: u32 = 0x1 | 0x2;
|
|
351
|
+
|
|
352
|
+
unsafe {
|
|
353
|
+
let mut start = addr as libc::mach_vm_address_t;
|
|
354
|
+
let mut size: libc::mach_vm_size_t = 0;
|
|
355
|
+
let mut info = [0u32; VM_REGION_BASIC_INFO_COUNT_64 as usize];
|
|
356
|
+
let mut count = VM_REGION_BASIC_INFO_COUNT_64;
|
|
357
|
+
let mut object_name: libc::mach_port_t = 0;
|
|
358
|
+
let kr = mach_vm_region(
|
|
359
|
+
mach_task_self_,
|
|
360
|
+
&mut start,
|
|
361
|
+
&mut size,
|
|
362
|
+
VM_REGION_BASIC_INFO_64,
|
|
363
|
+
info.as_mut_ptr(),
|
|
364
|
+
&mut count,
|
|
365
|
+
&mut object_name,
|
|
366
|
+
);
|
|
367
|
+
// Documented to come back MACH_PORT_NULL for this flavor, but a leaked
|
|
368
|
+
// port name in a per-fiber path would accumulate, so hand it back.
|
|
369
|
+
if object_name != 0 {
|
|
370
|
+
mach_port_deallocate(mach_task_self_, object_name);
|
|
371
|
+
}
|
|
372
|
+
if kr != KERN_SUCCESS || size == 0 || info[0] & VM_PROT_READ_WRITE != VM_PROT_READ_WRITE {
|
|
373
|
+
return (0, 0);
|
|
374
|
+
}
|
|
375
|
+
let (lo, hi) = (start as usize, start.saturating_add(size) as usize);
|
|
376
|
+
if addr >= lo && addr < hi {
|
|
377
|
+
(lo, hi)
|
|
378
|
+
} else {
|
|
379
|
+
(0, 0) // `addr` was in a hole; this is the region after it
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
219
385
|
fn query_region_bounds(_addr: usize) -> (usize, usize) {
|
|
220
386
|
(0, 0)
|
|
221
387
|
}
|
|
@@ -238,8 +404,8 @@ pub(crate) static STACK_DEBUG: AtomicBool = AtomicBool::new(false);
|
|
|
238
404
|
// * Too low (below the real bottom) and a deep recursion grows past the
|
|
239
405
|
// mapped stack and SEGVs the unmapped guard page below it.
|
|
240
406
|
// So detect the stack by comparing the SP to the cached native bounds: on the
|
|
241
|
-
// native stack, anchor to its pthread bottom; on a fiber,
|
|
242
|
-
//
|
|
407
|
+
// native stack, anchor to its pthread bottom; on a fiber, look up the bottom of
|
|
408
|
+
// the mapping holding the SP (the fiber's real bottom — anchoring to
|
|
243
409
|
// SP minus a fixed guard punched through the bottom of Avo's small/deep Capybara
|
|
244
410
|
// fibers and SEGV'd).
|
|
245
411
|
//
|
|
@@ -334,37 +500,44 @@ impl<'a> StackScope<'a> {
|
|
|
334
500
|
let limit = if on_native {
|
|
335
501
|
nbottom + NATIVE_GUARD
|
|
336
502
|
} else {
|
|
337
|
-
// Anchor to the FIBER's real bottom (the
|
|
338
|
-
//
|
|
503
|
+
// Anchor to the FIBER's real bottom (the mapping holding the SP),
|
|
504
|
+
// not the SP: SP - fixed_guard can punch through the
|
|
339
505
|
// bottom of a small/deep fiber stack and SEGV (Avo's deep Capybara
|
|
340
506
|
// filter chain). Reserve FIBER_RESERVE above the bottom for the
|
|
341
507
|
// throw, but keep the limit below the SP so we don't false-overflow;
|
|
342
508
|
// on a nearly-full fiber that clamps the headroom down (an early but
|
|
343
509
|
// CLEAN RangeError).
|
|
344
|
-
|
|
510
|
+
// Look it up by `stack_top`, not the SP: it is this op's highest
|
|
511
|
+
// frame, so it widens the cache's window as far as this op honestly
|
|
512
|
+
// can, and the scan-start test below then has a bound it can trust.
|
|
513
|
+
// Falls back to the SP if the caller had no marker to offer.
|
|
514
|
+
region = current_region_bounds_cached(if stack_top != 0 { stack_top } else { sp });
|
|
345
515
|
if region.0 != 0 {
|
|
346
516
|
(region.0 + FIBER_RESERVE).min(sp.saturating_sub(8 * 1024))
|
|
347
517
|
} else {
|
|
348
|
-
// Region unknown
|
|
349
|
-
//
|
|
350
|
-
//
|
|
518
|
+
// Region unknown. Both shipped platforms implement the lookup,
|
|
519
|
+
// so reaching this on one of them means the lookup FAILED (see
|
|
520
|
+
// query_region_bounds for what it refuses to vouch for) — worth
|
|
521
|
+
// chasing rather than assuming, since the budget here is only a
|
|
522
|
+
// guess: a fixed reserve below the SP, hoping the fiber is that
|
|
351
523
|
// deep. It usually is for an OUTERMOST fiber op, whose SP sits
|
|
352
524
|
// near the fiber's top, and much less reliably for a NESTED one,
|
|
353
525
|
// whose SP is already far down the fiber — there this can land
|
|
354
526
|
// below the fiber's mapped bottom, and V8 then grows through the
|
|
355
|
-
// guard page (SEGV) instead of throwing.
|
|
356
|
-
// means a mach_vm_region lookup for darwin.
|
|
527
|
+
// guard page (SEGV) instead of throwing.
|
|
357
528
|
sp.saturating_sub(64 * 1024)
|
|
358
529
|
}
|
|
359
530
|
};
|
|
360
531
|
// Opt-in diagnostics (RUSTY_RACER_STACK_DEBUG): the SP vs the native
|
|
361
|
-
// stack [nbottom, ntop], the fiber
|
|
532
|
+
// stack [nbottom, ntop], the fiber window (if any), the per-op limit, and
|
|
362
533
|
// whether the SP is above the limit. A crash with sp_above_limit=false
|
|
363
|
-
// means the limit is wrong for the current stack.
|
|
534
|
+
// means the limit is wrong for the current stack. The window is printed
|
|
535
|
+
// CLOSED because its top is the highest address an op was seen at on that
|
|
536
|
+
// stack, not the mapping's end — see current_region_bounds_cached.
|
|
364
537
|
if STACK_DEBUG.load(Ordering::Relaxed) {
|
|
365
538
|
eprintln!(
|
|
366
539
|
"[rusty stack] sp={sp:#x} nbottom={nbottom:#x} ntop={ntop:#x} \
|
|
367
|
-
|
|
540
|
+
window=[{:#x},{:#x}] limit={limit:#x} fiber={} sp_above_limit={} \
|
|
368
541
|
fiber_above_native={}",
|
|
369
542
|
region.0,
|
|
370
543
|
region.1,
|
|
@@ -399,7 +572,11 @@ impl<'a> StackScope<'a> {
|
|
|
399
572
|
}
|
|
400
573
|
// Only a nested op owes anything back: at the outermost op prev_limit is
|
|
401
574
|
// the previous, already-finished op's, which describes no live frame.
|
|
402
|
-
let restore_limit = if nested && changed_limit {
|
|
575
|
+
let restore_limit = if nested && changed_limit {
|
|
576
|
+
prev_limit
|
|
577
|
+
} else {
|
|
578
|
+
0
|
|
579
|
+
};
|
|
403
580
|
// Point V8's conservative-GC-scan start at the stack we're on. The
|
|
404
581
|
// scanner walks [marker, start), so on a fiber a native start runs it off
|
|
405
582
|
// the fiber's mapped top into unmapped memory and SEGVs (Avo's Capybara
|
|
@@ -410,14 +587,24 @@ impl<'a> StackScope<'a> {
|
|
|
410
587
|
// * a fiber we ENTERED (the enclosing op is elsewhere) — this op's
|
|
411
588
|
// `stack_top`, which keeps the range between two real stack pointers
|
|
412
589
|
// (marker..stack_top) and so guaranteed mapped. We can't use the
|
|
413
|
-
//
|
|
590
|
+
// looked-up region's top: that mapping isn't reliably contiguous, so
|
|
414
591
|
// the scan could still hit a hole below it.
|
|
415
592
|
// * a fiber we were ALREADY on (a nested op under a host callback that
|
|
416
593
|
// didn't switch stacks) — the enclosing op's start, which is above
|
|
417
|
-
// ours and
|
|
594
|
+
// ours and covers both ops' frames. Accepted only when it is at or
|
|
595
|
+
// below the highest address an op has been seen running at on this
|
|
596
|
+
// stack (the cache's `seen`, NOT the mapping top, which can reach far
|
|
597
|
+
// into an unrelated neighbour — see current_region_bounds_cached);
|
|
598
|
+
// otherwise we cannot tell it is on our stack at all, and adopting it
|
|
599
|
+
// would send the scan up through our guard page. Since that window
|
|
600
|
+
// lives in a cache with random eviction, the test can also fail
|
|
601
|
+
// because the entry was evicted between the two ops — the scan then
|
|
602
|
+
// narrows to this op's own frame, i.e. the same residual documented
|
|
603
|
+
// there for a real stack switch, now reachable at random once more
|
|
604
|
+
// fiber stacks are live than the cache holds.
|
|
418
605
|
let new_scan_start = if on_native {
|
|
419
606
|
unsafe { v8__base__Stack__GetStackStart() }
|
|
420
|
-
} else if nested && region.0 != 0 && prev_scan_start > sp && prev_scan_start
|
|
607
|
+
} else if nested && region.0 != 0 && prev_scan_start > sp && prev_scan_start <= region.1 {
|
|
421
608
|
prev_scan_start
|
|
422
609
|
} else {
|
|
423
610
|
stack_top
|
|
@@ -463,7 +650,8 @@ impl Drop for StackScope<'_> {
|
|
|
463
650
|
}
|
|
464
651
|
if self.restore_limit != 0 {
|
|
465
652
|
unsafe { v8__Isolate__SetStackLimit(self.real_isolate, self.restore_limit) };
|
|
466
|
-
self.installed_limit
|
|
653
|
+
self.installed_limit
|
|
654
|
+
.store(self.restore_limit, Ordering::Relaxed);
|
|
467
655
|
}
|
|
468
656
|
}
|
|
469
657
|
}
|
|
@@ -224,7 +224,10 @@ fn capture_js_backtrace(scope: &mut v8::PinScope<'_, '_>, max_frames: usize) ->
|
|
|
224
224
|
// bloat the error.
|
|
225
225
|
fn clamp_name(mut s: String) -> String {
|
|
226
226
|
if s.len() > MAX_FRAME_NAME {
|
|
227
|
-
let end = (0..=MAX_FRAME_NAME)
|
|
227
|
+
let end = (0..=MAX_FRAME_NAME)
|
|
228
|
+
.rev()
|
|
229
|
+
.find(|&i| s.is_char_boundary(i))
|
|
230
|
+
.unwrap_or(0);
|
|
228
231
|
s.truncate(end);
|
|
229
232
|
s.push('…');
|
|
230
233
|
}
|
|
@@ -255,7 +258,10 @@ pub(crate) fn arm_watchdog(scope: &mut v8::PinScope<'_, '_, ()>, timeout_ms: u64
|
|
|
255
258
|
// Terminated and the outermost frame sweeps the leftover terminate via
|
|
256
259
|
// WATCHDOG_FIRED; removing only this frame keeps a late terminate from
|
|
257
260
|
// poisoning the next request without clobbering a still-running outer op.
|
|
258
|
-
pub(crate) fn disarm_watchdog(
|
|
261
|
+
pub(crate) fn disarm_watchdog(
|
|
262
|
+
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
263
|
+
generation: Option<u64>,
|
|
264
|
+
) -> bool {
|
|
259
265
|
let Some(generation) = generation else {
|
|
260
266
|
return false;
|
|
261
267
|
};
|
data/lib/rusty_racer/version.rb
CHANGED