Skip to main content

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::class::SPAN_BYTES;
12use crate::heap::Heap;
13use crate::segment::{FIRST_DATA_SPAN, NO_CLASS, SEGMENT_BYTES, SPANS_PER_SEGMENT};
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 =
26            Stats { live: self.live_bytes, rounding: self.rounding_bytes, ..Stats::default() };
27        let mut seg = self.segments;
28        while !seg.is_null() {
29            // SAFETY: live header from our own list.
30            let s = unsafe { &*seg };
31            st.mapped += SEGMENT_BYTES as u64;
32            st.segment_overhead += SPAN_BYTES as u64;
33            // Slots freed by another thread are still inside this
34            // heap's `live`/`rounding` totals, because that thread could
35            // not reach across to adjust them. Move the amount over here
36            // so every byte is counted exactly once.
37            let parked = s.foreign_bytes.load(core::sync::atomic::Ordering::Relaxed) as u64;
38            let parked_live = s.foreign_live.load(core::sync::atomic::Ordering::Relaxed) as u64;
39            st.cache += parked;
40            st.live -= parked_live;
41            st.rounding -= parked - parked_live;
42            for ix in FIRST_DATA_SPAN..SPANS_PER_SEGMENT {
43                add_span(&mut st, &s.spans[ix]);
44                if s.spans[ix].class != NO_CLASS {
45                    st.spans_assigned += 1;
46                }
47            }
48            seg = s.next;
49        }
50        // Claimed-word bits the heap holds locally: span-side they
51        // count as live (they pin pages exactly as live slots do), but
52        // no caller holds them — they are resident, allocatable bytes,
53        // which is the definition of `span_free`.
54        st.span_free += self.claims_unused_bytes();
55        st
56    }
57}
58
59/// Which bucket a span with no class belongs in. Three, not one.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61enum Unassigned {
62    /// Carved with the segment and never claimed: mapped, never touched.
63    Virgin,
64    /// Emptied, retired, and its pages handed back.
65    Returned,
66    /// Emptied and retired, but the discard was refused — so it is still
67    /// resident, and held rather than released.
68    Held,
69}
70
71/// The classification, separated from the walk that applies it.
72///
73/// Which of the three a given machine can produce is fixed by that
74/// machine: where `os::page_size_matches()` is true no span is ever
75/// `Held`, and where it is false none is ever `Returned`. Deciding it in
76/// a function that takes the two facts as arguments is what lets a test
77/// see all three anywhere — and `Held` is the case worth seeing, because
78/// it is the one where reclaim does nothing.
79fn unassigned_bucket(retired: bool, discarded: u16) -> Unassigned {
80    if !retired {
81        Unassigned::Virgin
82    } else if discarded == crate::pagemap::ALL_PAGES_DISCARDED {
83        Unassigned::Returned
84    } else {
85        Unassigned::Held
86    }
87}
88
89/// Fold one span's bytes into a snapshot.
90///
91/// A span with no class used to go wholesale into `hysteresis`, which
92/// made that one number mean three opposite things: a span nobody has
93/// ever claimed (never touched — `virgin`), a span emptied and given
94/// back to the OS (`returned`), and a span emptied and deliberately
95/// kept (`hysteresis`, the only one the name describes). The identity
96/// balances whichever bucket they land in, so nothing failed; what the
97/// operator got was a single figure that could not answer the question
98/// it existed for. `returned` — the term page-granular reclaim was
99/// built to produce — read 0 on a workload that had just emptied
100/// 20,000 values, while 89 % of the map sat under `hysteresis`.
101fn add_span(st: &mut Stats, meta: &crate::segment::SpanMeta) {
102    if meta.class == NO_CLASS {
103        match unassigned_bucket(meta.retired, meta.discarded) {
104            Unassigned::Virgin => st.virgin += SPAN_BYTES as u64,
105            Unassigned::Returned => st.returned += SPAN_BYTES as u64,
106            Unassigned::Held => st.hysteresis += SPAN_BYTES as u64,
107        }
108        return;
109    }
110    if meta.live == 0 {
111        // Empty but still assigned: the per-sweep hysteresis is holding
112        // it for its class rather than retiring it. Resident and
113        // deliberately kept — the contract's `hysteresis`, exactly.
114        st.hysteresis += SPAN_BYTES as u64;
115        return;
116    }
117    let slot = class::size_of(meta.class as usize) as u64;
118    // live + rounding are already counted from the requested sizes; the
119    // slots themselves are exactly live * slot, so only the free parts
120    // are classified here. A free slot below the high-water mark is
121    // `returned` when every page it overlaps has been discarded
122    // (mapped, not resident) and `span_free` otherwise (touched,
123    // resident). Everything at or above the mark — including the tail
124    // no slot covers — was never touched: `virgin`.
125    for i in 0..u32::from(meta.high_water) {
126        if meta.is_live(i) {
127            continue;
128        }
129        let (pa, pb) = crate::pagemap::pages_of_slot(i, slot as usize);
130        let all_gone = (pa..=pb).all(|p| meta.discarded & (1u16 << p) != 0);
131        if all_gone {
132            st.returned += slot;
133        } else {
134            st.span_free += slot;
135        }
136    }
137    st.virgin += SPAN_BYTES as u64 - u64::from(meta.high_water) * slot;
138}
139
140#[cfg(test)]
141mod unassigned_tests {
142    use super::{Unassigned, unassigned_bucket};
143    use crate::pagemap::ALL_PAGES_DISCARDED;
144
145    /// All three, including the two this machine cannot produce. They
146    /// used to be one number, and the identity balanced either way —
147    /// which is exactly why nothing caught it: `returned` read 0 on a
148    /// workload that had handed most of its map back, while `hysteresis`
149    /// — "retained rather than released" — held it.
150    #[test]
151    fn an_unassigned_span_is_one_of_three_things() {
152        assert_eq!(unassigned_bucket(false, 0), Unassigned::Virgin);
153        assert_eq!(unassigned_bucket(false, ALL_PAGES_DISCARDED), Unassigned::Virgin);
154        assert_eq!(unassigned_bucket(true, ALL_PAGES_DISCARDED), Unassigned::Returned);
155        assert_eq!(unassigned_bucket(true, 0), Unassigned::Held);
156        // A partial discard is not a return: some of the span is still
157        // resident, so the whole span is held.
158        assert_eq!(unassigned_bucket(true, ALL_PAGES_DISCARDED >> 1), Unassigned::Held);
159    }
160}