#![cfg(all(feature = "alloc-core", feature = "alloc-decommit"))]
use core::alloc::Layout;
use sefer_alloc::AllocCore;
const MIB: usize = 1024 * 1024;
fn layout(mib: usize) -> Layout {
Layout::from_size_align(mib * MIB, 8).unwrap()
}
fn fill_cache(ac: &mut AllocCore, l: Layout, count: usize) -> Option<usize> {
let mut ptrs = [core::ptr::null_mut::<u8>(); 8];
assert!(count <= ptrs.len(), "fill_cache: count > 8");
for ptr in &mut ptrs[..count] {
*ptr = ac.alloc(l);
if (*ptr).is_null() {
for p in &mut ptrs[..count] {
if !p.is_null() {
ac.dealloc(*p, l);
*p = core::ptr::null_mut();
}
}
return None;
}
}
for ptr in &mut ptrs[..count] {
ac.dealloc(*ptr, l);
*ptr = core::ptr::null_mut();
}
Some(ac.dbg_large_cache_used())
}
#[test]
fn decay_releases_excess_over_target() {
let mut ac = AllocCore::new().expect("primordial");
ac.dbg_set_large_cache_budget(None);
ac.dbg_set_decay_config(5000, 0, 0);
let l = layout(4);
let used_before = match fill_cache(&mut ac, l, 2) {
Some(u) if u > 0 => u,
_ => {
eprintln!("OOM or cache empty — skipping decay_releases_excess_over_target");
return;
}
};
ac.dbg_force_decay_tick();
let used_after = ac.dbg_large_cache_used();
assert!(
used_after < used_before,
"decay tick must release some cache: before={used_before}, after={used_after}"
);
}
#[test]
fn decay_respects_headroom() {
let mut ac = AllocCore::new().expect("primordial");
ac.dbg_set_large_cache_budget(None);
let l = layout(4);
let span_size = match fill_cache(&mut ac, l, 1) {
Some(s) if s > 0 => s,
_ => {
eprintln!("OOM — skipping decay_respects_headroom");
return;
}
};
let l_small = layout(4);
let l_large = layout(8); let p1 = ac.alloc(l_small);
let p2 = ac.alloc(l_large);
if p1.is_null() || p2.is_null() {
if !p1.is_null() {
ac.dealloc(p1, l_small);
}
if !p2.is_null() {
ac.dealloc(p2, l_large);
}
eprintln!("OOM — skipping decay_respects_headroom");
return;
}
ac.dealloc(p1, l_small);
ac.dealloc(p2, l_large);
let used_before = ac.dbg_large_cache_used();
if used_before == 0 {
eprintln!("cache empty — skipping decay_respects_headroom");
return;
}
ac.dbg_set_decay_config(10_000, 0, span_size);
for _ in 0..5 {
ac.dbg_force_decay_tick();
}
let used_after = ac.dbg_large_cache_used();
assert!(
used_after < used_before,
"decay must reduce used: before={used_before} after={used_after}"
);
assert!(
used_after <= span_size,
"used_after={used_after} must be <= headroom={span_size}"
);
}
#[test]
fn decay_skips_when_under_target() {
let mut ac = AllocCore::new().expect("primordial");
ac.dbg_set_large_cache_budget(None);
let l = layout(4);
let used = match fill_cache(&mut ac, l, 1) {
Some(s) => s,
None => {
eprintln!("OOM — skipping decay_skips_when_under_target");
return;
}
};
if used == 0 {
return;
}
let big_headroom = used * 10;
ac.dbg_set_decay_config(10_000, 0, big_headroom);
ac.dbg_force_decay_tick();
assert_eq!(
ac.dbg_large_cache_used(),
used,
"decay must be a no-op when cache ({used}) < headroom ({big_headroom})"
);
}
#[test]
fn decay_interval_respected() {
let mut ac = AllocCore::new().expect("primordial");
ac.dbg_set_large_cache_budget(None);
ac.dbg_set_decay_config(10_000, 10_000, 0);
let l = layout(4);
match fill_cache(&mut ac, l, 1) {
Some(s) if s > 0 => s,
_ => {
eprintln!("OOM — skipping decay_interval_respected");
return;
}
};
let used_initial = ac.dbg_large_cache_used();
for _ in 0..50 {
let ptr = ac.alloc(l);
if ptr.is_null() {
break; }
ac.dealloc(ptr, l);
}
let used_after = ac.dbg_large_cache_used();
if used_initial > 0 {
assert!(
used_after > 0,
"cache must not have been fully drained in a tight loop (interval=10s); \
initial={used_initial}, after={used_after}"
);
}
}
#[test]
fn config_decay_rate_percent() {
use sefer_alloc::LargeCacheConfig;
let cfg = LargeCacheConfig::new().decay_rate_percent(25);
let ac = AllocCore::new_with_config(cfg).expect("primordial");
let (rate_bp, _interval_ms, _headroom) = ac.dbg_decay_config();
assert_eq!(
rate_bp, 2500,
"decay_rate_percent(25) must produce 2500 bp; got {rate_bp}"
);
}