Skip to main content

kevy_alloc/
reclaim.rs

1//! The reclaim sweep — where pages actually go back.
2//!
3//! Split from `heap.rs` for the file-size rule at the seam that makes
4//! sense: everything here runs on the shard tick, nothing on the
5//! allocation fast path. `reclaim` walks the segments; whole spans with
6//! nothing live are unassigned and discarded (v1 behaviour, with the
7//! per-sweep hysteresis), and spans that still hold live slots get the
8//! v2 treatment: every page no live slot overlaps is handed back
9//! individually, which is the structure M3 forced (RFC §5.1).
10
11use core::ptr::NonNull;
12
13use crate::class;
14use crate::class::SPAN_BYTES;
15use crate::heap::{EMPTY_SPAN_HYSTERESIS, Heap};
16use crate::os;
17use crate::segment::{FIRST_DATA_SPAN, NO_CLASS, SPANS_PER_SEGMENT, Segment};
18
19impl Heap {
20    /// Return free pages to the OS — whole spans where nothing is live,
21    /// and *individual pages* inside spans that still are. The second
22    /// half is v2 (RFC §5.1): M3 measured the whole-span rule returning
23    /// 3 % because a span only empties when all its slots die together,
24    /// while glibc works at page granularity. Now so do we.
25    ///
26    /// Drains foreign frees first: slots parked by other shards pin
27    /// their pages exactly as live slots do, so sweeping before
28    /// draining under-returns for no reason.
29    ///
30    /// The retained count is per sweep rather than cumulative. A running
31    /// counter looked equivalent and was not: it only ever grew, so the
32    /// second sweep found it already past the threshold and returned
33    /// everything, which made the hysteresis vanish after one call.
34    pub fn reclaim(&mut self) {
35        self.reclaim_with(os::page_size_matches());
36    }
37
38    /// [`Self::reclaim`] with the platform's answer supplied.
39    ///
40    /// Only one value of `can_discard` is possible on any given machine,
41    /// so the other side of every branch below is unreachable where it
42    /// runs — and the unreachable one is the interesting one: it is the
43    /// case in which page-granular reclaim does nothing at all, which is
44    /// every Apple Silicon Mac. Taking it as an argument is what lets a
45    /// test drive both, and it is asked once per sweep rather than once
46    /// per span.
47    pub(crate) fn reclaim_with(&mut self, can_discard: bool) {
48        // Claimed-word bits pin their pages exactly as live slots do;
49        // write them back first so the sweep sees true occupancy.
50        self.flush_claims();
51        // Retained large mappings go back each tick: retention beyond a
52        // tick requires sustained traffic to re-earn, and idle memory
53        // stays bounded by the tick length rather than the pool size.
54        crate::large::pool_drain();
55        // Ship pending foreign frees home before sweeping: they pin
56        // pages on OTHER heaps' segments, and the tick is the latency
57        // bound on how long a batch may sit.
58        if !self.outbound.is_empty() {
59            self.outbound.flush();
60        }
61        self.drain_foreign();
62        let mut kept: u16 = 0;
63        let mut seg = self.segments;
64        while !seg.is_null() {
65            // SAFETY: live header from our own list.
66            let s = unsafe { &mut *seg };
67            for ix in FIRST_DATA_SPAN..SPANS_PER_SEGMENT {
68                if s.spans[ix].class == NO_CLASS {
69                    continue;
70                }
71                if s.spans[ix].live != 0 {
72                    Self::discard_free_pages(s, ix, can_discard);
73                    continue;
74                }
75                if kept < EMPTY_SPAN_HYSTERESIS {
76                    kept += 1;
77                    continue;
78                }
79                // SAFETY: `seg` is the segment being walked in this loop; it came
80                // from the live span list, so it is a real segment address and
81                // never null.
82                self.retire_empty_span(unsafe { NonNull::new_unchecked(seg) }, s, ix, can_discard);
83            }
84            seg = s.next;
85        }
86    }
87
88    /// Unhook an empty span from its class and hand its pages back.
89    ///
90    /// Three steps that have to happen together and in this order: drop
91    /// the class's cached pointer to it (a `partial` entry naming a span
92    /// that has been reset would hand out slots from a span with no
93    /// class), decrement the class's span count, and only then reset the
94    /// metadata.
95    fn retire_empty_span(
96        &mut self,
97        seg: NonNull<Segment>,
98        s: &mut Segment,
99        ix: usize,
100        can_discard: bool,
101    ) {
102        let c = s.spans[ix].class as usize;
103        if self.partial[c] == Some((seg, ix as u8)) {
104            self.partial[c] = None;
105        }
106        self.spans_in_class[c] -= 1;
107        s.spans[ix].reset(crate::pagemap::NO_CLASS);
108        // Emptied and handed back, which is not the same unassigned as
109        // never-assigned: this span's pages were touched. The snapshot
110        // needs the difference to tell `returned` from `virgin`.
111        s.spans[ix].retired = true;
112        // Refused on a system whose page size is not `os::PAGE`: see
113        // `discard_free_pages` for why reporting a return that did not
114        // happen is worse than not returning. The span then stays
115        // resident and is accounted as `hysteresis`, which is what it
116        // is — held, not released.
117        if can_discard {
118            let base = s.span_base(ix);
119            // SAFETY: nothing is live in this span, and the range is
120            // page-aligned and inside a live mapping.
121            unsafe {
122                os::discard(NonNull::new_unchecked(base), SPAN_BYTES);
123            }
124            s.spans[ix].discarded = crate::pagemap::ALL_PAGES_DISCARDED;
125        }
126    }
127
128    /// Hand back every page of a *live* span that no live slot overlaps.
129    ///
130    /// The page rule, exactly: below the high-water byte (never-touched
131    /// pages are already non-resident — discarding them is a wasted
132    /// syscall), not already discarded, and no live slot overlapping.
133    /// Contiguous runs go to the OS in one call.
134    fn discard_free_pages(s: &mut Segment, ix: usize, can_discard: bool) {
135        use crate::pagemap::{PAGES_PER_SPAN, slots_of_page};
136        // The page rule is arithmetic at `os::PAGE`, and on a system
137        // whose pages are larger those ranges are not page-aligned.
138        // macOS answers 0 to such a `madvise` and reclaims nothing, so
139        // running on would set `discarded`, count the pages in
140        // `returned`, and lower `predicted_resident()` for memory the
141        // kernel still holds — the accounting would read as a success.
142        // Refusing keeps the seven-term identity true about the world.
143        if !can_discard {
144            return;
145        }
146        let meta = &mut s.spans[ix];
147        let slot = class::size_of(meta.class as usize);
148        let cap = meta.capacity();
149        let hw_bytes = meta.high_water as usize * slot;
150        let base = s.span_base(ix);
151        let mut run: Option<usize> = None;
152        for p in 0..PAGES_PER_SPAN {
153            let meta = &mut s.spans[ix];
154            let fresh = meta.discarded & (1u16 << p) == 0 && p * os::PAGE < hw_bytes && {
155                let (a, b) = slots_of_page(p, slot, cap);
156                !meta.range_has_live(a, b)
157            };
158            if fresh {
159                meta.discarded |= 1u16 << p;
160                run.get_or_insert(p);
161            } else if let Some(r0) = run.take() {
162                // SAFETY: pages r0..p hold no live slot and no metadata
163                // (the bitmap lives in the header — the whole point).
164                unsafe {
165                    os::discard(
166                        NonNull::new_unchecked(base.wrapping_add(r0 * os::PAGE)),
167                        (p - r0) * os::PAGE,
168                    );
169                }
170            }
171        }
172        if let Some(r0) = run {
173            // SAFETY: as above, through the end of the span.
174            unsafe {
175                os::discard(
176                    NonNull::new_unchecked(base.wrapping_add(r0 * os::PAGE)),
177                    (PAGES_PER_SPAN - r0) * os::PAGE,
178                );
179            }
180        }
181    }
182}