Skip to main content

mnemosyne_prof/sampler/
stack_interner.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use super::hasher::FastBuildHasher;
5
6/// Interned identity of a captured stack trace.
7///
8/// Samples store this `u32` handle instead of an owned `Box<[usize]>`, so the
9/// per-live-allocation metadata is a fixed 4 bytes regardless of stack depth and
10/// the actual frame arrays are deduplicated: the leak detector's retained memory
11/// scales with the number of *distinct call sites*, not the number of live
12/// allocations (which can differ by orders of magnitude).
13#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
14pub struct StackId(u32);
15
16const STACK_INTERNER_SHARDS: usize = 64;
17const STACK_INTERNER_SHARD_BITS: u32 = STACK_INTERNER_SHARDS.trailing_zeros();
18const STACK_ID_LOCAL_BITS: u32 = u32::BITS - STACK_INTERNER_SHARD_BITS;
19const STACK_ID_LOCAL_MASK: u32 = (1u32 << STACK_ID_LOCAL_BITS) - 1;
20const _: () = assert!(STACK_INTERNER_SHARDS.is_power_of_two());
21
22impl StackId {
23    #[inline]
24    fn new(shard: usize, local_id: u32) -> Self {
25        debug_assert!(shard < STACK_INTERNER_SHARDS);
26        debug_assert!(local_id <= STACK_ID_LOCAL_MASK);
27        Self(((shard as u32) << STACK_ID_LOCAL_BITS) | local_id)
28    }
29
30    #[inline]
31    fn shard(self) -> usize {
32        (self.0 >> STACK_ID_LOCAL_BITS) as usize
33    }
34
35    #[inline]
36    fn local_index(self) -> usize {
37        (self.0 & STACK_ID_LOCAL_MASK) as usize
38    }
39
40    #[inline]
41    fn local_id(self) -> u32 {
42        self.0 & STACK_ID_LOCAL_MASK
43    }
44}
45
46/// Global stack-trace interner shared by every sampled allocation.
47///
48/// Each distinct live frame sequence is stored exactly once as an `Arc<[usize]>`.
49/// Repeat call sites increment a reference count without allocating; the last
50/// free removes the content-keyed entry and recycles the id slot.
51struct StackInternerShard {
52    forward: HashMap<Arc<[usize]>, StackId, FastBuildHasher>,
53    entries: Vec<Option<StackEntry>>,
54    free_ids: Vec<u32>,
55}
56
57struct StackEntry {
58    frames: Arc<[usize]>,
59    refs: usize,
60}
61
62type RetiredStack = (Arc<[usize]>, Arc<[usize]>);
63
64#[repr(align(64))]
65struct InternerShard {
66    mutex: Mutex<Option<StackInternerShard>>,
67}
68
69static STACK_INTERNER: [InternerShard; STACK_INTERNER_SHARDS] = [const {
70    InternerShard {
71        mutex: Mutex::new(None),
72    }
73}; STACK_INTERNER_SHARDS];
74
75fn stack_interner_shard(frames: &[usize]) -> usize {
76    let mut hasher = <FastBuildHasher as std::hash::BuildHasher>::build_hasher(&FastBuildHasher);
77    std::hash::Hash::hash(&frames, &mut hasher);
78    (std::hash::Hasher::finish(&hasher) as usize) & (STACK_INTERNER_SHARDS - 1)
79}
80
81fn get_stack_interner(shard: usize) -> std::sync::MutexGuard<'static, Option<StackInternerShard>> {
82    let mut lock = STACK_INTERNER[shard]
83        .mutex
84        .lock()
85        .unwrap_or_else(std::sync::PoisonError::into_inner);
86    if lock.is_none() {
87        *lock = Some(StackInternerShard {
88            forward: HashMap::with_hasher(FastBuildHasher),
89            entries: Vec::new(),
90            free_ids: Vec::new(),
91        });
92    }
93    lock
94}
95
96pub(super) fn resolve_stack(id: StackId) -> Option<Arc<[usize]>> {
97    let guard = STACK_INTERNER[id.shard()]
98        .mutex
99        .lock()
100        .unwrap_or_else(std::sync::PoisonError::into_inner);
101    guard.as_ref().and_then(|interner| interner.resolve(id))
102}
103
104/// Interns `frames`, returning its stable [`StackId`]. Allocates only on a
105/// first-seen call site; repeat sites are a hash lookup with no allocation.
106pub(super) fn intern_stack(frames: &[usize]) -> StackId {
107    let shard = stack_interner_shard(frames);
108    {
109        let mut guard = get_stack_interner(shard);
110        let interner = guard
111            .as_mut()
112            .expect("stack interner shard must be initialized");
113        if let Some(&id) = interner.forward.get(frames) {
114            return interner.retain(id);
115        }
116    }
117
118    let arc: Arc<[usize]> = Arc::from(frames);
119    let mut guard = get_stack_interner(shard);
120    let interner = guard
121        .as_mut()
122        .expect("stack interner shard must be initialized");
123    if let Some(&id) = interner.forward.get(arc.as_ref()) {
124        return interner.retain(id);
125    }
126    let id = if let Some(local_id) = interner.free_ids.pop() {
127        let id = StackId::new(shard, local_id);
128        interner.entries[local_id as usize] = Some(StackEntry {
129            frames: Arc::clone(&arc),
130            refs: 1,
131        });
132        id
133    } else {
134        assert!(
135            interner.entries.len() <= STACK_ID_LOCAL_MASK as usize,
136            "invariant: stack interner shard id count exceeds its bit budget"
137        );
138        let local_id = u32::try_from(interner.entries.len())
139            .expect("invariant: stack interner shard id count exceeds u32::MAX");
140        let id = StackId::new(shard, local_id);
141        interner.entries.push(Some(StackEntry {
142            frames: Arc::clone(&arc),
143            refs: 1,
144        }));
145        id
146    };
147    interner.forward.insert(arc, id);
148    id
149}
150
151pub(super) fn release_stack(id: StackId) {
152    let mut guard = STACK_INTERNER[id.shard()]
153        .mutex
154        .lock()
155        .unwrap_or_else(std::sync::PoisonError::into_inner);
156    let retired = guard.as_mut().and_then(|interner| interner.release(id));
157    drop(guard);
158    // The entry and map key can hold the final two strong references. Their
159    // backing allocation is released after the shard lock, so allocator work
160    // cannot lengthen the interner's critical section or re-enter it.
161    drop(retired);
162}
163
164pub(super) fn reset_stack_interner_state() {
165    for shard in &STACK_INTERNER {
166        let mut lock = shard
167            .mutex
168            .lock()
169            .unwrap_or_else(std::sync::PoisonError::into_inner);
170        *lock = None;
171    }
172}
173
174impl StackInternerShard {
175    fn retain(&mut self, id: StackId) -> StackId {
176        let entry = self
177            .entries
178            .get_mut(id.local_index())
179            .and_then(Option::as_mut)
180            .expect("invariant: stack interner forward map points at a live entry");
181        entry.refs = entry
182            .refs
183            .checked_add(1)
184            .expect("invariant: stack interner reference count overflow");
185        id
186    }
187
188    fn resolve(&self, id: StackId) -> Option<Arc<[usize]>> {
189        self.entries
190            .get(id.local_index())
191            .and_then(Option::as_ref)
192            .map(|entry| Arc::clone(&entry.frames))
193    }
194
195    fn release(&mut self, id: StackId) -> Option<RetiredStack> {
196        let entry_slot = self.entries.get_mut(id.local_index())?;
197        if entry_slot.as_ref()?.refs > 1 {
198            entry_slot.as_mut()?.refs -= 1;
199            return None;
200        }
201
202        let entry = entry_slot
203            .take()
204            .expect("invariant: checked live stack entry must remain present");
205        let (key, removed_id) = self
206            .forward
207            .remove_entry(entry.frames.as_ref())
208            .expect("invariant: live stack entry must have a forward-map key");
209        assert_eq!(
210            removed_id, id,
211            "invariant: stack interner forward map points at a different id"
212        );
213        self.free_ids.push(id.local_id());
214        Some((entry.frames, key))
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    fn frames_for_shards<const N: usize>() -> [(usize, [usize; 2]); N] {
223        let mut frames = [(usize::MAX, [0usize; 2]); N];
224        let mut found = 0usize;
225        for word in 1..16_384usize {
226            // Synthetic frame values exercise shard diversity without relying
227            // on a 64-bit virtual-address layout unavailable to wasm32.
228            let stack = [0x1000usize | word, usize::MAX - 0x1000];
229            let shard = stack_interner_shard(&stack);
230            if frames[..found].iter().all(|(seen, _)| *seen != shard) {
231                frames[found] = (shard, stack);
232                found += 1;
233                if found == N {
234                    return frames;
235                }
236            }
237        }
238        panic!("invariant: deterministic stack hash did not cover {N} distinct shards");
239    }
240
241    fn distinct_frames_for_shard(shard: usize, excluded: &[usize]) -> [usize; 2] {
242        for word in 1..16_384usize {
243            let stack = [0x0010_0000usize | word, usize::MAX - 0x1000];
244            if stack_interner_shard(&stack) == shard && stack.as_slice() != excluded {
245                return stack;
246            }
247        }
248        panic!("invariant: deterministic stack hash did not find a distinct same-shard stack");
249    }
250
251    #[test]
252    fn stack_interner_hash_covers_all_shards() {
253        let frames = frames_for_shards::<STACK_INTERNER_SHARDS>();
254        let mut seen = [false; STACK_INTERNER_SHARDS];
255        for (shard, stack) in frames {
256            assert_eq!(
257                stack_interner_shard(&stack),
258                shard,
259                "fixture must route to its recorded shard"
260            );
261            seen[shard] = true;
262        }
263        assert!(
264            seen.into_iter().all(|covered| covered),
265            "deterministic stack fixtures must cover every interner shard"
266        );
267    }
268
269    #[test]
270    fn stack_interner_encodes_shard_and_local_id() {
271        crate::reset_profiler_for_testing();
272
273        let [(first_shard, first_stack), (second_shard, second_stack)] = frames_for_shards::<2>();
274        let first = intern_stack(&first_stack);
275        let second = intern_stack(&second_stack);
276
277        assert_eq!(first.shard(), first_shard);
278        assert_eq!(second.shard(), second_shard);
279        assert_eq!(first.local_id(), 0);
280        assert_eq!(second.local_id(), 0);
281        assert_ne!(
282            first, second,
283            "equal local ids in distinct shards must still form distinct StackIds"
284        );
285
286        release_stack(first);
287        release_stack(second);
288        crate::reset_profiler_for_testing();
289    }
290
291    #[test]
292    fn stack_interner_reuses_ids_and_releases_last_reference() {
293        crate::reset_profiler_for_testing();
294
295        let first = intern_stack(&[1, 2, 3]);
296        let repeat = intern_stack(&[1, 2, 3]);
297        assert_eq!(first, repeat);
298
299        {
300            let guard = STACK_INTERNER[first.shard()]
301                .mutex
302                .lock()
303                .unwrap_or_else(std::sync::PoisonError::into_inner);
304            let interner = guard.as_ref().expect("stack interner must be initialized");
305            let entry = interner.entries[first.local_index()]
306                .as_ref()
307                .expect("interned stack id must point to a live entry");
308            assert_eq!(entry.refs, 2);
309            assert_eq!(interner.forward.len(), 1);
310        }
311
312        release_stack(first);
313        {
314            let guard = STACK_INTERNER[first.shard()]
315                .mutex
316                .lock()
317                .unwrap_or_else(std::sync::PoisonError::into_inner);
318            let interner = guard
319                .as_ref()
320                .expect("stack interner must stay initialized");
321            let entry = interner.entries[first.local_index()]
322                .as_ref()
323                .expect("one remaining reference must keep the entry live");
324            assert_eq!(entry.refs, 1);
325            assert_eq!(interner.forward.len(), 1);
326        }
327
328        release_stack(repeat);
329        {
330            let guard = STACK_INTERNER[first.shard()]
331                .mutex
332                .lock()
333                .unwrap_or_else(std::sync::PoisonError::into_inner);
334            let interner = guard
335                .as_ref()
336                .expect("stack interner must stay initialized");
337            assert!(interner.entries[first.local_index()].is_none());
338            assert!(interner.forward.is_empty());
339            assert_eq!(interner.free_ids.as_slice(), &[first.local_id()]);
340        }
341
342        let same_shard = distinct_frames_for_shard(first.shard(), &[1, 2, 3]);
343        let reused = intern_stack(&same_shard);
344        assert_eq!(
345            reused, first,
346            "released same-shard stack ids should be recycled instead of growing the table"
347        );
348
349        crate::reset_profiler_for_testing();
350    }
351
352    #[test]
353    fn stack_interner_interns_distinct_shards_concurrently() {
354        crate::reset_profiler_for_testing();
355
356        let frames = frames_for_shards::<STACK_INTERNER_SHARDS>();
357        let barrier = Arc::new(std::sync::Barrier::new(STACK_INTERNER_SHARDS));
358        let mut workers = Vec::with_capacity(STACK_INTERNER_SHARDS);
359        for (expected_shard, stack) in frames {
360            let barrier = Arc::clone(&barrier);
361            workers.push(std::thread::spawn(move || {
362                barrier.wait();
363                let id = intern_stack(&stack);
364                assert_eq!(id.shard(), expected_shard);
365                id
366            }));
367        }
368
369        let ids: Vec<_> = workers
370            .into_iter()
371            .map(|worker| worker.join().expect("interner worker must not panic"))
372            .collect();
373        assert_eq!(ids.len(), STACK_INTERNER_SHARDS);
374        for id in &ids {
375            let guard = STACK_INTERNER[id.shard()]
376                .mutex
377                .lock()
378                .unwrap_or_else(std::sync::PoisonError::into_inner);
379            let interner = guard.as_ref().expect("stack interner must be initialized");
380            let entry = interner.entries[id.local_index()]
381                .as_ref()
382                .expect("worker interned stack id must point to a live entry");
383            assert_eq!(entry.refs, 1);
384        }
385        for id in ids {
386            release_stack(id);
387        }
388
389        crate::reset_profiler_for_testing();
390    }
391}