Skip to main content

mz_ore/pool/
region.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Size-class virtual-memory regions for the buffer pool.
17//!
18//! One [`Region`] per size class, each a single anonymous `mmap` reservation.
19//! The reservation is virtual; physical memory materializes on first write to
20//! a slot. Slots are scoped to residency: eviction releases a slot's physical
21//! pages with [`dontneed`] and returns the slot index to the free list, so a
22//! chunk holds a slot only from insert until its eviction, and the pool reads
23//! slots strictly copy-out under the owning chunk's state lock.
24//!
25//! Two fault-amortization mechanisms soften the cost of cycling slots:
26//!
27//! * Regions whose class is at least one huge page are aligned to the huge
28//!   page and advised `MADV_HUGEPAGE`, so populating a large slot costs one
29//!   fault instead of one per 4 KiB.
30//! * The free list is split into a *warm* side (pages kept resident; reuse
31//!   faults nothing and skips the kernel's page zeroing) and a *cold* side
32//!   (pages released). The pool decides which side a freed slot joins,
33//!   bounding total warm bytes as a fraction of its budget.
34//!
35//! TODO: consider `mlock`ing slot regions so kernel swap can never write
36//! out pages the engine would discard or write better itself. Needs
37//! `RLIMIT_MEMLOCK` tuning before it can be on by default.
38//!
39//! All platform access goes through the [`sys`] seam: mapping, unmapping,
40//! paging advice, and the page size. Under Miri the seam swaps to a
41//! Rust-heap backing with advice as a contents-preserving no-op, so the
42//! pool's tests (including the unsafe slot borrows they exercise) run under
43//! the interpreter.
44
45use std::io;
46use std::sync::Mutex;
47
48use crate::cast::CastFrom;
49
50/// Chunk size classes in bytes, smallest first. The pool places each chunk in
51/// the smallest class that fits its payload.
52///
53/// The top classes deliberately overshoot the batchers' nominal ~2 MiB chunk
54/// target: the ship heuristic re-targets the next 2 MiB boundary whenever a
55/// single push crosses one, so real chunk sizes are multimodal with bands
56/// just under each boundary. A class that fits only the nominal target sends
57/// the higher bands to the unpageable heap fallback. Slot internal
58/// fragmentation is virtual-only — slots populate lazily, so a chunk costs
59/// physical memory for its payload, not its class size.
60pub(crate) const SIZE_CLASSES: [usize; 8] = [
61    64 << 10,
62    128 << 10,
63    256 << 10,
64    512 << 10,
65    1 << 20,
66    2 << 20,
67    4 << 20,
68    8 << 20,
69];
70
71/// The smallest size class that fits a payload of `len_bytes`, or `None`
72/// when even the largest class is too small. A selected class always fits:
73/// `SIZE_CLASSES[class] >= len_bytes`, the bound the pool turns into slice
74/// lengths over slot memory (proved by the Kani harnesses).
75pub(crate) fn size_class_for(len_bytes: usize) -> Option<usize> {
76    SIZE_CLASSES.iter().position(|&c| c >= len_bytes)
77}
78
79/// One anonymous virtual-memory reservation serving fixed-size slots of a
80/// single size class.
81#[derive(Debug)]
82pub(crate) struct Region {
83    base: *mut u8,
84    capacity: usize,
85    /// Alignment of `base`, needed to return the mapping to [`sys::unmap`].
86    align: usize,
87    class_size: usize,
88    slots: Mutex<SlotAllocator>,
89}
90
91/// Free-list-plus-bump slot allocator. A slot index returns to a free list
92/// whenever its chunk stops being resident — eviction and free alike. Warm
93/// slots keep their physical pages (reuse is fault-free); cold slots had
94/// theirs released. Never-allocated slots beyond the high-water mark are
95/// untouched virtual space and fault on first write like cold ones.
96#[derive(Debug)]
97struct SlotAllocator {
98    free_warm: Vec<u32>,
99    free_cold: Vec<u32>,
100    high_water: u32,
101    max_slots: u32,
102}
103
104impl SlotAllocator {
105    fn new(max_slots: u32) -> SlotAllocator {
106        SlotAllocator {
107            free_warm: Vec::new(),
108            free_cold: Vec::new(),
109            high_water: 0,
110            max_slots,
111        }
112    }
113
114    /// Allocates a slot index, or `None` when every slot is in use; the flag
115    /// reports whether the slot came from the warm list. Warm slots are
116    /// preferred, then cold, then never-touched bump slots.
117    fn alloc(&mut self) -> Option<(u32, bool)> {
118        if let Some(slot) = self.free_warm.pop() {
119            return Some((slot, true));
120        }
121        if let Some(slot) = self.free_cold.pop() {
122            return Some((slot, false));
123        }
124        if self.high_water == self.max_slots {
125            return None;
126        }
127        let slot = self.high_water;
128        self.high_water += 1;
129        Some((slot, false))
130    }
131
132    /// Returns a previously allocated slot to the warm or cold free list.
133    fn free(&mut self, slot: u32, warm: bool) {
134        crate::soft_assert_no_log!(slot < self.high_water);
135        if warm {
136            self.free_warm.push(slot);
137        } else {
138            self.free_cold.push(slot);
139        }
140    }
141}
142
143// SAFETY: `base` points at an anonymous mapping owned exclusively by this
144// `Region` for its whole lifetime. Slot allocation is serialized by the
145// `slots` mutex, and access to a slot's contents is serialized by the
146// owning chunk's state mutex; the raw pointer itself carries no thread
147// affinity.
148unsafe impl Send for Region {}
149// SAFETY: see the `Send` justification; all interior mutability is behind
150// the `slots` mutex, and disjoint slots are written only by their owning
151// chunks.
152unsafe impl Sync for Region {}
153
154impl Region {
155    /// Reserves a region of `capacity_bytes` (rounded down to a whole number
156    /// of slots) for slots of `class_size` bytes.
157    ///
158    /// On Linux, regions whose class is at least [`HUGE_PAGE`] are aligned to
159    /// the huge page and advised `MADV_HUGEPAGE`: their slots tile huge-page
160    /// boundaries exactly, so populating a slot is one huge-page fault rather
161    /// than one fault per 4 KiB, and a whole-slot [`dontneed`] frees whole
162    /// huge pages without splitting any. Regions whose class is below the
163    /// huge page are advised `MADV_NOHUGEPAGE` instead, so reclaim over a
164    /// single slot stays page-exact.
165    pub(crate) fn new(class_size: usize, capacity_bytes: usize) -> io::Result<Region> {
166        Region::with_huge_pages(class_size, capacity_bytes, class_size >= HUGE_PAGE)
167    }
168
169    /// As [`Region::new`], but the region opts out of transparent huge pages
170    /// regardless of class size. For regions whose slots are reclaimed with
171    /// `MADV_PAGEOUT` under observed residency: reclaiming a range a large
172    /// folio covers only partially needs a folio split, and a failed split
173    /// silently leaves pages resident.
174    pub(crate) fn new_nohuge(class_size: usize, capacity_bytes: usize) -> io::Result<Region> {
175        Region::with_huge_pages(class_size, capacity_bytes, false)
176    }
177
178    fn with_huge_pages(class_size: usize, capacity_bytes: usize, huge: bool) -> io::Result<Region> {
179        let page = sys::page_size();
180        assert!(class_size > 0 && class_size % page == 0);
181        let capacity = capacity_bytes - capacity_bytes % class_size;
182        if capacity == 0 {
183            // A capacity below one slot yields an empty region: `alloc`
184            // always answers `None` and the caller's exhaustion fallback
185            // carries the class. No mapping exists; drop has nothing to do.
186            return Ok(Region {
187                base: std::ptr::NonNull::<u8>::dangling().as_ptr(),
188                capacity: 0,
189                align: page,
190                class_size,
191                slots: Mutex::new(SlotAllocator::new(0)),
192            });
193        }
194        let max_slots = u32::try_from(capacity / class_size).expect("slot count fits u32");
195        let align = if cfg!(target_os = "linux") && huge {
196            HUGE_PAGE
197        } else {
198            page
199        };
200        let base = sys::map(capacity, align)?;
201        if !huge {
202            // Opt the whole region out of transparent huge pages before any
203            // slot is touched. Production hosts run
204            // `transparent_hugepage=madvise`, where an unadvised region gets
205            // no fault-time folios anyway, so this is defense in depth for
206            // hosts running `always`: there, fault-time folios would straddle
207            // sub-huge-page slot boundaries, and khugepaged (which tolerates
208            // up to `max_ptes_none` empty entries) re-collapses ranges a
209            // slot-granular [`dontneed`] just reclaimed, resurrecting the
210            // released memory.
211            nohugepage(base, capacity);
212        }
213        // Pool contents are recreatable spill data: keep the (potentially
214        // huge) anonymous reservation out of core dumps.
215        dontdump(base, capacity);
216        Ok(Region {
217            base,
218            capacity,
219            align,
220            class_size,
221            slots: Mutex::new(SlotAllocator::new(max_slots)),
222        })
223    }
224
225    /// Size in bytes of every slot in this region.
226    pub(crate) fn class_size(&self) -> usize {
227        self.class_size
228    }
229
230    /// Allocates a slot index, or `None` if every slot of the class is in
231    /// use; the flag reports whether the slot came from the warm list (its
232    /// pages are resident; writing it faults nothing). Exhaustion means
233    /// residency outgrew the class reservation; callers degrade rather
234    /// than fail.
235    pub(crate) fn alloc(&self) -> Option<(u32, bool)> {
236        self.slots
237            .lock()
238            .expect("region allocator poisoned")
239            .alloc()
240    }
241
242    /// Returns a slot to the warm or cold free list. The caller must be
243    /// freeing the chunk that owned the slot, must have released the slot's
244    /// physical pages iff `warm` is false, and owns the warm-bytes accounting
245    /// that bounds the warm side.
246    pub(crate) fn free(&self, slot: u32, warm: bool) {
247        self.slots
248            .lock()
249            .expect("region allocator poisoned")
250            .free(slot, warm);
251    }
252
253    /// Moves warm free slots to the cold list, releasing their physical
254    /// pages, until at least `want_bytes` have been cooled or no warm slot
255    /// remains. Returns the bytes cooled. The caller owns the warm-bytes
256    /// accounting that bounds the warm side.
257    pub(crate) fn cool_warm_slots(&self, want_bytes: usize) -> usize {
258        let mut slots = self.slots.lock().expect("region allocator poisoned");
259        let mut cooled = 0;
260        while cooled < want_bytes {
261            let Some(slot) = slots.free_warm.pop() else {
262                break;
263            };
264            let offset = usize::cast_from(slot) * self.class_size;
265            // SAFETY: the slot is on a free list and the allocator mutex is
266            // held, so no chunk owns it and no allocation can race; the
267            // range stays within the region's mapping.
268            unsafe {
269                dontneed(self.base.add(offset), self.class_size);
270            }
271            slots.free_cold.push(slot);
272            cooled += self.class_size;
273        }
274        cooled
275    }
276
277    /// Test hook: overwrites every free slot with `0xDE` so stale contents
278    /// cannot masquerade as correct data when a slot is reused.
279    #[cfg(test)]
280    pub(crate) fn poison_free_slots(&self) {
281        let slots = self.slots.lock().expect("region allocator poisoned");
282        for &slot in slots.free_warm.iter().chain(slots.free_cold.iter()) {
283            let offset = usize::cast_from(slot) * self.class_size;
284            // SAFETY: the slot is on a free list and the allocator mutex is
285            // held, so no chunk owns it and no allocation can race; the write
286            // stays within the region's mapping.
287            unsafe {
288                std::ptr::write_bytes(self.base.add(offset), 0xDE, self.class_size);
289            }
290        }
291    }
292
293    /// The base address of a slot, fixed while its owning chunk is resident.
294    pub(crate) fn slot_ptr(&self, slot: u32) -> *mut u8 {
295        let offset = usize::cast_from(slot) * self.class_size;
296        crate::soft_assert_no_log!(offset + self.class_size <= self.capacity);
297        // SAFETY: `slot` was handed out by `alloc`, so `offset + class_size`
298        // lies within the single `capacity`-byte mapping that `base` points
299        // to; the add stays in bounds of one allocated object.
300        unsafe { self.base.add(offset) }
301    }
302}
303
304impl Drop for Region {
305    fn drop(&mut self) {
306        // Empty regions never created a mapping.
307        if self.capacity == 0 {
308            return;
309        }
310        // SAFETY: `base`/`capacity`/`align` describe exactly the mapping
311        // created in `new`, and dropping the region means no chunk (and
312        // hence no outstanding borrow) refers into it any longer.
313        unsafe {
314            sys::unmap(self.base, self.capacity, self.align);
315        }
316    }
317}
318
319/// The transparent-huge-page size assumed for region alignment. Linux x86-64
320/// and aarch64 (4 KiB base pages) both use 2 MiB; if a platform differs, the
321/// alignment is merely unhelpful, never wrong.
322pub(crate) const HUGE_PAGE: usize = 2 << 20;
323
324/// Releases the physical pages of the page-aligned subrange of
325/// `[ptr, ptr + len)`, keeping the virtual range mapped.
326///
327/// # Safety
328///
329/// The range must lie within a live mapping exclusively owned by the caller,
330/// with no outstanding references into it. After the call the range's contents
331/// are undefined: Linux replaces them with zero pages, but other systems
332/// (macOS in particular) may keep the old bytes resident, so callers must
333/// fully overwrite the range before reading it again.
334pub(crate) unsafe fn dontneed(ptr: *mut u8, len: usize) {
335    // SAFETY: forwarding this function's contract.
336    unsafe { sys::advise(ptr, len, sys::Advice::DontNeed) }
337}
338
339/// Hints the kernel to reclaim the page-aligned subrange of `[ptr, ptr + len)`
340/// immediately, writing it to the swap device. Contents are preserved; this is
341/// a non-destructive hint. No-op outside Linux.
342pub(crate) fn pageout(ptr: *mut u8, len: usize) {
343    // SAFETY: the advice is a contents-preserving hint, so the only
344    // obligation is that the range lies in a live mapping, which callers
345    // guarantee by passing a live slot or extent allocation.
346    unsafe { sys::advise(ptr, len, sys::Advice::PageOut) }
347}
348
349/// Hints the kernel to fault the page-aligned subrange of `[ptr, ptr + len)`
350/// back in ahead of need: asynchronous swap-in, the swap-backed extent store's
351/// readahead mechanism. Contents are preserved. No-op outside Linux.
352pub(crate) fn willneed(ptr: *mut u8, len: usize) {
353    // SAFETY: as in `pageout`, a contents-preserving hint over a live
354    // mapping.
355    unsafe { sys::advise(ptr, len, sys::Advice::WillNeed) }
356}
357
358/// Opts the page-aligned subrange of `[ptr, ptr + len)` out of transparent
359/// huge pages, so reclaim advice over the range operates on base pages and
360/// never needs a folio split. Contents are preserved. No-op outside Linux.
361pub(crate) fn nohugepage(ptr: *mut u8, len: usize) {
362    // SAFETY: as in `pageout`, a contents-preserving hint over a live
363    // mapping.
364    unsafe { sys::advise(ptr, len, sys::Advice::NoHugePage) }
365}
366
367/// Excludes the page-aligned subrange of `[ptr, ptr + len)` from core dumps.
368/// Contents are preserved. No-op outside Linux.
369pub(crate) fn dontdump(ptr: *mut u8, len: usize) {
370    // SAFETY: as in `pageout`, a contents-preserving hint over a live
371    // mapping.
372    unsafe { sys::advise(ptr, len, sys::Advice::DontDump) }
373}
374
375/// The system page size.
376pub(crate) fn page_size() -> usize {
377    sys::page_size()
378}
379
380/// Whether every page of the page-aligned subrange of `[ptr, ptr + len)`
381/// has been unmapped from this process, per the pagemap present bits: the
382/// observation the pageout ledger trusts instead of the reclaim advice's
383/// return value. A page unmapped to a swap entry counts as reclaimed even
384/// while its clean copy lingers in the kernel's swap cache. Errs toward
385/// `false` (resident) when the observation is unavailable. In test builds
386/// the answer comes from the `fake_residency` seam instead of the platform.
387pub(crate) fn nonresident(ptr: *mut u8, len: usize) -> bool {
388    #[cfg(test)]
389    {
390        let _ = (ptr, len);
391        fake_residency::observe()
392    }
393    #[cfg(not(test))]
394    sys::nonresident(ptr, len)
395}
396
397/// Test seam over the pageout residency observation. The decline queue is
398/// thread-local because observation runs on whichever thread enforces the
399/// compressed cap, so tests drive enforcement inline on their own thread.
400#[cfg(test)]
401pub(crate) mod fake_residency {
402    use std::cell::Cell;
403
404    thread_local! {
405        static DECLINES: Cell<u64> = const { Cell::new(0) };
406    }
407
408    /// Makes the next `n` observations on this thread report pages still
409    /// resident, modeling a kernel that declined the reclaim advice.
410    /// Replaces any previously queued declines.
411    pub(crate) fn decline_next(n: u64) {
412        DECLINES.with(|d| d.set(n));
413    }
414
415    /// One observation: consumes a queued decline (reporting the range
416    /// still resident), or reports it fully nonresident.
417    pub(super) fn observe() -> bool {
418        DECLINES.with(|d| {
419            let n = d.get();
420            if n > 0 {
421                d.set(n - 1);
422                false
423            } else {
424                true
425            }
426        })
427    }
428}
429
430/// The largest `page`-aligned subrange of `[addr, addr + len)`, as a
431/// `(byte offset from addr, subrange length)` pair, or `None` when the range
432/// covers no whole page (including on address-space overflow). `page` must
433/// be a power of two.
434///
435/// Guarantees, relied on by [`sys::advise`] for pointer arithmetic and
436/// proved by the Kani harnesses: `offset <= len`, `offset + sub_len <= len`,
437/// and both `addr + offset` and `sub_len` are `page`-aligned.
438#[cfg_attr(miri, allow(dead_code))]
439fn aligned_subrange(addr: usize, len: usize, page: usize) -> Option<(usize, usize)> {
440    crate::soft_assert_no_log!(page.is_power_of_two());
441    let start = addr.checked_add(page - 1)? & !(page - 1);
442    let end = addr.checked_add(len)? & !(page - 1);
443    (start < end).then(|| (start - addr, end - start))
444}
445
446/// Splits an over-mapped range of `map_len` bytes at `addr` into
447/// `(head, tail)` trim amounts such that discarding `head` bytes from the
448/// front and `tail` from the back leaves an `align`-aligned range of exactly
449/// `len` bytes. `None` on address-space overflow or when the range cannot
450/// fit an aligned `len` bytes.
451///
452/// When `addr` and `len` are page-aligned and `align` is a page-multiple
453/// power of two, `head` and `tail` are page-aligned (so both trims are
454/// unmappable) — proved by the Kani harnesses.
455#[cfg_attr(miri, allow(dead_code))]
456fn align_trim(addr: usize, map_len: usize, len: usize, align: usize) -> Option<(usize, usize)> {
457    crate::soft_assert_no_log!(align.is_power_of_two());
458    let aligned = addr.checked_next_multiple_of(align)?;
459    let head = aligned - addr;
460    let tail = map_len.checked_sub(head.checked_add(len)?)?;
461    Some((head, tail))
462}
463
464/// The platform seam: mapping, unmapping, paging advice, and the page size.
465///
466/// The `mmap` variant is production; the Miri variant backs regions with the
467/// Rust heap and treats every advice as a contents-preserving no-op — the
468/// weakest behavior the advice contracts allow — so the pool's tests run
469/// under the interpreter with full provenance and data-race checking:
470///
471/// ```text
472/// MIRIFLAGS=-Zmiri-disable-isolation cargo +nightly miri test -p mz-ore --features pool pool::
473/// ```
474///
475/// (The isolation flag is for the test harness's wall-clock log timestamps,
476/// not for anything the pool does.)
477#[cfg(not(miri))]
478mod sys {
479    use std::io;
480
481    use super::{align_trim, aligned_subrange};
482
483    /// Paging advice, in the vocabulary the pool needs.
484    pub(super) enum Advice {
485        /// Release physical pages; contents become undefined.
486        DontNeed,
487        /// Reclaim to the swap device now; contents preserved.
488        PageOut,
489        /// Fault back in ahead of need; contents preserved.
490        WillNeed,
491        /// Exclude from transparent huge pages; contents preserved.
492        NoHugePage,
493        /// Exclude from core dumps; contents preserved.
494        DontDump,
495    }
496
497    /// Maps `len` bytes of anonymous memory with the base aligned to
498    /// `align`. `len` must be a whole number of pages and `align` a
499    /// page-multiple power of two. Alignments beyond one page over-map and
500    /// trim; huge-page alignments additionally advise `MADV_HUGEPAGE`
501    /// (best-effort; the kernel falls back to base pages under
502    /// fragmentation).
503    pub(super) fn map(len: usize, align: usize) -> io::Result<*mut u8> {
504        let page = page_size();
505        crate::soft_assert_no_log!(len % page == 0 && align % page == 0);
506        #[cfg(target_os = "linux")]
507        let flags = libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE;
508        #[cfg(not(target_os = "linux"))]
509        let flags = libc::MAP_PRIVATE | libc::MAP_ANONYMOUS;
510
511        let map_len = if align > page { len + align } else { len };
512        // SAFETY: anonymous mapping with a null hint; the kernel picks a
513        // fresh range that aliases no existing Rust object. `map_len` is
514        // positive and page-aligned by construction.
515        let raw = unsafe {
516            libc::mmap(
517                std::ptr::null_mut(),
518                map_len,
519                libc::PROT_READ | libc::PROT_WRITE,
520                flags,
521                -1,
522                0,
523            )
524        };
525        if raw == libc::MAP_FAILED {
526            return Err(io::Error::last_os_error());
527        }
528        let raw = raw.cast::<u8>();
529        if align <= page {
530            return Ok(raw);
531        }
532
533        // Trim the over-mapped head and tail so the base is aligned and the
534        // region owns exactly `len` bytes; `unmap` releases that range.
535        let Some((head, tail)) = align_trim(raw.addr(), map_len, len, align) else {
536            // Address-space arithmetic overflowed; treat the reservation as
537            // failed rather than keep an unaligned mapping.
538            // SAFETY: unmapping the mapping created above, in full.
539            unsafe { libc::munmap(raw.cast::<libc::c_void>(), map_len) };
540            return Err(io::Error::from(io::ErrorKind::OutOfMemory));
541        };
542        // SAFETY: `head` and `tail` are page-aligned subranges of the
543        // mapping just created (`align_trim`'s contract with page-aligned
544        // inputs), disjoint from the `len` bytes the region keeps; nothing
545        // references them.
546        unsafe {
547            if head > 0 {
548                libc::munmap(raw.cast::<libc::c_void>(), head);
549            }
550            if tail > 0 {
551                libc::munmap(raw.add(head + len).cast::<libc::c_void>(), tail);
552            }
553        }
554        // SAFETY: `head` stays within the original mapping.
555        let base = unsafe { raw.add(head) };
556
557        #[cfg(target_os = "linux")]
558        if align >= super::HUGE_PAGE {
559            // SAFETY: `base`/`len` describe the live aligned mapping; the
560            // advice is a non-destructive hint and failure is ignorable.
561            unsafe {
562                libc::madvise(base.cast::<libc::c_void>(), len, libc::MADV_HUGEPAGE);
563            }
564        }
565        Ok(base)
566    }
567
568    /// Releases a mapping returned by [`map`].
569    ///
570    /// # Safety
571    ///
572    /// `ptr`, `len`, and `align` must describe exactly one prior [`map`]
573    /// result, with no outstanding references into the range.
574    pub(super) unsafe fn unmap(ptr: *mut u8, len: usize, _align: usize) {
575        // SAFETY: per the function contract.
576        unsafe {
577            libc::munmap(ptr.cast::<libc::c_void>(), len);
578        }
579    }
580
581    /// Applies `advice` to the largest page-aligned subrange of
582    /// `[ptr, ptr + len)`, rounding the start up and the end down so the
583    /// advice never spills onto pages the range only partially covers.
584    ///
585    /// # Safety
586    ///
587    /// The range must lie within a live mapping. For [`Advice::DontNeed`]
588    /// the caller must additionally uphold the exclusivity contract
589    /// documented on [`super::dontneed`]; the remaining advice values are
590    /// non-mutating hints.
591    pub(super) unsafe fn advise(ptr: *mut u8, len: usize, advice: Advice) {
592        let advice = match advice {
593            Advice::DontNeed => libc::MADV_DONTNEED,
594            #[cfg(target_os = "linux")]
595            Advice::PageOut => libc::MADV_PAGEOUT,
596            #[cfg(target_os = "linux")]
597            Advice::WillNeed => libc::MADV_WILLNEED,
598            #[cfg(target_os = "linux")]
599            Advice::NoHugePage => libc::MADV_NOHUGEPAGE,
600            #[cfg(target_os = "linux")]
601            Advice::DontDump => libc::MADV_DONTDUMP,
602            // Reclaim, prefetch, THP, and dump hints have no portable
603            // equivalent.
604            #[cfg(not(target_os = "linux"))]
605            Advice::PageOut | Advice::WillNeed | Advice::NoHugePage | Advice::DontDump => return,
606        };
607        let Some((offset, sub_len)) = aligned_subrange(ptr.addr(), len, page_size()) else {
608            return;
609        };
610        // SAFETY: `offset <= len` (`aligned_subrange`'s contract), so the
611        // add stays within the caller's range and preserves provenance.
612        let aligned = unsafe { ptr.byte_add(offset) }.cast::<libc::c_void>();
613        // SAFETY: pointer and length describe a fully page-aligned subrange
614        // of the caller's live mapping; destructive advice is covered by the
615        // function contract.
616        unsafe {
617            libc::madvise(aligned, sub_len, advice);
618        }
619    }
620
621    /// Whether every page of the page-aligned subrange of `[ptr, ptr + len)`
622    /// has been unmapped from this process, per the present bits in
623    /// `/proc/self/pagemap`. A failed observation reports `false`: the
624    /// ledger keeps counting pages it cannot prove gone.
625    ///
626    /// The present bit is the signal, deliberately not `mincore(2)`:
627    /// successful asynchronous reclaim unmaps the PTE to a swap entry while
628    /// the page's clean copy lingers in the kernel's swap cache, and mincore
629    /// reports swap-cache pages as in core. Observing in-core-ness would
630    /// therefore misclassify essentially every successful pageout on an
631    /// unpressured host (the retry advice cannot change the outcome either,
632    /// since the advice skips already-unmapped PTEs). An unmapped page is
633    /// reclaimed for this ledger's purposes: a clean swap-cache copy is
634    /// memory the kernel drops for free.
635    ///
636    /// Only Linux answers from the kernel. Elsewhere the reclaim advice is
637    /// compiled out, so there is no reclaim to observe and the answer is
638    /// `true`, keeping the compressed tier cycling on development platforms.
639    #[cfg_attr(test, allow(dead_code))]
640    pub(super) fn nonresident(ptr: *mut u8, len: usize) -> bool {
641        #[cfg(target_os = "linux")]
642        {
643            use std::cell::RefCell;
644            use std::os::unix::fs::FileExt;
645            use std::sync::OnceLock;
646
647            // Reading a process's own pagemap needs no privilege since
648            // Linux 4.2: unprivileged readers see zeroed frame numbers,
649            // which this probe never looks at.
650            static PAGEMAP: OnceLock<Option<std::fs::File>> = OnceLock::new();
651            let Some(pagemap) = PAGEMAP
652                .get_or_init(|| std::fs::File::open("/proc/self/pagemap").ok())
653                .as_ref()
654            else {
655                return false;
656            };
657            let page = page_size();
658            let Some((offset, sub_len)) = aligned_subrange(ptr.addr(), len, page) else {
659                // No whole page to observe: vacuously nonresident.
660                return true;
661            };
662            let first_page = (ptr.addr() + offset) / page;
663            let pages = sub_len / page;
664            thread_local! {
665                static SCRATCH: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
666            }
667            SCRATCH.with(|scratch| {
668                let mut buf = scratch.borrow_mut();
669                buf.clear();
670                buf.resize(pages, 0);
671                let file_offset = u64::try_from(first_page).expect("page index fits u64") * 8;
672                if pagemap
673                    .read_exact_at(bytemuck::cast_slice_mut(buf.as_mut_slice()), file_offset)
674                    .is_err()
675                {
676                    return false;
677                }
678                // One native-endian u64 per page, per the kernel ABI
679                // (Documentation/admin-guide/mm/pagemap.rst); bit 63 is
680                // "present in RAM". A swap-entry PTE (bit 62) and a
681                // never-faulted zero entry are both out of memory.
682                buf.iter().all(|entry| entry & (1 << 63) == 0)
683            })
684        }
685        #[cfg(not(target_os = "linux"))]
686        {
687            let _ = (ptr, len);
688            true
689        }
690    }
691
692    pub(super) fn page_size() -> usize {
693        // SAFETY: `sysconf` with a valid argument is safe.
694        let raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
695        let page = usize::try_from(raw).expect("page size is positive and fits usize");
696        // The alignment arithmetic (`aligned_subrange`, `align_trim`) masks
697        // with `page - 1`; a non-power-of-two page would mis-round silently.
698        assert!(page.is_power_of_two(), "page size is a power of two");
699        page
700    }
701}
702
703#[cfg(miri)]
704mod sys {
705    use std::alloc::Layout;
706    use std::io;
707
708    pub(super) enum Advice {
709        DontNeed,
710        PageOut,
711        WillNeed,
712        NoHugePage,
713        DontDump,
714    }
715
716    pub(super) fn map(len: usize, align: usize) -> io::Result<*mut u8> {
717        let layout = Layout::from_size_align(len, align).expect("valid region layout");
718        // SAFETY: `len` is positive (empty regions never map). The memory is
719        // deliberately left uninitialized, matching the slot contract that
720        // contents are unspecified until fully overwritten; Miri enforces
721        // that no path reads a byte before writing it.
722        let ptr = unsafe { std::alloc::alloc(layout) };
723        if ptr.is_null() {
724            std::alloc::handle_alloc_error(layout);
725        }
726        Ok(ptr)
727    }
728
729    pub(super) unsafe fn unmap(ptr: *mut u8, len: usize, align: usize) {
730        let layout = Layout::from_size_align(len, align).expect("valid region layout");
731        // SAFETY: `ptr` was returned by `map` with exactly this layout.
732        unsafe { std::alloc::dealloc(ptr, layout) }
733    }
734
735    /// Advice is a contents-preserving no-op: the weakest behavior the
736    /// contracts allow (`DontNeed` leaves contents undefined, and "old bytes
737    /// kept" is one of the permitted outcomes, as on macOS).
738    pub(super) unsafe fn advise(_ptr: *mut u8, _len: usize, _advice: Advice) {}
739
740    /// The heap backing has no residency to observe, so reclaim advice is
741    /// treated as fully effective, mirroring the non-Linux answer.
742    #[cfg_attr(test, allow(dead_code))]
743    pub(super) fn nonresident(_ptr: *mut u8, _len: usize) -> bool {
744        true
745    }
746
747    pub(super) fn page_size() -> usize {
748        4096
749    }
750}
751
752/// Kani proof harnesses over the pure arithmetic and the slot allocator:
753/// the sequential facts the pool's unsafe blocks rest on. Run
754/// `cargo kani --features pool` from `src/ore`; each harness is exhaustive
755/// over its symbolic inputs. Kani ships its own toolchain, so it needs a
756/// release whose rustc is at or above the workspace `rust-version` (or the
757/// manifest check bypassed for the run). Concurrency-justified claims (the
758/// lock-protocol aliasing arguments in `pool.rs`) are outside Kani's
759/// sequential model and are exercised under Miri instead.
760#[cfg(kani)]
761mod proofs {
762    use super::*;
763
764    /// `aligned_subrange`'s contract: the subrange lies within the input
765    /// range, is nonempty, and is page-aligned at both ends.
766    #[kani::proof]
767    fn aligned_subrange_stays_in_bounds() {
768        let addr: usize = kani::any();
769        let len: usize = kani::any();
770        let shift: u32 = kani::any();
771        kani::assume(shift < usize::BITS);
772        let page = 1usize << shift;
773        if let Some((offset, sub_len)) = aligned_subrange(addr, len, page) {
774            assert!(offset <= len);
775            assert!(sub_len <= len - offset);
776            assert!(sub_len > 0);
777            assert!((addr + offset) % page == 0);
778            assert!(sub_len % page == 0);
779        }
780    }
781
782    /// `align_trim`'s contract: head and tail partition the over-map
783    /// exactly around an aligned range of the requested length, and with
784    /// page-aligned inputs both trims are page-aligned, so each can be
785    /// unmapped independently.
786    #[kani::proof]
787    fn align_trim_partitions_the_overmap() {
788        let addr: usize = kani::any();
789        let len: usize = kani::any();
790        let page_shift: u32 = kani::any();
791        let align_shift: u32 = kani::any();
792        kani::assume(page_shift <= align_shift && align_shift < usize::BITS - 1);
793        let page = 1usize << page_shift;
794        let align = 1usize << align_shift;
795        kani::assume(addr % page == 0);
796        kani::assume(len % page == 0);
797        let Some(map_len) = len.checked_add(align) else {
798            return;
799        };
800        if let Some((head, tail)) = align_trim(addr, map_len, len, align) {
801            assert_eq!(head + len + tail, map_len);
802            assert!(head < align);
803            assert!(head % page == 0);
804            assert!(tail % page == 0);
805            assert!((addr + head) % align == 0);
806        }
807    }
808
809    /// The slot allocator, over every alloc/free sequence of a bounded
810    /// length: an allocated slot is always in range and never aliases a
811    /// live one. This is the disjointness fact `slot_ptr`'s callers turn
812    /// into non-aliasing slices.
813    #[kani::proof]
814    #[kani::unwind(8)]
815    fn slot_allocator_hands_out_disjoint_slots() {
816        const MAX: u32 = 3;
817        let mut slots = SlotAllocator::new(MAX);
818        let mut live = [false; MAX as usize];
819        for _ in 0..5 {
820            if kani::any() {
821                if let Some((slot, _warm)) = slots.alloc() {
822                    let slot = usize::try_from(slot).expect("fits");
823                    assert!(slot < live.len(), "slot out of range");
824                    assert!(!live[slot], "live slot handed out twice");
825                    live[slot] = true;
826                }
827            } else {
828                let slot: usize = kani::any();
829                kani::assume(slot < live.len());
830                if live[slot] {
831                    live[slot] = false;
832                    slots.free(u32::try_from(slot).expect("fits"), kani::any());
833                }
834            }
835        }
836    }
837
838    /// The class-selection contract the pool turns into slice bounds: a
839    /// selected class always fits the payload.
840    #[kani::proof]
841    #[kani::unwind(10)]
842    fn selected_class_fits_payload() {
843        let len_bytes: usize = kani::any();
844        if let Some(class) = size_class_for(len_bytes) {
845            assert!(class < SIZE_CLASSES.len());
846            assert!(SIZE_CLASSES[class] >= len_bytes);
847        } else {
848            assert!(len_bytes > SIZE_CLASSES[SIZE_CLASSES.len() - 1]);
849        }
850    }
851
852    /// The offset arithmetic behind `slot_ptr`: for every size class and
853    /// every slot index the allocator can hand out, the slot lies within
854    /// the region's capacity.
855    #[kani::proof]
856    #[kani::unwind(9)]
857    fn slot_offsets_lie_within_capacity() {
858        for &class_size in &SIZE_CLASSES {
859            let capacity_bytes: usize = kani::any();
860            kani::assume(capacity_bytes <= 1 << 40);
861            let capacity = capacity_bytes - capacity_bytes % class_size;
862            let max_slots = capacity / class_size;
863            let slot: usize = kani::any();
864            kani::assume(slot < max_slots);
865            let offset = slot * class_size;
866            assert!(offset + class_size <= capacity);
867        }
868    }
869}
870
871#[cfg(test)]
872mod tests {
873    use super::*;
874
875    #[mz_ore::test]
876    fn alloc_free_reuses_slots() {
877        let region = Region::new(64 << 10, 1 << 20).expect("mmap");
878        let (a, warm_a) = region.alloc().expect("slot");
879        let (b, _) = region.alloc().expect("slot");
880        assert!(!warm_a, "bump slots are not warm");
881        assert_ne!(a, b);
882        assert_ne!(region.slot_ptr(a), region.slot_ptr(b));
883        let ptr_a = region.slot_ptr(a);
884        // A warm free is preferred by the next alloc and reported warm.
885        region.free(a, true);
886        let (c, warm_c) = region.alloc().expect("slot");
887        assert_eq!(c, a);
888        assert!(warm_c);
889        assert_eq!(region.slot_ptr(c), ptr_a);
890        // A cold free comes back, but not warm.
891        region.free(c, false);
892        let (d, warm_d) = region.alloc().expect("slot");
893        assert_eq!(d, a);
894        assert!(!warm_d);
895    }
896
897    /// Hugepage-class regions get a huge-page-aligned base, so slots tile
898    /// huge-page boundaries exactly.
899    #[mz_ore::test]
900    fn hugepage_class_base_is_aligned() {
901        let region = Region::new(2 << 20, 16 << 20).expect("mmap");
902        let (slot, _) = region.alloc().expect("slot");
903        if cfg!(target_os = "linux") {
904            assert_eq!(
905                region.slot_ptr(slot).addr() % HUGE_PAGE,
906                0,
907                "hugepage-class slots must be huge-page aligned",
908            );
909        }
910    }
911
912    #[mz_ore::test]
913    fn exhaustion_returns_none() {
914        let region = Region::new(64 << 10, 128 << 10).expect("mmap");
915        assert!(region.alloc().is_some());
916        assert!(region.alloc().is_some());
917        assert!(region.alloc().is_none(), "third slot exceeds capacity");
918    }
919
920    #[mz_ore::test]
921    fn slots_are_writable_and_advice_is_accepted() {
922        let region = Region::new(64 << 10, 1 << 20).expect("mmap");
923        let (slot, _) = region.alloc().expect("slot");
924        let ptr = region.slot_ptr(slot);
925        // SAFETY: freshly allocated slot, exclusively owned by this test.
926        unsafe {
927            std::ptr::write_bytes(ptr, 0xAB, region.class_size());
928        }
929        pageout(ptr, region.class_size());
930        willneed(ptr, region.class_size());
931        // SAFETY: the slot is exclusively owned and is not read again before
932        // being overwritten (it is not read again at all).
933        unsafe {
934            dontneed(ptr, region.class_size());
935        }
936    }
937
938    #[mz_ore::test]
939    fn aligned_subrange_agrees_with_examples() {
940        // Fully aligned range: identity.
941        assert_eq!(aligned_subrange(4096, 8192, 4096), Some((0, 8192)));
942        // Unaligned start rounds up, unaligned end rounds down.
943        assert_eq!(aligned_subrange(4097, 8192, 4096), Some((4095, 4096)));
944        // Too short to cover a whole page.
945        assert_eq!(aligned_subrange(4097, 4096, 4096), None);
946        assert_eq!(aligned_subrange(0, 0, 4096), None);
947    }
948
949    #[mz_ore::test]
950    fn align_trim_agrees_with_examples() {
951        // Already aligned: no head, tail is the whole over-map.
952        assert_eq!(
953            align_trim(HUGE_PAGE, 3 * HUGE_PAGE, 2 * HUGE_PAGE, HUGE_PAGE),
954            Some((0, HUGE_PAGE))
955        );
956        // Unaligned base: head consumes the misalignment.
957        assert_eq!(
958            align_trim(HUGE_PAGE + 4096, 3 * HUGE_PAGE, 2 * HUGE_PAGE, HUGE_PAGE),
959            Some((HUGE_PAGE - 4096, 4096)),
960        );
961    }
962}