kevy_alloc/snapshot.rs
1//! Turning a heap into a [`Stats`] snapshot.
2//!
3//! Split out of `heap.rs` for the file-size rule, and the seam is a real
4//! one: everything here reads, nothing allocates, and it runs on an INFO
5//! call rather than per operation. Only `live` and `rounding` have to be
6//! maintained as allocations happen — they depend on the size a caller
7//! asked for, which nothing else records. The rest is derived by walking
8//! the segments when someone asks.
9
10use crate::class;
11use crate::segment::{FIRST_DATA_SPAN, NO_CLASS, SEGMENT_BYTES, SPANS_PER_SEGMENT};
12use crate::class::SPAN_BYTES;
13use crate::heap::Heap;
14use crate::stats::Stats;
15
16impl Heap {
17 /// Where every mapped byte is (`bench/V5-ACCOUNTING-CONTRACT.md` §1).
18 ///
19 /// Walks the segments rather than maintaining seven counters on the
20 /// hot path: only `live` and `rounding` depend on the requested size
21 /// and must be tracked as allocations happen. Stats are read on INFO,
22 /// not per operation.
23 #[must_use]
24 pub fn snapshot(&self) -> Stats {
25 let mut st = Stats { live: self.live_bytes, rounding: self.rounding_bytes, ..Stats::default() };
26 let mut seg = self.segments;
27 while !seg.is_null() {
28 // SAFETY: live header from our own list.
29 let s = unsafe { &*seg };
30 st.mapped += SEGMENT_BYTES as u64;
31 st.segment_overhead += SPAN_BYTES as u64;
32 // Slots freed by another thread are still inside this
33 // heap's `live`/`rounding` totals, because that thread could
34 // not reach across to adjust them. Move the amount over here
35 // so every byte is counted exactly once.
36 let parked = s.foreign_bytes.load(core::sync::atomic::Ordering::Relaxed) as u64;
37 let parked_live = s.foreign_live.load(core::sync::atomic::Ordering::Relaxed) as u64;
38 st.cache += parked;
39 st.live -= parked_live;
40 st.rounding -= parked - parked_live;
41 for ix in FIRST_DATA_SPAN..SPANS_PER_SEGMENT {
42 add_span(&mut st, &s.spans[ix]);
43 if s.spans[ix].class != NO_CLASS {
44 st.spans_assigned += 1;
45 }
46 }
47 seg = s.next;
48 }
49 // Claimed-word bits the heap holds locally: span-side they
50 // count as live (they pin pages exactly as live slots do), but
51 // no caller holds them — they are resident, allocatable bytes,
52 // which is the definition of `span_free`.
53 st.span_free += self.claims_unused_bytes();
54 st
55 }
56}
57
58/// Fold one span's bytes into a snapshot.
59fn add_span(st: &mut Stats, meta: &crate::segment::SpanMeta) {
60 if meta.class == NO_CLASS {
61 st.hysteresis += SPAN_BYTES as u64;
62 return;
63 }
64 let slot = class::size_of(meta.class as usize) as u64;
65 // live + rounding are already counted from the requested sizes; the
66 // slots themselves are exactly live * slot, so only the free parts
67 // are classified here. A free slot below the high-water mark is
68 // `returned` when every page it overlaps has been discarded
69 // (mapped, not resident) and `span_free` otherwise (touched,
70 // resident). Everything at or above the mark — including the tail
71 // no slot covers — was never touched: `virgin`.
72 for i in 0..u32::from(meta.high_water) {
73 if meta.is_live(i) {
74 continue;
75 }
76 let (pa, pb) = crate::pagemap::pages_of_slot(i, slot as usize);
77 let all_gone = (pa..=pb).all(|p| meta.discarded & (1u16 << p) != 0);
78 if all_gone {
79 st.returned += slot;
80 } else {
81 st.span_free += slot;
82 }
83 }
84 st.virgin += SPAN_BYTES as u64 - u64::from(meta.high_water) * slot;
85}
86