Skip to main content

kevy_alloc/
heap_claims.rs

1//! The claimed-word layer of [`Heap`] (child module via `#[path]`, the
2//! house pattern) — the far-line amortizer for small-allocation churn.
3//! One segment-header round-trip claims up to 64 slots; handout and
4//! same-word recycling are heap-local. Split from `heap.rs` for the
5//! 500-LOC ceiling; the seam is real: everything here is the claim
6//! lifecycle, nothing else in the heap touches a claim's fields.
7
8use core::ptr::NonNull;
9
10use crate::class::{self, NCLASSES};
11use crate::segment::Segment;
12
13use super::Heap;
14
15/// One claimed word of one span, held heap-locally. `base` is the
16/// span's data base, precomputed so the handout path performs no
17/// segment-header access at all.
18#[derive(Clone, Copy)]
19pub(crate) struct Claim {
20    pub(crate) seg: NonNull<Segment>,
21    pub(crate) span_ix: u8,
22    pub(crate) word: u8,
23    pub(crate) claimed: u64,
24    pub(crate) taken: u64,
25    pub(crate) base: *mut u8,
26}
27
28impl Heap {
29    /// Hand out the lowest available bit of the claimed word. Fully
30    /// heap-local: no segment-header access on this path.
31    pub(super) fn pop_claimed(&mut self, c: usize) -> Option<NonNull<u8>> {
32        let cl = self.claims[c].as_mut()?;
33        let avail = cl.claimed & !cl.taken;
34        if avail == 0 {
35            return None;
36        }
37        let b = avail.trailing_zeros();
38        cl.taken |= 1u64 << b;
39        let i = u32::from(cl.word) * 64 + b;
40        NonNull::new(cl.base.wrapping_add(i as usize * class::size_of(c)))
41    }
42
43    /// Retire any outstanding claim, then claim the lowest holed word
44    /// of the class's current span. `None` = the span is full (the
45    /// slow path takes over) or there is no current span.
46    pub(super) fn refill_claim(&mut self, c: usize) -> Option<()> {
47        self.retire_claim(c);
48        let (seg, span_ix) = self.partial[c]?;
49        // SAFETY: partial entries are spans this heap assigned and has
50        // not released; the segment header outlives them.
51        let meta = unsafe { &mut (*seg.as_ptr()).spans[span_ix as usize] };
52        let Some((word, claimed)) = meta.claim_word() else {
53            self.partial[c] = None;
54            return None;
55        };
56        // Claimed bits may land in returned pages; a fresh allocation
57        // owes nothing to its contents, only the bookkeeping notices.
58        if meta.discarded != 0 {
59            let slot_size = class::size_of(c);
60            let lo = u32::from(word) * 64 + claimed.trailing_zeros();
61            let hi = u32::from(word) * 64 + (63 - claimed.leading_zeros());
62            let (pa, _) = crate::pagemap::pages_of_slot(lo, slot_size);
63            let (_, pb) = crate::pagemap::pages_of_slot(hi, slot_size);
64            for p in pa..=pb {
65                meta.discarded &= !(1u16 << p);
66            }
67        }
68        // SAFETY: same header liveness as above.
69        let base = unsafe { seg.as_ref() }.span_base(span_ix as usize);
70        self.claims[c] = Some(Claim { seg, span_ix, word, claimed, taken: 0, base });
71        Some(())
72    }
73
74    /// Write a claim's unused bits back to its span. The span regains
75    /// its holes and the hint walks back; a formerly-full span is
76    /// findable again through `adopt_partial`'s scan (the partial ring
77    /// is an optimization, not the source of truth).
78    pub(super) fn retire_claim(&mut self, c: usize) {
79        let Some(cl) = self.claims[c].take() else { return };
80        let unused = cl.claimed & !cl.taken;
81        if unused == 0 {
82            return;
83        }
84        // SAFETY: claims only reference spans of this heap's live
85        // segments; the header outlives the claim.
86        let meta = unsafe { &mut (*cl.seg.as_ptr()).spans[cl.span_ix as usize] };
87        meta.retire_word(cl.word, unused);
88    }
89
90    /// Retire every class's claim — the write-back before anything
91    /// that reads span occupancy as truth (reclaim's page sweep, drop).
92    pub fn flush_claims(&mut self) {
93        for c in 0..NCLASSES {
94            self.retire_claim(c);
95        }
96    }
97
98    /// Bytes sitting claimed-but-unused across every class — from the
99    /// span's view they are live (the claim pins them), from the
100    /// heap's they are allocatable. The snapshot folds them into
101    /// `span_free` so the accounting identity balances without a
102    /// flush.
103    pub(crate) fn claims_unused_bytes(&self) -> u64 {
104        let mut sum = 0u64;
105        for (c, cl) in self.claims.iter().enumerate() {
106            if let Some(cl) = cl {
107                sum += u64::from((cl.claimed & !cl.taken).count_ones())
108                    * class::size_of(c) as u64;
109            }
110        }
111        sum
112    }
113}