Skip to main content

frame_alloc/implementations/wrappers/
depot.rs

1use crate::util::cache::CachePadded;
2use crate::util::lifo::{Lifo, LifoGuard};
3use crate::{
4    AllocError, CpuId, InterruptControl, NoInterruptControl, PageSize, PhysicalAllocator,
5    RegionInit,
6};
7use core::marker::PhantomData;
8use core::num::NonZeroUsize;
9
10const N1: NonZeroUsize = NonZeroUsize::MIN;
11
12/// A per-CPU magazine cache with a shared **depot** tier, over a backing physical
13/// allocator. This extends [`MagazineAllocator`](crate::MagazineAllocator) with a second,
14/// shared cache tier between the per-CPU magazines and the backend.
15///
16/// `SLOTS` is the number of per-CPU magazines, `CAP` each magazine's depth, and
17/// `DEPOT_CAP` the shared depot's capacity in frames. [`CpuId::current_cpu`] is taken
18/// modulo `SLOTS`. `CAP` defaults to 128, `DEPOT_CAP` to 512.
19/// `I` is the [`InterruptControl`] strategy for the magazine and depot locks; it
20/// defaults to [`NoInterruptControl`].
21/// Multi-frame requests first use the backend unchanged. On backend OOM, the
22/// wrapper progressively returns cached frames and retries, preserving cache
23/// contents once the request becomes satisfiable.
24pub struct DepotAllocator<
25    A,
26    S,
27    const SLOTS: usize,
28    const CAP: usize = 128,
29    const DEPOT_CAP: usize = 512,
30    I: InterruptControl = NoInterruptControl,
31> {
32    backend: A,
33    mags: [CachePadded<Lifo<CAP, I>>; SLOTS],
34    depot: CachePadded<Lifo<DEPOT_CAP, I>>,
35    base_frame: PageSize,
36    /// Total cached base frames returned to the backend by explicit flushes or
37    /// OOM recovery across the allocator's life. Diagnostic only.
38    #[cfg(any(feature = "stats", test))]
39    frames_flushed: core::sync::atomic::AtomicUsize,
40    /// Maximum number of frames simultaneously held by the shared depot.
41    #[cfg(any(feature = "stats", test))]
42    peak_depot_len: core::sync::atomic::AtomicUsize,
43    _selector: PhantomData<fn() -> S>,
44}
45
46// SAFETY: the magazine and depot cells are only touched through a `LifoGuard` (i.e.
47// under their lock), and the backend is shared only if it is itself `Sync`. So the
48// wrapper is `Sync`/`Send` exactly when the backend is.
49unsafe impl<
50    A: Sync,
51    S,
52    const SLOTS: usize,
53    const CAP: usize,
54    const DEPOT_CAP: usize,
55    I: InterruptControl,
56> Sync for DepotAllocator<A, S, SLOTS, CAP, DEPOT_CAP, I>
57{
58}
59unsafe impl<
60    A: Send,
61    S,
62    const SLOTS: usize,
63    const CAP: usize,
64    const DEPOT_CAP: usize,
65    I: InterruptControl,
66> Send for DepotAllocator<A, S, SLOTS, CAP, DEPOT_CAP, I>
67{
68}
69
70impl<A, S, const SLOTS: usize, const CAP: usize, const DEPOT_CAP: usize, I: InterruptControl>
71    DepotAllocator<A, S, SLOTS, CAP, DEPOT_CAP, I>
72{
73    /// Create a depot cache over `backend`. Call
74    /// [`init_region`](RegionInit::init_region) before use.
75    ///
76    /// `base_frame` must match the backend's base frame size.
77    pub const fn new(base_frame: PageSize, backend: A) -> Self {
78        assert!(SLOTS > 0, "SLOTS must be > 0");
79        assert!(CAP >= 2, "CAP must be >= 2");
80        assert!(DEPOT_CAP >= 1, "DEPOT_CAP must be >= 1");
81        Self {
82            backend,
83            mags: [const { CachePadded::new(Lifo::new()) }; SLOTS],
84            depot: CachePadded::new(Lifo::new()),
85            base_frame,
86            #[cfg(any(feature = "stats", test))]
87            frames_flushed: core::sync::atomic::AtomicUsize::new(0),
88            #[cfg(any(feature = "stats", test))]
89            peak_depot_len: core::sync::atomic::AtomicUsize::new(0),
90            _selector: PhantomData,
91        }
92    }
93
94    /// The backing allocator, for diagnostics.
95    #[cfg(any(feature = "stats", test))]
96    pub(crate) fn backend(&self) -> &A {
97        &self.backend
98    }
99
100    /// Number of base frames currently cached across all Depots. These are
101    /// frames the backend considers allocated but that are immediately available
102    /// on the fast path. Non-linearizable under concurrent use.
103    #[cfg(any(feature = "stats", test))]
104    pub fn cached_frames(&self) -> usize {
105        let mut total = 0;
106        for mag in &self.mags {
107            total += mag.lock().len();
108        }
109        total + self.depot_len()
110    }
111
112    /// Base frames currently held in the shared depot. Non-linearizable under
113    /// concurrent use.
114    #[cfg(any(feature = "stats", test))]
115    pub fn depot_len(&self) -> usize {
116        self.depot.lock().len()
117    }
118
119    /// High-water mark of base frames held in the shared depot.
120    #[cfg(any(feature = "stats", test))]
121    pub fn peak_depot_len(&self) -> usize {
122        self.peak_depot_len
123            .load(core::sync::atomic::Ordering::Relaxed)
124    }
125
126    /// Total cached base frames returned to the backend by [`flush`](Self::flush)
127    /// or OOM recovery over this allocator's life. Includes depot and magazine
128    /// frames. Monotonic and safe to read concurrently.
129    #[cfg(any(feature = "stats", test))]
130    pub fn frames_flushed(&self) -> usize {
131        self.frames_flushed
132            .load(core::sync::atomic::Ordering::Relaxed)
133    }
134}
135
136impl<
137    A: PhysicalAllocator,
138    S: CpuId,
139    const SLOTS: usize,
140    const CAP: usize,
141    const DEPOT_CAP: usize,
142    I: InterruptControl,
143> DepotAllocator<A, S, SLOTS, CAP, DEPOT_CAP, I>
144{
145    /// Frames moved per refill / drain. Half the capacity gives boundary
146    /// hysteresis; `new` asserts `CAP >= 2`, so this is always >= 1.
147    #[inline(always)]
148    const fn batch() -> usize {
149        CAP / 2
150    }
151
152    /// Move up to `want` frames from the shared depot into `mag`, returning the
153    /// number moved. The caller holds `mag`'s lock; this briefly takes the depot
154    /// lock (always magazine-before-depot, the same order everywhere).
155    fn depot_to_mag(&self, mag: &mut LifoGuard<'_, CAP, I>, want: usize) -> usize {
156        let mut depot = self.depot.lock();
157        let mut moved = 0;
158        while moved < want {
159            match depot.pop() {
160                Some(addr) => {
161                    mag.push(addr);
162                    moved += 1;
163                }
164                None => break,
165            }
166        }
167        moved
168    }
169
170    /// Push as many of `src` as fit into the depot. Returns the number accepted.
171    /// The caller holds the source magazine's lock.
172    fn push_to_depot(&self, src: &[usize]) -> usize {
173        let mut depot = self.depot.lock();
174        let pushed = depot.push_slice(src);
175        #[cfg(any(feature = "stats", test))]
176        self.peak_depot_len
177            .fetch_max(depot.len(), core::sync::atomic::Ordering::Relaxed);
178        pushed
179    }
180
181    fn alloc_one(&self) -> Result<usize, AllocError> {
182        let current = S::current_cpu() % SLOTS;
183        {
184            let mut mag = self.mags[current].lock();
185            if let Some(addr) = mag.pop() {
186                return Ok(addr);
187            }
188
189            // Empty — pull a batch from the shared depot, then hand out one.
190            if self.depot_to_mag(&mut mag, Self::batch()) > 0
191                && let Some(addr) = mag.pop()
192            {
193                return Ok(addr);
194            }
195
196            // Depot empty — refill a batch from the backend, then hand out one.
197            let mut filled = 0usize;
198            while filled < Self::batch() {
199                match self.backend.allocate_physical(self.base_frame, N1) {
200                    Ok(addr) => {
201                        mag.push(addr);
202                        filled += 1;
203                    }
204                    Err(AllocError::OutOfMemory) => break,
205                    Err(e) => return Err(e),
206                }
207            }
208
209            if let Some(addr) = mag.pop() {
210                return Ok(addr);
211            }
212        }
213
214        // A sibling magazine can still own a free frame after the shared tiers
215        // are exhausted. Reuse it directly for an order-0 request.
216        for offset in 1..SLOTS {
217            let slot = (current + offset) % SLOTS;
218            if let Some(addr) = self.mags[slot].lock().pop() {
219                return Ok(addr);
220            }
221        }
222
223        Err(AllocError::OutOfMemory)
224    }
225
226    /// # Safety
227    ///
228    /// `addr` must be a `(base, 1)` frame previously obtained from this allocator.
229    unsafe fn free_one(&self, addr: usize) {
230        let mut mag = self.mags[S::current_cpu() % SLOTS].lock();
231        if mag.is_full() {
232            // Move a batch off the top: into the depot first, backend for the rest.
233            let overflow = mag.take_top(Self::batch());
234            let pushed = self.push_to_depot(overflow);
235            for &a in &overflow[pushed..] {
236                // SAFETY: `a` was handed out by the backend as a (base, 1) frame
237                // and is not referenced elsewhere once drained.
238                unsafe { self.backend.deallocate_physical(self.base_frame, N1, a) };
239            }
240        }
241        mag.push(addr);
242    }
243
244    /// Return up to `want` depot-cached frames to the backend. Drained in `CAP`-sized
245    /// sub-batches. Returns the number drained.
246    fn drain_chunk(&self, want: usize) -> usize {
247        let mut moved = 0;
248        let mut batch = [0usize; CAP];
249        while moved < want {
250            let take = (want - moved).min(CAP);
251            let n = {
252                let mut depot = self.depot.lock();
253                let mut got = 0;
254                while got < take {
255                    match depot.pop() {
256                        Some(addr) => {
257                            batch[got] = addr;
258                            got += 1;
259                        }
260                        None => break,
261                    }
262                }
263                got
264            };
265            if n == 0 {
266                break;
267            }
268            for &addr in &batch[..n] {
269                // SAFETY: addr was handed out by the backend as a (base, 1) frame and
270                // was owned by the depot until popped into `batch` above.
271                unsafe { self.backend.deallocate_physical(self.base_frame, N1, addr) };
272            }
273            moved += n;
274        }
275        #[cfg(any(feature = "stats", test))]
276        self.frames_flushed
277            .fetch_add(moved, core::sync::atomic::Ordering::Relaxed);
278        moved
279    }
280
281    /// Return up to `want` frames from one magazine to the backend. Backend calls
282    /// happen only after releasing the magazine lock.
283    fn drain_magazine(&self, slot: usize, want: usize) -> usize {
284        let mut moved = 0;
285        let mut batch = [0usize; CAP];
286        while moved < want {
287            let take = (want - moved).min(CAP);
288            let n = {
289                let mut mag = self.mags[slot].lock();
290                let mut got = 0;
291                while got < take {
292                    match mag.pop() {
293                        Some(addr) => {
294                            batch[got] = addr;
295                            got += 1;
296                        }
297                        None => break,
298                    }
299                }
300                got
301            };
302            if n == 0 {
303                break;
304            }
305            for &addr in &batch[..n] {
306                // SAFETY: the frame was owned by this magazine until it was
307                // popped into `batch` above.
308                unsafe { self.backend.deallocate_physical(self.base_frame, N1, addr) };
309            }
310            moved += n;
311        }
312        #[cfg(any(feature = "stats", test))]
313        self.frames_flushed
314            .fetch_add(moved, core::sync::atomic::Ordering::Relaxed);
315        moved
316    }
317
318    /// Return the current bounded snapshot of every cache tier to the backend.
319    /// No cache lock is held while invoking the backend. Concurrent frees may
320    /// repopulate a tier after its snapshot is taken.
321    pub fn flush(&self) {
322        let depot = self.depot.lock().len();
323        self.drain_chunk(depot);
324        for slot in 0..SLOTS {
325            let magazine = self.mags[slot].lock().len();
326            self.drain_magazine(slot, magazine);
327        }
328    }
329
330    /// Continue a multi-frame allocation after its initial backend attempt OOMed,
331    /// draining the shared depot first and then the per-CPU magazines.
332    fn recover_after_oom(&self, ps: PageSize, count: NonZeroUsize) -> Result<usize, AllocError> {
333        let mut chunk = Self::batch();
334        loop {
335            let moved = self.drain_chunk(chunk);
336            if moved == 0 {
337                break;
338            }
339            match self.backend.allocate_physical(ps, count) {
340                Err(AllocError::OutOfMemory) => {}
341                other => return other,
342            }
343            chunk = chunk.saturating_mul(2);
344        }
345
346        let current = S::current_cpu() % SLOTS;
347        for offset in 0..SLOTS {
348            let slot = (current + offset) % SLOTS;
349            let mut remaining = self.mags[slot].lock().len();
350            while remaining > 0 {
351                let moved = self.drain_magazine(slot, remaining.min(Self::batch()));
352                if moved == 0 {
353                    break;
354                }
355                remaining -= moved;
356                match self.backend.allocate_physical(ps, count) {
357                    Err(AllocError::OutOfMemory) => {}
358                    other => return other,
359                }
360            }
361        }
362
363        Err(AllocError::OutOfMemory)
364    }
365
366    fn alloc_multiframe(&self, ps: PageSize, count: NonZeroUsize) -> Result<usize, AllocError> {
367        match self.backend.allocate_physical(ps, count) {
368            Err(AllocError::OutOfMemory) => {}
369            other => return other,
370        }
371
372        self.recover_after_oom(ps, count)
373    }
374}
375
376unsafe impl<
377    A: PhysicalAllocator,
378    S: CpuId,
379    const SLOTS: usize,
380    const CAP: usize,
381    const DEPOT_CAP: usize,
382    I: InterruptControl,
383> PhysicalAllocator for DepotAllocator<A, S, SLOTS, CAP, DEPOT_CAP, I>
384{
385    fn allocate_physical(&self, ps: PageSize, count: NonZeroUsize) -> Result<usize, AllocError> {
386        if ps == self.base_frame && count == N1 {
387            return self.alloc_one();
388        }
389        self.alloc_multiframe(ps, count)
390    }
391
392    unsafe fn deallocate_physical(&self, ps: PageSize, count: NonZeroUsize, phys: usize) {
393        if ps == self.base_frame && count == N1 {
394            // SAFETY: caller upholds the per-frame contract; (base, 1) frames are
395            // fungible and cached.
396            unsafe { self.free_one(phys) };
397        } else {
398            // SAFETY: forwarded unchanged to the backend.
399            unsafe { self.backend.deallocate_physical(ps, count, phys) };
400        }
401    }
402}
403
404unsafe impl<
405    A: RegionInit,
406    S,
407    const SLOTS: usize,
408    const CAP: usize,
409    const DEPOT_CAP: usize,
410    I: InterruptControl,
411> RegionInit for DepotAllocator<A, S, SLOTS, CAP, DEPOT_CAP, I>
412{
413    unsafe fn try_init(
414        &self,
415        phys_base: usize,
416        span_len: usize,
417        usable: &[crate::allocator::PhysRange],
418    ) -> Result<(), crate::InitError> {
419        // SAFETY: forwarded unchanged to the backend.
420        unsafe { self.backend.try_init(phys_base, span_len, usable) }
421    }
422
423    unsafe fn add_usable(&self, base: usize, len: usize) {
424        // SAFETY: forwarded unchanged to the backend.
425        unsafe { self.backend.add_usable(base, len) };
426    }
427}
428
429#[cfg(any(feature = "stats", test))]
430impl<
431    A: crate::AllocatorStats,
432    S,
433    const SLOTS: usize,
434    const CAP: usize,
435    const DEPOT_CAP: usize,
436    I: InterruptControl,
437> crate::AllocatorStats for DepotAllocator<A, S, SLOTS, CAP, DEPOT_CAP, I>
438{
439    fn total_bytes(&self) -> usize {
440        self.backend().total_bytes()
441    }
442
443    fn free_bytes(&self) -> usize {
444        self.backend().free_bytes() + self.cached_frames() * self.base_frame.bytes()
445    }
446
447    fn largest_free_bytes(&self) -> usize {
448        self.backend().largest_free_bytes()
449    }
450}