use crate::chunk::{CHUNK, ChunkHead, KIND_HUGE, head_of};
use crate::lock::SpinLock;
use crate::os;
use crate::prof::Sample;
use crate::stats;
use core::ptr::{self, null_mut};
use std::time::{Duration, Instant};
#[repr(C)]
pub struct HugeHead {
pub head: ChunkHead,
pub map_base: *mut u8,
pub map_len: usize,
pub usable: usize,
pub sample: *mut Sample,
}
const CACHE_SLOTS: usize = 8;
const CACHE_MAX_BYTES: usize = 64 << 20;
const CACHE_TTL: Duration = Duration::from_secs(10);
struct Cache {
entries: [(*mut HugeHead, Option<Instant>); CACHE_SLOTS],
len: usize,
bytes: usize,
}
static CACHE: SpinLock<Cache> = SpinLock::new(Cache {
entries: [(null_mut(), None); CACHE_SLOTS],
len: 0,
bytes: 0,
});
#[inline]
pub unsafe fn head(ptr: *mut u8) -> *mut HugeHead {
head_of(ptr).cast()
}
pub unsafe fn alloc(size: usize, align: usize, zero: bool, sample: *mut Sample) -> *mut u8 {
let ps = os::page_size();
let offset = ps.max(align);
let usable = os::round_up(size.max(1), ps);
if offset == ps {
let h = cache_take(usable);
if !h.is_null() {
(*h).sample = sample;
(*h).head.sampled = u8::from(!sample.is_null());
let p = core::ptr::with_exposed_provenance_mut::<u8>((*h).map_base.addr() + ps);
if zero {
ptr::write_bytes(p, 0, size);
}
return p;
}
}
let len = offset + usable;
let base = crate::arena::map_or_make_room(len, CHUNK.max(align));
if base.is_null() {
return null_mut();
}
base.expose_provenance();
stats::HUGE_BYTES.add(len);
let p = base.add(offset);
let h = head(p);
ptr::write(
h,
HugeHead {
head: ChunkHead {
kind: KIND_HUGE,
sampled: u8::from(!sample.is_null()),
},
map_base: base,
map_len: len,
usable,
sample,
},
);
p
}
pub unsafe fn free(ptr: *mut u8) {
let h = head(ptr);
let len = (*h).map_len;
if ptr.addr() - (*h).map_base.addr() == os::page_size() && len <= CACHE_MAX_BYTES / 2 {
let mut evict = [null_mut(); CACHE_SLOTS + 1];
let n;
{
let mut cache = CACHE.lock();
let now = Instant::now();
let i = cache.len;
cache.entries[i] = (h, Some(now));
cache.len += 1;
cache.bytes += len;
stats::HUGE_BYTES.sub(len);
stats::HUGE_CACHED_BYTES.add(len);
n = cache.evict(
&mut evict,
|c| c.len == CACHE_SLOTS || c.bytes > CACHE_MAX_BYTES,
now,
);
}
for &h in &evict[..n] {
stats::HUGE_CACHED_BYTES.sub((*h).map_len);
os::unmap((*h).map_base, (*h).map_len);
}
return;
}
stats::HUGE_BYTES.sub(len);
os::unmap((*h).map_base, len);
}
impl Cache {
unsafe fn evict(
&mut self,
out: &mut [*mut HugeHead],
over: impl Fn(&Cache) -> bool,
now: Instant,
) -> usize {
let mut n = 0;
let mut i = 0;
while i < self.len {
let (h, t) = self.entries[i];
if t.is_some_and(|t| now.duration_since(t) >= CACHE_TTL) {
out[n] = h;
n += 1;
self.remove(i);
} else {
i += 1;
}
}
while self.len > 0 && over(self) {
out[n] = self.entries[0].0;
n += 1;
self.remove(0);
}
n
}
unsafe fn remove(&mut self, i: usize) {
self.bytes -= (*self.entries[i].0).map_len;
self.entries.copy_within(i + 1..self.len, i);
self.len -= 1;
}
}
unsafe fn cache_take(usable: usize) -> *mut HugeHead {
let mut cache = CACHE.lock();
let mut best = usize::MAX;
for i in 0..cache.len {
let u = (*cache.entries[i].0).usable;
if u >= usable
&& u - usable <= usable / 4
&& (best == usize::MAX || u < (*cache.entries[best].0).usable)
{
best = i;
}
}
if best == usize::MAX {
return null_mut();
}
let h = cache.entries[best].0;
cache.remove(best);
stats::HUGE_CACHED_BYTES.sub((*h).map_len);
stats::HUGE_BYTES.add((*h).map_len);
h
}
pub unsafe fn resize_in_place(ptr: *mut u8, size: usize) -> bool {
let usable = (*head(ptr)).usable;
size <= usable && size > usable / 2
}
#[cfg(all(unix, not(miri)))]
pub(crate) fn fork_lock() {
CACHE.acquire();
}
#[cfg(all(unix, not(miri)))]
pub(crate) unsafe fn fork_unlock() {
CACHE.release();
}
pub unsafe fn purge() {
release(true, Instant::now());
}
pub unsafe fn decay() {
release(false, Instant::now());
}
unsafe fn release(all: bool, now: Instant) {
let mut evict = [null_mut(); CACHE_SLOTS + 1];
let n = CACHE.lock().evict(&mut evict, |_| all, now);
for &h in &evict[..n] {
stats::HUGE_CACHED_BYTES.sub((*h).map_len);
os::unmap((*h).map_base, (*h).map_len);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cached_mappings_expire() {
unsafe {
let p = alloc(3 << 20, 8, false, null_mut());
assert!(!p.is_null());
free(p);
let cached = |h: *mut HugeHead| {
let cache = CACHE.lock();
cache.entries[..cache.len].iter().any(|e| e.0 == h)
};
let h = head(p);
assert!(cached(h));
release(false, Instant::now());
assert!(cached(h), "kept while fresh");
release(false, Instant::now() + CACHE_TTL);
assert!(!cached(h), "unmapped once its time is up");
}
}
}