use super::super::*;
use mnemosyne_backend::MemoryBackendWrapper as Backend;
use mnemosyne_core::policy::StandardPolicy;
const BLOCK: usize = 64;
#[track_caller]
unsafe fn assert_block_intact(p: *mut u8, stamp: u8, index: usize, stage: &str) {
let actual = unsafe { core::slice::from_raw_parts(p, BLOCK) };
let expected = [stamp; BLOCK];
if actual == expected {
return;
}
let off = actual
.iter()
.position(|&b| b != stamp)
.expect("slices differ, so some byte differs");
panic!(
"{stage} block #{index}: byte {off} is {:#x}, expected {stamp:#x} — the \
segment was released while its blocks were still live",
actual[off]
);
}
fn fill(owner: &mut ThreadAllocator<Backend>, count: usize) -> std::vec::Vec<*mut u8> {
let mut blocks = std::vec::Vec::with_capacity(count);
for i in 0..count {
let p = unsafe { owner.alloc::<StandardPolicy>(BLOCK) };
assert!(!p.is_null(), "allocation {i} failed");
unsafe { core::ptr::write_bytes(p, stamp_for(i), BLOCK) };
blocks.push(p);
}
blocks
}
const fn stamp_for(i: usize) -> u8 {
(i as u8).wrapping_mul(37).wrapping_add(0xD1)
}
fn free_all_and_reclaim(owner: &mut ThreadAllocator<Backend>, blocks: &[*mut u8]) {
for &p in blocks {
unsafe { crate::thread_free::<StandardPolicy, Backend>(p) };
}
owner.reclaim_owned_segments();
}
#[test]
fn reclaim_orphans_a_segment_whose_blocks_are_still_live() {
let _guard = TEST_LOCK
.lock()
.expect("local allocator test lock was poisoned");
unsafe { super::fixtures::drain_all_pools() };
let mut owner = ThreadAllocator::<Backend>::new();
let blocks = fill(&mut owner, 4);
assert!(
owner.stats().current_thread_owned_segments >= 1,
"the owner must hold a segment before it can be reclaimed"
);
owner.reclaim_owned_segments();
assert_eq!(
owner.stats().current_thread_owned_segments,
0,
"reclamation must release the allocator's claim on every segment"
);
for (i, &p) in blocks.iter().enumerate() {
unsafe { assert_block_intact(p, stamp_for(i), i, "orphaned") };
}
let mut adopter = ThreadAllocator::<Backend>::new();
let fresh_block = unsafe { adopter.alloc::<StandardPolicy>(BLOCK) };
assert!(!fresh_block.is_null(), "adopter allocation failed");
assert!(
adopter.stats().orphan_segments_adopted >= 1,
"the reclaimed segment did not reach the orphan pool: the adopter \
mapped a fresh segment instead of taking it"
);
for (i, &p) in blocks.iter().enumerate() {
unsafe { assert_block_intact(p, stamp_for(i), i, "adopted") };
}
let mut all = blocks;
all.push(fresh_block);
free_all_and_reclaim(&mut adopter, &all);
}
#[test]
fn reclaim_returns_an_emptied_segment_to_the_pool() {
let _guard = TEST_LOCK
.lock()
.expect("local allocator test lock was poisoned");
unsafe { super::fixtures::drain_all_pools() };
let retained_before = mnemosyne_arena::arena_memory_stats::<Backend>().retained_free_segments;
let unmapped_before = mnemosyne_backend::backend_memory_stats().unmap_calls;
let mut owner = ThreadAllocator::<Backend>::new();
let blocks = fill(&mut owner, 4);
assert!(owner.stats().current_thread_owned_segments >= 1);
free_all_and_reclaim(&mut owner, &blocks);
assert_eq!(
owner.stats().current_thread_owned_segments,
0,
"reclamation must release the allocator's claim on every segment"
);
let retained_after = mnemosyne_arena::arena_memory_stats::<Backend>().retained_free_segments;
let unmapped_after = mnemosyne_backend::backend_memory_stats().unmap_calls;
assert!(
retained_after > retained_before || unmapped_after > unmapped_before,
"an emptied segment was neither pooled nor released: retained went \
{retained_before} -> {retained_after}, unmap calls went \
{unmapped_before} -> {unmapped_after}"
);
use mnemosyne_arena::HasSegmentPool;
let stranded = <Backend as HasSegmentPool>::global_orphan_pool().pop();
assert!(
stranded.is_none(),
"an emptied segment was orphaned rather than pooled"
);
}