use rusty_alloc::init::{self, HeapBox};
use rusty_alloc::{alloc, arena, options};
unsafe fn hmalloc(hb: *mut HeapBox, n: usize) -> *mut u8 {
unsafe { alloc::heap_malloc(hb, n) }
}
#[test]
fn heaps_arenas_subprocs_options() {
let h1 = init::create_heap(7, false, -1);
let mut blocks = Vec::new();
unsafe {
for i in 0..500usize {
let p = hmalloc(h1, 32 + (i % 900));
assert!(!p.is_null());
core::ptr::write_bytes(p, 0x77, 32 + (i % 900));
blocks.push((p, 32 + (i % 900)));
}
for &(p, _) in blocks.iter().take(100) {
alloc::free(p);
}
let mut seen = 0usize;
let ok = (*(*h1).heap.get()).visit_blocks(true, &mut |_area, block, _sz| {
if !block.is_null() {
seen += 1;
}
true
});
assert!(ok);
assert_eq!(seen, 400, "visitor missed blocks");
let (probe, _) = blocks[200];
assert!(alloc::heap_contains_block(h1, probe));
assert!(alloc::heap_check_owned(h1, probe));
let foreign = alloc::malloc(64);
assert!(!alloc::heap_contains_block(h1, foreign));
alloc::free(foreign);
init::heap_delete(h1);
for &(p, n) in blocks.iter().skip(100) {
assert_eq!(p.read(), 0x77);
assert_eq!(p.add(n - 1).read(), 0x77);
alloc::free(p); }
}
let h2 = init::create_heap(0, true, -1);
unsafe {
for i in 0..300usize {
let p = hmalloc(h2, 64 + i * 7 % 2000);
assert!(!p.is_null());
}
init::heap_destroy(h2);
}
let sanity = alloc::malloc(128);
assert!(!sanity.is_null());
unsafe { alloc::free(sanity) };
let h3 = init::create_heap(0, true, -1);
unsafe {
let prev = init::set_default_heap(h3);
let p = alloc::malloc(64); assert!(alloc::heap_contains_block(h3, p));
alloc::free(p);
init::set_default_heap(prev);
assert_eq!(init::backing_heap(), prev);
init::heap_destroy(h3);
}
let arena_bytes = (512 * 1024usize)
.div_ceil(rusty_alloc::types::SEGMENT_SIZE)
.max(2)
* rusty_alloc::types::SEGMENT_SIZE;
let arena_id =
arena::reserve_os_memory_ex(arena_bytes, true, false, true).expect("arena reserve failed");
let (abase, asize) = arena::arena_area(arena_id);
assert!(!abase.is_null() && asize == arena_bytes);
let ha = init::create_heap(0, true, arena_id);
unsafe {
let s0 = (*(*ha).heap.get()).stats.segments;
let p = hmalloc(ha, 100_000);
assert!(!p.is_null());
let addr = p.addr();
assert!(
addr >= abase.addr() && addr < abase.addr() + asize,
"exclusive-arena heap allocated outside its arena"
);
assert!((*(*ha).heap.get()).stats.segments > s0);
alloc::free(p);
init::heap_destroy(ha);
}
let hb2 = init::create_heap(0, true, arena_id);
unsafe {
let p = hmalloc(hb2, 4096);
assert!(!p.is_null());
assert!(p.addr() >= abase.addr() && p.addr() < abase.addr() + asize);
let z = alloc::heap_zalloc(hb2, 200_000);
for i in (0..200_000).step_by(4096) {
assert_eq!(z.add(i).read(), 0, "dirty arena chunk leaked at +{i}");
}
alloc::free(z);
alloc::free(p);
init::heap_destroy(hb2);
}
if cfg!(miri) {
return;
}
let sp = init::subproc_new();
let handle = std::thread::spawn(move || {
init::subproc_add_current_thread(sp);
let p = alloc::malloc(5000);
assert!(!p.is_null());
unsafe { core::ptr::write_bytes(p, 0x3C, 5000) };
p as usize
});
let leaked = handle.join().unwrap();
let before = alloc::stats().reclaims;
let churn: Vec<Vec<u8>> = (0..100).map(|_| vec![1u8; 40_000]).collect();
drop(churn);
let _ = before; let mut found = 0usize;
init::abandoned_visit_blocks(sp, -1, true, &mut |_a, b, _s| {
if !b.is_null() {
found += 1;
}
true
});
assert!(found >= 1, "abandoned block not visible in its subproc");
unsafe { alloc::free(leaked as *mut u8) };
assert_eq!(options::get(15), -1, "purge_delay default");
options::set(15, 0);
assert_eq!(options::get(15), 0);
assert!(
options::get_size(23) >= 1024 * 1024 * 1024,
"arena_reserve KiB scaling"
);
#[cfg(debug_assertions)]
{
let merged = rusty_alloc::stats::merged();
assert!(merged.allocs > 0);
}
let (_, _, _, rss, ..) = rusty_alloc::stats::process_info();
assert!(rss > 0, "process_info rss");
}
#[test]
fn exclusive_arena_confines_huge_allocations() {
use rusty_alloc::types::LARGE_OBJ_SIZE_MAX;
let arena_bytes = 256 * 1024 * 1024;
let Ok(arena_id) = arena::reserve_os_memory_ex(arena_bytes, true, false, true) else {
eprintln!("skipped: could not reserve a {arena_bytes}-byte exclusive arena");
return;
};
let (abase, asize) = arena::arena_area(arena_id);
assert!(!abase.is_null());
let ha = init::create_heap(0, true, arena_id);
let huge = LARGE_OBJ_SIZE_MAX + 1;
unsafe {
let s0 = (*(*ha).heap.get()).stats.segments;
let p = hmalloc(ha, huge);
assert!(
!p.is_null(),
"exclusive-arena heap could not serve {huge} bytes"
);
assert!(
p.addr() >= abase.addr() && p.addr() < abase.addr() + asize,
"huge block at {:#x} escaped its exclusive arena [{:#x}, {:#x})",
p.addr(),
abase.addr(),
abase.addr() + asize
);
core::ptr::write_bytes(p, 0xC3, huge);
assert_eq!(*p, 0xC3);
assert_eq!(*p.add(huge - 1), 0xC3);
let st = (*(*ha).heap.get()).stats;
assert!(
st.segments > s0,
"huge allocation did not count a segment ({} -> {})",
s0,
st.segments
);
assert!(
st.segments >= st.segments_freed,
"more segments freed ({}) than allocated ({})",
st.segments_freed,
st.segments
);
alloc::free(p);
}
}
#[test]
fn arena_bitmap_reaches_past_its_first_word() {
use rusty_alloc::types::{LARGE_OBJ_SIZE_MAX, SEGMENT_SIZE};
const CHUNKS: usize = 33;
let want = CHUNKS * SEGMENT_SIZE;
let Ok(arena_id) = arena::reserve_os_memory_ex(want, true, false, true) else {
eprintln!("skipped: could not reserve {want} bytes for a {CHUNKS}-chunk arena");
return;
};
let (abase, asize) = arena::arena_area(arena_id);
assert!(!abase.is_null() && asize >= want);
let ha = init::create_heap(0, true, arena_id);
let one_chunk = LARGE_OBJ_SIZE_MAX;
let mut blocks = Vec::new();
unsafe {
for i in 0..CHUNKS {
let p = hmalloc(ha, one_chunk);
assert!(
!p.is_null(),
"chunk {i} of {CHUNKS} refused — the bitmap scan stopped at word \
{} of {}",
i / 32,
CHUNKS.div_ceil(32)
);
assert!(
p.addr() >= abase.addr() && p.addr() < abase.addr() + asize,
"chunk {i} escaped the arena"
);
blocks.push(p);
}
for p in blocks {
alloc::free(p);
}
}
}
#[test]
fn collect_reclaims_a_bins_last_page() {
for force in [false, true] {
let h = init::create_heap(0, true, -1);
unsafe {
let p = hmalloc(h, 1536);
assert!(!p.is_null(), "fresh heap served a 1536-byte block");
alloc::free(p);
let before = (*(*h).heap.get()).stats.pages_retired;
alloc::heap_collect(h, force);
let after = (*(*h).heap.get()).stats.pages_retired;
assert!(
after > before,
"collect(force={force}) must reclaim an all-free page even when \
it is the bin's only one (retired {before} -> {after})"
);
init::heap_destroy(h);
}
}
}
#[test]
fn generic_collect_fires_on_its_own() {
let prev = options::get(options::GENERIC_COLLECT);
options::set(options::GENERIC_COLLECT, 8);
let h = init::create_heap(0, true, -1);
unsafe {
let p = hmalloc(h, 1536);
assert!(!p.is_null());
alloc::free(p);
let before = (*(*h).heap.get()).stats.pages_retired;
let g0 = (*(*h).heap.get()).stats.generic;
for i in 0..64usize {
let q = hmalloc(h, 24 + i * 8);
if !q.is_null() {
alloc::free(q);
}
}
let trips = (*(*h).heap.get()).stats.generic - g0;
assert!(
trips > 8,
"precondition: the loop must actually take the generic path more often than the threshold (took it {trips} times)"
);
let after = (*(*h).heap.get()).stats.pages_retired;
assert!(
after > before,
"the periodic collect must retire pages with no explicit call \
(retired {before} -> {after})"
);
init::heap_destroy(h);
}
options::set(options::GENERIC_COLLECT, prev);
}
#[cfg(ra_small_profile)]
#[test]
fn generic_path_reclaims_before_returning_null() {
let bytes = 2 * rusty_alloc::types::SEGMENT_SIZE;
let Ok(arena) = arena::reserve_os_memory_ex(bytes, true, false, true) else {
return; };
let h = init::create_heap(0, true, arena);
unsafe {
let mut held = Vec::new();
for i in 0..24usize {
let p = hmalloc(h, 16 + i * 16);
if !p.is_null() {
held.push(p);
}
}
let touched = held.len();
for p in held.drain(..) {
alloc::free(p);
}
assert!(
touched >= 16,
"precondition: the arena must actually hold the cached pages that \
starve the retry (only {touched} classes were served)"
);
let mut ptrs = Vec::new();
for _ in 0..512 {
let p = hmalloc(h, 512);
if p.is_null() {
break;
}
ptrs.push(p);
}
let served = ptrs.len();
for p in ptrs {
alloc::free(p);
}
assert!(
served > 192,
"a heap holding {touched} idle cached pages must reclaim them rather \
than report OOM (served only {served} blocks of 512 B)"
);
init::heap_destroy(h);
}
}