rusty_racer 0.2.6 → 0.2.7
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 +15 -0
- data/ext/rusty_racer/Cargo.toml +4 -3
- data/ext/rusty_racer/src/stack.rs +383 -107
- data/lib/rusty_racer/version.rb +1 -1
- 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: 214a3eb41745b4ed7c93662fa1cc1e1ecd11bc4678d5633eeab9a2075f5b4965
|
|
4
|
+
data.tar.gz: 87d473be7421ac6ff8d2c5ef7d2e5039ab75987660c313a37916d3f81def2477
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8bd2a0dbfd742b01ceb21447a7028401362d3f9cf55ed449e1eae66aae88fa020c9a80edcdd49e606395880d9c54e4a3d3e8af4b20449387ab6246a234fcd6f6
|
|
7
|
+
data.tar.gz: 0147def7b482ce78b420bfff341f5575a045ba806c1ea853ad3d24bbe2557735a2e672ed9a1510da8727ce9ae0f01ce7409460d8362b6819f3cc2cc30bffbbd8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.7
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- An operation on a Ruby Fiber looks its stack's bounds up once per stack
|
|
8
|
+
rather than once per operation that a 16-entry cache had dropped. Capybara
|
|
9
|
+
filters each result set in an Enumerator left suspended until GC frees it, so
|
|
10
|
+
a page of finds runs on hundreds of distinct fiber stacks, and each lookup read
|
|
11
|
+
all of `/proc/self/maps` — a fifth of a find loop, and slower the larger
|
|
12
|
+
Ruby's fiber pool grew. Every stack an operation has run on is now kept (Ruby
|
|
13
|
+
hands its pooled stacks out again and never unmaps them), and on Linux 6.11+ a
|
|
14
|
+
lookup asks the kernel for the one mapping (`PROCMAP_QUERY`) instead of
|
|
15
|
+
reading the whole map. A call into V8 from a fresh fiber, steady state: 160 µs
|
|
16
|
+
→ 1.0 µs beside a 2,200-line map, 521 µs → 1.0 µs beside 8,300 lines.
|
|
17
|
+
|
|
3
18
|
## 0.2.6
|
|
4
19
|
|
|
5
20
|
### Added
|
data/ext/rusty_racer/Cargo.toml
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# links and loads with no custom archive.
|
|
6
6
|
[package]
|
|
7
7
|
name = "rusty_racer"
|
|
8
|
-
version = "0.2.
|
|
8
|
+
version = "0.2.7"
|
|
9
9
|
edition = "2024"
|
|
10
10
|
publish = false
|
|
11
11
|
|
|
@@ -29,8 +29,9 @@ rb-sys = "0.9"
|
|
|
29
29
|
# (which is built with rusty_v8_enable_simdutf=true) on every platform.
|
|
30
30
|
v8 = { version = "150.4.0", features = ["simdutf"] }
|
|
31
31
|
# pthread stack-bounds query, to set V8's stack limit per op relative to the
|
|
32
|
-
# current native thread (in-thread V8 runs on the Ruby thread's stack).
|
|
33
|
-
|
|
32
|
+
# current native thread (in-thread V8 runs on the Ruby thread's stack). 0.2.172
|
|
33
|
+
# for `_IOWR`, which builds the PROCMAP_QUERY ioctl number per architecture.
|
|
34
|
+
libc = "0.2.172"
|
|
34
35
|
|
|
35
36
|
# `cargo test` links libruby: the crate calls into it, though the marshalling tests
|
|
36
37
|
# that run without a VM never do.
|
|
@@ -151,33 +151,24 @@ fn native_stack_bounds() -> (usize, usize) {
|
|
|
151
151
|
(0, 0)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// (0, 0) if unknown.
|
|
154
|
+
// The BOTTOM of the stack `addr` is on, and the highest address an op has been
|
|
155
|
+
// seen running at on it. Used for a Ruby Fiber, whose mmap'd stack pthread can't
|
|
156
|
+
// see: V8's limit must sit ABOVE this bottom or a deep fiber recursion overflows
|
|
157
|
+
// the real stack and SEGVs the unmapped guard. (0, 0) if unknown.
|
|
159
158
|
//
|
|
160
|
-
// Cached per native thread,
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
159
|
+
// Cached per native thread, for EVERY stack an op has run on, because a miss
|
|
160
|
+
// asks the OS and a workload can run each op on a stack it has never used before:
|
|
161
|
+
// Capybara filters each result set in an Enumerator, left suspended until GC
|
|
162
|
+
// frees it, so a thousand finds run on a thousand distinct fiber stacks. A cache
|
|
163
|
+
// of sixteen slots missed on nearly every one of them, and where a miss reads all
|
|
164
|
+
// of /proc/self/maps — whose length grows with Ruby's fiber pool — those reads
|
|
165
|
+
// were a fifth of the find loop (1003 per 1000 finds). Keeping every stack makes
|
|
166
|
+
// the steady state free: the pool hands the same stacks out again (its free list
|
|
167
|
+
// is LIFO, and it never unmaps them — below), so once each has had an op on it
|
|
168
|
+
// nothing misses. The table is as large as the set of stacks ops have run on,
|
|
169
|
+
// which that pool bounds.
|
|
168
170
|
//
|
|
169
|
-
//
|
|
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
|
|
171
|
+
// The mapping's top is never used, or even kept, as an upper bound. A fiber
|
|
181
172
|
// stack's mapping can extend far above the stack itself, where the kernel merged
|
|
182
173
|
// it with an adjacent anonymous mapping of the same protection that has nothing
|
|
183
174
|
// to do with the fiber pool (measured: a 644KB stack inside an 8840KB mapping,
|
|
@@ -207,94 +198,160 @@ fn native_stack_bounds() -> (usize, usize) {
|
|
|
207
198
|
// Within that window a cached bottom is the fiber's real bottom: Ruby brackets
|
|
208
199
|
// every fiber stack with PROT_NONE guard pages, so the mapping cannot start
|
|
209
200
|
// below the stack, and a pooled slot is handed to the next fiber with identical
|
|
210
|
-
// bounds.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
201
|
+
// bounds. Entries are never invalidated, and that rests on the pool too: Ruby
|
|
202
|
+
// frees a pool allocation only when built with FIBER_POOL_ALLOCATION_FREE (cont.c
|
|
203
|
+
// defines it for a limited address space, i.e. 32-bit, which V8 doesn't ship for,
|
|
204
|
+
// and for RB_EXPERIMENTAL_FIBER_POOL), so the bounds an op saw on a stack stay
|
|
205
|
+
// the bounds of every later fiber there. Under a build that does unmap, an entry
|
|
206
|
+
// can outlive its memory; a hit is then still right when the memory is mapped
|
|
207
|
+
// again as the same pool's stacks at the same address — the same stride, so the
|
|
208
|
+
// same guards — and wrong, with a bottom below the new one, when a later mapping
|
|
209
|
+
// puts a guard page inside the old window. The same goes for a stack that is not
|
|
210
|
+
// Ruby's: a C extension's own coroutine stack, unmapped once it is done with and
|
|
211
|
+
// its addresses reused, can leave an entry that answers for memory it no longer
|
|
212
|
+
// describes. Ruby's M:N thread stacks are safe the way its fibers are: a freed
|
|
213
|
+
// one is only madvised, never unmapped.
|
|
214
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
215
|
+
struct FiberStack {
|
|
216
|
+
// The bottom of the stack's mapping.
|
|
216
217
|
lo: usize,
|
|
217
218
|
// Highest address a V8 op has run at on this stack.
|
|
218
219
|
seen: usize,
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
|
|
222
|
+
// Sorted by `lo`, with disjoint [lo, seen] windows — so `seen` ascends too, and
|
|
223
|
+
// both lookups below are binary searches.
|
|
224
|
+
struct FiberStacks(Vec<FiberStack>);
|
|
225
|
+
|
|
226
|
+
impl FiberStacks {
|
|
227
|
+
// The entry whose window holds `addr`.
|
|
228
|
+
fn find(&self, addr: usize) -> Option<FiberStack> {
|
|
229
|
+
let below = self.0.partition_point(|s| s.lo <= addr);
|
|
230
|
+
let stack = *self.0.get(below.checked_sub(1)?)?;
|
|
231
|
+
(addr <= stack.seen).then_some(stack)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// (bottom, highest-address-seen) of the stack `addr` is on: the entry's when
|
|
235
|
+
// one holds it, else `query`'s answer for `addr`, recorded — 0 there being
|
|
236
|
+
// "unknown", which is recorded nowhere.
|
|
237
|
+
fn bounds(&mut self, addr: usize, query: impl FnOnce(usize) -> usize) -> (usize, usize) {
|
|
238
|
+
if let Some(stack) = self.find(addr) {
|
|
239
|
+
return (stack.lo, stack.seen);
|
|
240
|
+
}
|
|
241
|
+
let lo = query(addr);
|
|
242
|
+
if lo == 0 {
|
|
243
|
+
return (0, 0);
|
|
244
|
+
}
|
|
245
|
+
self.record(lo, addr);
|
|
246
|
+
(lo, addr)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Record an op at `addr` on the stack whose bottom was just looked up as
|
|
250
|
+
// `lo`. Being fresh, that replaces every entry overlapping [lo, addr]: the
|
|
251
|
+
// same stack's narrower window, or one describing memory since mapped afresh.
|
|
252
|
+
// Every query checks that its mapping holds `addr`, so `lo <= addr`.
|
|
253
|
+
fn record(&mut self, lo: usize, addr: usize) {
|
|
254
|
+
debug_assert!(lo <= addr);
|
|
255
|
+
let from = self.0.partition_point(|s| s.seen < lo);
|
|
256
|
+
let to = self.0.partition_point(|s| s.lo <= addr);
|
|
257
|
+
self.0.splice(from..to, [FiberStack { lo, seen: addr }]);
|
|
258
|
+
}
|
|
228
259
|
}
|
|
229
260
|
|
|
230
261
|
thread_local! {
|
|
231
|
-
static
|
|
232
|
-
std::cell::RefCell::new(
|
|
233
|
-
slots: [FiberRegion { lo: 0, seen: 0 }; FIBER_REGION_SLOTS],
|
|
234
|
-
rng: 0x9E37_79B9_7F4A_7C15,
|
|
235
|
-
})
|
|
236
|
-
};
|
|
262
|
+
static FIBER_STACKS: std::cell::RefCell<FiberStacks> =
|
|
263
|
+
const { std::cell::RefCell::new(FiberStacks(Vec::new())) };
|
|
237
264
|
}
|
|
238
265
|
|
|
239
266
|
// (bottom, highest-address-seen) of the stack `addr` is on. `addr` must be an
|
|
240
267
|
// address the CALLER knows is on that stack and as high up it as it can offer —
|
|
241
268
|
// 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.
|
|
243
|
-
//
|
|
244
|
-
// whole table out on every fiber op.
|
|
269
|
+
// caller that passes its topmost frame gets the widest sound window. An op run
|
|
270
|
+
// while the thread's locals are being torn down gets the fail-safe (0, 0).
|
|
245
271
|
fn current_region_bounds_cached(addr: usize) -> (usize, usize) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
.slots
|
|
250
|
-
.iter()
|
|
251
|
-
.find(|r| r.lo != 0 && addr >= r.lo && addr <= r.seen)
|
|
252
|
-
{
|
|
253
|
-
return (r.lo, r.seen);
|
|
254
|
-
}
|
|
255
|
-
let (lo, _) = query_region_bounds(addr);
|
|
256
|
-
if lo == 0 {
|
|
257
|
-
return (0, 0);
|
|
258
|
-
}
|
|
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)
|
|
276
|
-
})
|
|
272
|
+
FIBER_STACKS
|
|
273
|
+
.try_with(|stacks| stacks.borrow_mut().bounds(addr, query_region_bottom))
|
|
274
|
+
.unwrap_or((0, 0))
|
|
277
275
|
}
|
|
278
276
|
|
|
279
|
-
// The
|
|
280
|
-
// established. Only called on a cache miss
|
|
277
|
+
// The bottom of the mapping containing `addr`, or 0 when it can't be
|
|
278
|
+
// established. Only called on a cache miss: a stack no op has run on yet, or a
|
|
279
|
+
// frame above any seen on it.
|
|
281
280
|
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
281
|
+
// 0 is the fail-safe answer, not an error: every caller falls back to a fixed
|
|
282
|
+
// budget below the SP, which is what this gem did on every platform before any
|
|
283
|
+
// of these lookups existed. So a platform without an implementation, or a lookup
|
|
284
|
+
// that returns something we can't vouch for, degrades to a smaller JS stack
|
|
285
|
+
// rather than to a wrong one.
|
|
287
286
|
//
|
|
288
|
-
// Linux
|
|
287
|
+
// Linux asks the kernel for the one mapping (PROCMAP_QUERY, 6.11+: ~0.65us for
|
|
288
|
+
// the open, the ioctl and the close, measured beside 3000 mappings) and reads
|
|
289
|
+
// all of /proc/self/maps only where the ioctl isn't there — ~25-200us, growing
|
|
290
|
+
// with the map. The file is opened per call rather than kept: an open file
|
|
291
|
+
// would go on describing the PARENT's address space in a forked child.
|
|
292
|
+
#[cfg(target_os = "linux")]
|
|
293
|
+
fn query_region_bottom(addr: usize) -> usize {
|
|
294
|
+
let Ok(mut maps) = std::fs::File::open("/proc/self/maps") else {
|
|
295
|
+
return 0;
|
|
296
|
+
};
|
|
297
|
+
procmap_query_bottom(&maps, addr).unwrap_or_else(|| read_maps_bottom(&mut maps, addr))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The bottom of the mapping containing `addr`, through the PROCMAP_QUERY ioctl
|
|
301
|
+
// on an open /proc/self/maps; None when the ioctl fails, which on a live stack
|
|
302
|
+
// address means a kernel without it (ENOTTY), or answers with a mapping that
|
|
303
|
+
// does not hold `addr`, which the kernel's never does but an emulated /proc
|
|
304
|
+
// might.
|
|
289
305
|
#[cfg(target_os = "linux")]
|
|
290
|
-
fn
|
|
306
|
+
fn procmap_query_bottom(maps: &std::fs::File, addr: usize) -> Option<usize> {
|
|
307
|
+
use std::os::fd::AsRawFd;
|
|
308
|
+
// struct procmap_query, from linux/fs.h — not in libc. Only `vma_start` is
|
|
309
|
+
// read back, but the ioctl number encodes the whole struct's size.
|
|
310
|
+
#[repr(C)]
|
|
311
|
+
#[derive(Default)]
|
|
312
|
+
struct ProcmapQuery {
|
|
313
|
+
size: u64,
|
|
314
|
+
query_flags: u64,
|
|
315
|
+
query_addr: u64,
|
|
316
|
+
vma_start: u64,
|
|
317
|
+
vma_end: u64,
|
|
318
|
+
vma_flags: u64,
|
|
319
|
+
vma_page_size: u64,
|
|
320
|
+
vma_offset: u64,
|
|
321
|
+
inode: u64,
|
|
322
|
+
dev_major: u32,
|
|
323
|
+
dev_minor: u32,
|
|
324
|
+
vma_name_size: u32,
|
|
325
|
+
build_id_size: u32,
|
|
326
|
+
vma_name_addr: u64,
|
|
327
|
+
build_id_addr: u64,
|
|
328
|
+
}
|
|
329
|
+
// Flags 0 ask for the mapping COVERING query_addr, not the next one above it.
|
|
330
|
+
const PROCMAP_QUERY: libc::Ioctl = libc::_IOWR::<ProcmapQuery>(b'f' as u32, 17);
|
|
331
|
+
let mut query = ProcmapQuery {
|
|
332
|
+
size: std::mem::size_of::<ProcmapQuery>() as u64,
|
|
333
|
+
query_addr: addr as u64,
|
|
334
|
+
..Default::default()
|
|
335
|
+
};
|
|
336
|
+
let rc = unsafe {
|
|
337
|
+
libc::ioctl(
|
|
338
|
+
maps.as_raw_fd(),
|
|
339
|
+
PROCMAP_QUERY,
|
|
340
|
+
&mut query as *mut ProcmapQuery,
|
|
341
|
+
)
|
|
342
|
+
};
|
|
343
|
+
let (lo, hi) = (query.vma_start as usize, query.vma_end as usize);
|
|
344
|
+
(rc == 0 && lo <= addr && addr < hi).then_some(lo)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// The bottom of the mapping containing `addr`, from the text of
|
|
348
|
+
// /proc/self/maps, or 0 if no line covers it.
|
|
349
|
+
#[cfg(target_os = "linux")]
|
|
350
|
+
fn read_maps_bottom(maps: &mut std::fs::File, addr: usize) -> usize {
|
|
291
351
|
use std::io::Read;
|
|
292
352
|
let mut buf = String::new();
|
|
293
|
-
if
|
|
294
|
-
|
|
295
|
-
.is_err()
|
|
296
|
-
{
|
|
297
|
-
return (0, 0);
|
|
353
|
+
if maps.read_to_string(&mut buf).is_err() {
|
|
354
|
+
return 0;
|
|
298
355
|
}
|
|
299
356
|
for line in buf.lines() {
|
|
300
357
|
// e.g. "7f6a...000-7f6a...000 rw-p 00000000 00:00 0 ..."
|
|
@@ -309,10 +366,10 @@ fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
|
309
366
|
continue;
|
|
310
367
|
};
|
|
311
368
|
if addr >= lo && addr < hi {
|
|
312
|
-
return
|
|
369
|
+
return lo;
|
|
313
370
|
}
|
|
314
371
|
}
|
|
315
|
-
|
|
372
|
+
0
|
|
316
373
|
}
|
|
317
374
|
|
|
318
375
|
// macOS has no /proc, so ask the Mach VM directly. mach_vm_region reports the
|
|
@@ -322,7 +379,7 @@ fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
|
322
379
|
// that isn't tells us we either walked into something that is not a stack or
|
|
323
380
|
// misread the reply, in which case the fixed-budget fallback is the safer answer.
|
|
324
381
|
#[cfg(target_os = "macos")]
|
|
325
|
-
fn
|
|
382
|
+
fn query_region_bottom(addr: usize) -> usize {
|
|
326
383
|
// Not in libc, which points at the `mach2` crate instead; these are the only
|
|
327
384
|
// entry points we need, so declare them here rather than take the dependency.
|
|
328
385
|
// (libc does carry mach_task_self_, but deprecated for the same reason.)
|
|
@@ -370,20 +427,20 @@ fn query_region_bounds(addr: usize) -> (usize, usize) {
|
|
|
370
427
|
mach_port_deallocate(mach_task_self_, object_name);
|
|
371
428
|
}
|
|
372
429
|
if kr != KERN_SUCCESS || size == 0 || info[0] & VM_PROT_READ_WRITE != VM_PROT_READ_WRITE {
|
|
373
|
-
return
|
|
430
|
+
return 0;
|
|
374
431
|
}
|
|
375
432
|
let (lo, hi) = (start as usize, start.saturating_add(size) as usize);
|
|
376
433
|
if addr >= lo && addr < hi {
|
|
377
|
-
|
|
434
|
+
lo
|
|
378
435
|
} else {
|
|
379
|
-
|
|
436
|
+
0 // `addr` was in a hole; this is the region after it
|
|
380
437
|
}
|
|
381
438
|
}
|
|
382
439
|
}
|
|
383
440
|
|
|
384
441
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
385
|
-
fn
|
|
386
|
-
|
|
442
|
+
fn query_region_bottom(_addr: usize) -> usize {
|
|
443
|
+
0
|
|
387
444
|
}
|
|
388
445
|
|
|
389
446
|
// Set from RUSTY_RACER_STACK_DEBUG at init; gates the per-op stack diagnostics.
|
|
@@ -517,7 +574,7 @@ impl<'a> StackScope<'a> {
|
|
|
517
574
|
} else {
|
|
518
575
|
// Region unknown. Both shipped platforms implement the lookup,
|
|
519
576
|
// so reaching this on one of them means the lookup FAILED (see
|
|
520
|
-
//
|
|
577
|
+
// query_region_bottom for what it refuses to vouch for) — worth
|
|
521
578
|
// chasing rather than assuming, since the budget here is only a
|
|
522
579
|
// guess: a fixed reserve below the SP, hoping the fiber is that
|
|
523
580
|
// deep. It usually is for an OUTERMOST fiber op, whose SP sits
|
|
@@ -596,12 +653,15 @@ impl<'a> StackScope<'a> {
|
|
|
596
653
|
// stack (the cache's `seen`, NOT the mapping top, which can reach far
|
|
597
654
|
// into an unrelated neighbour — see current_region_bounds_cached);
|
|
598
655
|
// otherwise we cannot tell it is on our stack at all, and adopting it
|
|
599
|
-
// would send the scan up through our guard page.
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
656
|
+
// would send the scan up through our guard page. The enclosing op
|
|
657
|
+
// ran on this stack at or below `seen` — it widened the entry to its
|
|
658
|
+
// own `stack_top` if nothing had — and an entry is only ever replaced
|
|
659
|
+
// by a fresh lookup overlapping it, which for a stack still mapped is
|
|
660
|
+
// the same stack with a higher `seen`. So the test holds for every
|
|
661
|
+
// enclosing op on our stack whose lookup went through this thread's
|
|
662
|
+
// table; one that ran on another native thread (an M:N Ruby thread
|
|
663
|
+
// that resumed elsewhere inside a host callback) is unknown here, and
|
|
664
|
+
// the scan narrows to this op's own frame — the residual below.
|
|
605
665
|
let new_scan_start = if on_native {
|
|
606
666
|
unsafe { v8__base__Stack__GetStackStart() }
|
|
607
667
|
} else if nested && region.0 != 0 && prev_scan_start > sp && prev_scan_start <= region.1 {
|
|
@@ -655,3 +715,219 @@ impl Drop for StackScope<'_> {
|
|
|
655
715
|
}
|
|
656
716
|
}
|
|
657
717
|
}
|
|
718
|
+
|
|
719
|
+
#[cfg(test)]
|
|
720
|
+
mod tests {
|
|
721
|
+
use super::*;
|
|
722
|
+
|
|
723
|
+
fn stacks(entries: &[(usize, usize)]) -> FiberStacks {
|
|
724
|
+
FiberStacks(
|
|
725
|
+
entries
|
|
726
|
+
.iter()
|
|
727
|
+
.map(|&(lo, seen)| FiberStack { lo, seen })
|
|
728
|
+
.collect(),
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#[test]
|
|
733
|
+
fn a_lookup_hits_only_inside_a_window() {
|
|
734
|
+
let table = stacks(&[(0x1000, 0x1800), (0x3000, 0x3400)]);
|
|
735
|
+
assert_eq!(table.find(0x0fff), None);
|
|
736
|
+
assert_eq!(
|
|
737
|
+
table.find(0x1000),
|
|
738
|
+
Some(FiberStack {
|
|
739
|
+
lo: 0x1000,
|
|
740
|
+
seen: 0x1800
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
assert_eq!(table.find(0x1800).map(|s| s.lo), Some(0x1000));
|
|
744
|
+
// Above `seen` is not known to be on that stack, though no other entry
|
|
745
|
+
// claims it.
|
|
746
|
+
assert_eq!(table.find(0x1801), None);
|
|
747
|
+
assert_eq!(table.find(0x3200).map(|s| s.lo), Some(0x3000));
|
|
748
|
+
assert_eq!(table.find(0x3401), None);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#[test]
|
|
752
|
+
fn a_stack_is_looked_up_once_and_again_only_above_its_window() {
|
|
753
|
+
let mut table = stacks(&[]);
|
|
754
|
+
let queries = std::cell::Cell::new(0);
|
|
755
|
+
let mut bounds = |addr| {
|
|
756
|
+
table.bounds(addr, |_| {
|
|
757
|
+
queries.set(queries.get() + 1);
|
|
758
|
+
0x1000
|
|
759
|
+
})
|
|
760
|
+
};
|
|
761
|
+
assert_eq!(bounds(0x1800), (0x1000, 0x1800));
|
|
762
|
+
assert_eq!(bounds(0x1400), (0x1000, 0x1800));
|
|
763
|
+
assert_eq!(bounds(0x1800), (0x1000, 0x1800));
|
|
764
|
+
assert_eq!(queries.get(), 1);
|
|
765
|
+
assert_eq!(bounds(0x1c00), (0x1000, 0x1c00));
|
|
766
|
+
assert_eq!(bounds(0x1900), (0x1000, 0x1c00));
|
|
767
|
+
assert_eq!(queries.get(), 2);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
#[test]
|
|
771
|
+
fn an_unknown_bottom_is_not_recorded() {
|
|
772
|
+
let mut table = stacks(&[]);
|
|
773
|
+
assert_eq!(table.bounds(0x1800, |_| 0), (0, 0));
|
|
774
|
+
assert_eq!(table.0, Vec::new());
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
#[test]
|
|
778
|
+
fn a_frame_above_the_window_widens_the_same_entry() {
|
|
779
|
+
let mut table = stacks(&[(0x1000, 0x1800), (0x3000, 0x3400)]);
|
|
780
|
+
table.record(0x1000, 0x1c00);
|
|
781
|
+
assert_eq!(table.0, stacks(&[(0x1000, 0x1c00), (0x3000, 0x3400)]).0);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
#[test]
|
|
785
|
+
fn a_new_stack_is_inserted_in_order() {
|
|
786
|
+
let mut table = stacks(&[(0x1000, 0x1800), (0x5000, 0x5400)]);
|
|
787
|
+
table.record(0x3000, 0x3100);
|
|
788
|
+
table.record(0x0100, 0x0200);
|
|
789
|
+
table.record(0x9000, 0x9100);
|
|
790
|
+
assert_eq!(
|
|
791
|
+
table.0,
|
|
792
|
+
stacks(&[
|
|
793
|
+
(0x0100, 0x0200),
|
|
794
|
+
(0x1000, 0x1800),
|
|
795
|
+
(0x3000, 0x3100),
|
|
796
|
+
(0x5000, 0x5400),
|
|
797
|
+
(0x9000, 0x9100)
|
|
798
|
+
])
|
|
799
|
+
.0
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
#[test]
|
|
804
|
+
fn a_fresh_bottom_replaces_every_entry_it_overlaps() {
|
|
805
|
+
// Memory once holding two stacks, now one mapping from 0x1400: both old
|
|
806
|
+
// windows reach into [0x1400, 0x3200], so neither may answer again.
|
|
807
|
+
let mut table = stacks(&[
|
|
808
|
+
(0x0100, 0x0200),
|
|
809
|
+
(0x1000, 0x1800),
|
|
810
|
+
(0x3000, 0x3400),
|
|
811
|
+
(0x5000, 0x5400),
|
|
812
|
+
]);
|
|
813
|
+
table.record(0x1400, 0x3200);
|
|
814
|
+
assert_eq!(
|
|
815
|
+
table.0,
|
|
816
|
+
stacks(&[(0x0100, 0x0200), (0x1400, 0x3200), (0x5000, 0x5400)]).0
|
|
817
|
+
);
|
|
818
|
+
assert_eq!(table.find(0x1200), None);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// A mapping laid out as Ruby's fiber pool lays a stack out: read-write
|
|
822
|
+
// pages with PROT_NONE guard pages among them. Unmapped on drop.
|
|
823
|
+
#[cfg(target_os = "linux")]
|
|
824
|
+
struct GuardedStack {
|
|
825
|
+
base: usize,
|
|
826
|
+
len: usize,
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#[cfg(target_os = "linux")]
|
|
830
|
+
impl GuardedStack {
|
|
831
|
+
fn page() -> usize {
|
|
832
|
+
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// `pages` pages, of which those at the `guards` indexes are PROT_NONE.
|
|
836
|
+
fn map(pages: usize, guards: &[usize]) -> Self {
|
|
837
|
+
let len = pages * Self::page();
|
|
838
|
+
let stack = Self {
|
|
839
|
+
base: Self::mmap(0, len, 0),
|
|
840
|
+
len,
|
|
841
|
+
};
|
|
842
|
+
stack.protect(guards);
|
|
843
|
+
stack
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Map the same addresses afresh (MAP_FIXED replaces the old mapping in
|
|
847
|
+
// one step, so no other thread's mmap can land in between), with other
|
|
848
|
+
// guards.
|
|
849
|
+
fn remap(&self, guards: &[usize]) {
|
|
850
|
+
assert_eq!(Self::mmap(self.base, self.len, libc::MAP_FIXED), self.base);
|
|
851
|
+
self.protect(guards);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
fn mmap(at: usize, len: usize, flags: libc::c_int) -> usize {
|
|
855
|
+
let base = unsafe {
|
|
856
|
+
libc::mmap(
|
|
857
|
+
at as *mut c_void,
|
|
858
|
+
len,
|
|
859
|
+
libc::PROT_READ | libc::PROT_WRITE,
|
|
860
|
+
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | flags,
|
|
861
|
+
-1,
|
|
862
|
+
0,
|
|
863
|
+
)
|
|
864
|
+
};
|
|
865
|
+
assert_ne!(base, libc::MAP_FAILED);
|
|
866
|
+
base as usize
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
fn protect(&self, guards: &[usize]) {
|
|
870
|
+
for &guard in guards {
|
|
871
|
+
let page = self.page_at(guard) as *mut c_void;
|
|
872
|
+
assert_eq!(
|
|
873
|
+
unsafe { libc::mprotect(page, Self::page(), libc::PROT_NONE) },
|
|
874
|
+
0
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
fn page_at(&self, index: usize) -> usize {
|
|
880
|
+
self.base + index * Self::page()
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
#[cfg(target_os = "linux")]
|
|
885
|
+
impl Drop for GuardedStack {
|
|
886
|
+
fn drop(&mut self) {
|
|
887
|
+
unsafe { libc::munmap(self.base as *mut c_void, self.len) };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Both ways of asking agree with each other and with the layout.
|
|
892
|
+
#[cfg(target_os = "linux")]
|
|
893
|
+
fn assert_bottom(addr: usize, expected: usize) {
|
|
894
|
+
assert_eq!(query_region_bottom(addr), expected);
|
|
895
|
+
let mut maps = std::fs::File::open("/proc/self/maps").unwrap();
|
|
896
|
+
assert_eq!(read_maps_bottom(&mut maps, addr), expected);
|
|
897
|
+
// Older kernels have no PROCMAP_QUERY; where there is one it must agree.
|
|
898
|
+
if let Some(bottom) = procmap_query_bottom(&maps, addr) {
|
|
899
|
+
assert_eq!(bottom, expected);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
#[cfg(target_os = "linux")]
|
|
904
|
+
#[test]
|
|
905
|
+
fn the_bottom_is_the_page_above_the_guard() {
|
|
906
|
+
let stack = GuardedStack::map(8, &[0]);
|
|
907
|
+
assert_bottom(stack.page_at(5) + 17, stack.page_at(1));
|
|
908
|
+
assert_bottom(stack.page_at(1), stack.page_at(1));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
#[cfg(target_os = "linux")]
|
|
912
|
+
#[test]
|
|
913
|
+
fn a_stack_mapped_again_with_other_guards_has_its_new_bottom() {
|
|
914
|
+
// Unmap a fiber's stack and map the same addresses again with a guard
|
|
915
|
+
// inside the old one: a lookup is a fresh question, so it answers with
|
|
916
|
+
// the new bottom, never the one the old mapping had.
|
|
917
|
+
let stack = GuardedStack::map(8, &[0]);
|
|
918
|
+
assert_bottom(stack.page_at(6), stack.page_at(1));
|
|
919
|
+
stack.remap(&[0, 4]);
|
|
920
|
+
assert_bottom(stack.page_at(6), stack.page_at(5));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
#[cfg(target_os = "linux")]
|
|
924
|
+
#[test]
|
|
925
|
+
fn a_live_stack_is_found_where_it_runs() {
|
|
926
|
+
// This thread's own stack: the frame is somewhere above its bottom.
|
|
927
|
+
let marker = 0u8;
|
|
928
|
+
let addr = &marker as *const u8 as usize;
|
|
929
|
+
let bottom = query_region_bottom(addr);
|
|
930
|
+
assert_ne!(bottom, 0);
|
|
931
|
+
assert!(bottom <= addr);
|
|
932
|
+
}
|
|
933
|
+
}
|
data/lib/rusty_racer/version.rb
CHANGED