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            let stack = [0x7ff6_0000_0000usize | word, 0x7ff6_ffff_ffffusize];
227            let shard = stack_interner_shard(&stack);
228            if frames[..found].iter().all(|(seen, _)| *seen != shard) {
229                frames[found] = (shard, stack);
230                found += 1;
231                if found == N {
232                    return frames;
233                }
234            }
235        }
236        panic!("invariant: deterministic stack hash did not cover {N} distinct shards");
237    }
238
239    fn distinct_frames_for_shard(shard: usize, excluded: &[usize]) -> [usize; 2] {
240        for word in 1..16_384usize {
241            let stack = [0x7ff6_1000_0000usize | word, 0x7ff6_ffff_ffffusize];
242            if stack_interner_shard(&stack) == shard && stack.as_slice() != excluded {
243                return stack;
244            }
245        }
246        panic!("invariant: deterministic stack hash did not find a distinct same-shard stack");
247    }
248
249    #[test]
250    fn stack_interner_hash_covers_all_shards() {
251        let frames = frames_for_shards::<STACK_INTERNER_SHARDS>();
252        let mut seen = [false; STACK_INTERNER_SHARDS];
253        for (shard, stack) in frames {
254            assert_eq!(
255                stack_interner_shard(&stack),
256                shard,
257                "fixture must route to its recorded shard"
258            );
259            seen[shard] = true;
260        }
261        assert!(
262            seen.into_iter().all(|covered| covered),
263            "deterministic stack fixtures must cover every interner shard"
264        );
265    }
266
267    #[test]
268    fn stack_interner_encodes_shard_and_local_id() {
269        crate::reset_profiler_for_testing();
270
271        let [(first_shard, first_stack), (second_shard, second_stack)] = frames_for_shards::<2>();
272        let first = intern_stack(&first_stack);
273        let second = intern_stack(&second_stack);
274
275        assert_eq!(first.shard(), first_shard);
276        assert_eq!(second.shard(), second_shard);
277        assert_eq!(first.local_id(), 0);
278        assert_eq!(second.local_id(), 0);
279        assert_ne!(
280            first, second,
281            "equal local ids in distinct shards must still form distinct StackIds"
282        );
283
284        release_stack(first);
285        release_stack(second);
286        crate::reset_profiler_for_testing();
287    }
288
289    #[test]
290    fn stack_interner_reuses_ids_and_releases_last_reference() {
291        crate::reset_profiler_for_testing();
292
293        let first = intern_stack(&[1, 2, 3]);
294        let repeat = intern_stack(&[1, 2, 3]);
295        assert_eq!(first, repeat);
296
297        {
298            let guard = STACK_INTERNER[first.shard()]
299                .mutex
300                .lock()
301                .unwrap_or_else(std::sync::PoisonError::into_inner);
302            let interner = guard.as_ref().expect("stack interner must be initialized");
303            let entry = interner.entries[first.local_index()]
304                .as_ref()
305                .expect("interned stack id must point to a live entry");
306            assert_eq!(entry.refs, 2);
307            assert_eq!(interner.forward.len(), 1);
308        }
309
310        release_stack(first);
311        {
312            let guard = STACK_INTERNER[first.shard()]
313                .mutex
314                .lock()
315                .unwrap_or_else(std::sync::PoisonError::into_inner);
316            let interner = guard
317                .as_ref()
318                .expect("stack interner must stay initialized");
319            let entry = interner.entries[first.local_index()]
320                .as_ref()
321                .expect("one remaining reference must keep the entry live");
322            assert_eq!(entry.refs, 1);
323            assert_eq!(interner.forward.len(), 1);
324        }
325
326        release_stack(repeat);
327        {
328            let guard = STACK_INTERNER[first.shard()]
329                .mutex
330                .lock()
331                .unwrap_or_else(std::sync::PoisonError::into_inner);
332            let interner = guard
333                .as_ref()
334                .expect("stack interner must stay initialized");
335            assert!(interner.entries[first.local_index()].is_none());
336            assert!(interner.forward.is_empty());
337            assert_eq!(interner.free_ids.as_slice(), &[first.local_id()]);
338        }
339
340        let same_shard = distinct_frames_for_shard(first.shard(), &[1, 2, 3]);
341        let reused = intern_stack(&same_shard);
342        assert_eq!(
343            reused, first,
344            "released same-shard stack ids should be recycled instead of growing the table"
345        );
346
347        crate::reset_profiler_for_testing();
348    }
349
350    #[test]
351    fn stack_interner_interns_distinct_shards_concurrently() {
352        crate::reset_profiler_for_testing();
353
354        let frames = frames_for_shards::<STACK_INTERNER_SHARDS>();
355        let barrier = Arc::new(std::sync::Barrier::new(STACK_INTERNER_SHARDS));
356        let mut workers = Vec::with_capacity(STACK_INTERNER_SHARDS);
357        for (expected_shard, stack) in frames {
358            let barrier = Arc::clone(&barrier);
359            workers.push(std::thread::spawn(move || {
360                barrier.wait();
361                let id = intern_stack(&stack);
362                assert_eq!(id.shard(), expected_shard);
363                id
364            }));
365        }
366
367        let ids: Vec<_> = workers
368            .into_iter()
369            .map(|worker| worker.join().expect("interner worker must not panic"))
370            .collect();
371        assert_eq!(ids.len(), STACK_INTERNER_SHARDS);
372        for id in &ids {
373            let guard = STACK_INTERNER[id.shard()]
374                .mutex
375                .lock()
376                .unwrap_or_else(std::sync::PoisonError::into_inner);
377            let interner = guard.as_ref().expect("stack interner must be initialized");
378            let entry = interner.entries[id.local_index()]
379                .as_ref()
380                .expect("worker interned stack id must point to a live entry");
381            assert_eq!(entry.refs, 1);
382        }
383        for id in ids {
384            release_stack(id);
385        }
386
387        crate::reset_profiler_for_testing();
388    }
389}