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}