use super::*;
#[test]
fn test_snmalloc_message_passing() {
let _guard = TEST_LOCK
.lock()
.expect("local allocator test lock was poisoned");
use std::thread;
unsafe {
mnemosyne_arena::purge_segment_pool::<DefaultBackend>();
mnemosyne_arena::purge_segment_pool::<mnemosyne_backend::MemoryBackendWrapper>();
}
let mut alloc_a = ThreadAllocator::<DefaultBackend>::new();
let ptr = unsafe { alloc_a.alloc::<StandardPolicy>(32) };
assert!(
!ptr.is_null(),
"producer allocation for cross-thread free failed"
);
let ptr_usize = ptr as usize;
let handle = thread::spawn(move || {
unsafe {
crate::thread_free::<mnemosyne_core::StandardPolicy, DefaultBackend>(
ptr_usize as *mut u8,
);
}
});
handle.join().expect("cross-thread free worker panicked");
let mut reclaimed_remote_free = false;
let ptr_val = ptr as usize;
let segment_addr = ptr_val & !(mnemosyne_core::constants::SEGMENT_SIZE - 1);
let segment = segment_addr as *mut Segment;
let page_index = (ptr_val >> PAGE_SHIFT) & (PAGES_PER_SEGMENT - 1);
let max_blocks = unsafe { (*segment).pages[page_index].max_blocks() };
let mut probe_allocations = std::vec::Vec::with_capacity(max_blocks);
for _ in 0..max_blocks {
let ptr2 = unsafe { alloc_a.alloc::<StandardPolicy>(32) };
assert!(
!ptr2.is_null(),
"reclaim probe allocation failed before reclaiming remote free"
);
probe_allocations.push(ptr2);
if ptr2 == ptr {
reclaimed_remote_free = true;
break;
}
}
assert!(
reclaimed_remote_free,
"cross-thread freed block was not reclaimed after {} small allocations",
max_blocks
);
unsafe {
for probe in probe_allocations {
crate::thread_free::<mnemosyne_core::StandardPolicy, DefaultBackend>(probe);
}
}
alloc_a.reclaim_owned_segments();
}
#[test]
fn test_mixed_policy_free_and_realloc_preserve_segment_encoding() {
let _guard = TEST_LOCK
.lock()
.expect("local allocator test lock was poisoned");
use mnemosyne_core::policy::HardenedPolicy;
use std::alloc::Layout;
unsafe { super::super::fixtures::drain_all_pools() };
let hardened_slot =
<DefaultBackend as LocalAllocatorSelector<DefaultBackend>>::get_allocator_ptr_for_policy::<
HardenedPolicy,
>();
let standard_slot =
<DefaultBackend as LocalAllocatorSelector<DefaultBackend>>::get_allocator_ptr_for_policy::<
StandardPolicy,
>();
assert!(
!standard_slot.is_null(),
"standard TLS slot must initialize"
);
let hardened_first = unsafe { crate::thread_alloc::<HardenedPolicy, DefaultBackend>(32, 8) };
let hardened_second = unsafe { crate::thread_alloc::<HardenedPolicy, DefaultBackend>(32, 8) };
let hardened_third = unsafe { crate::thread_alloc::<HardenedPolicy, DefaultBackend>(32, 8) };
assert!(!hardened_first.is_null());
assert!(!hardened_second.is_null());
assert!(!hardened_third.is_null());
let hardened_slot_after = <DefaultBackend as LocalAllocatorSelector<DefaultBackend>>::
get_allocator_ptr_raw_for_policy::<HardenedPolicy>();
assert!(!hardened_slot_after.is_null());
assert_ne!(
hardened_slot_after, standard_slot,
"standard and hardened policies must not share a TLS allocator"
);
assert_eq!(hardened_slot, hardened_slot_after);
unsafe {
crate::thread_free::<StandardPolicy, DefaultBackend>(hardened_second);
}
assert!(
hardened_chain_contains(hardened_second),
"a standard-policy free must leave the hardened block decodable in its owner chain"
);
let layout = Layout::from_size_align(32, 8).expect("test layout is valid");
let resized = unsafe {
crate::thread_realloc::<StandardPolicy, DefaultBackend>(hardened_first, layout, 64)
};
assert!(
!resized.is_null(),
"mixed-policy realloc must produce a block"
);
assert!(
hardened_chain_contains(hardened_first),
"the block freed by a standard-policy realloc must stay decodable in the owner chain"
);
unsafe {
crate::thread_free::<HardenedPolicy, DefaultBackend>(hardened_third);
crate::thread_free::<StandardPolicy, DefaultBackend>(resized);
}
}
#[test]
fn cross_thread_free_pushes_block_to_page_thread_free_queue() {
let _guard = TEST_LOCK
.lock()
.expect("local allocator test lock was poisoned");
use std::thread;
let mut owner = ThreadAllocator::<DefaultBackend>::new();
let ptr = unsafe { owner.alloc::<StandardPolicy>(32) };
assert!(
!ptr.is_null(),
"owner alloc for thread_free queue anchor failed"
);
let ptr_val = ptr as usize;
let segment_addr = ptr_val & !(mnemosyne_core::constants::SEGMENT_SIZE - 1);
let segment = segment_addr as *mut Segment;
let page_index = (ptr_val >> PAGE_SHIFT) & (PAGES_PER_SEGMENT - 1);
let page = unsafe { &(*segment).pages[page_index] };
assert!(
page.thread_free.is_empty(),
"thread_free must be empty before any remote free; alloc_count={}",
page.alloc_count,
);
let handle = thread::spawn(move || unsafe {
crate::thread_free::<mnemosyne_core::StandardPolicy, DefaultBackend>(ptr_val as *mut u8);
});
handle.join().expect("cross-thread free worker panicked");
let page = unsafe { &mut (*segment).pages[page_index] };
assert!(
!page.thread_free.is_empty(),
"cross-thread free did not enqueue the block on page.thread_free",
);
let before_alloc_count = page.alloc_count;
let reclaimed = unsafe { Page::reclaim_thread_free_for_policy(segment, page_index) };
assert_eq!(
reclaimed, 1,
"expected exactly one block from the cross-thread free on this page; got {} \
(alloc_count before drain = {})",
reclaimed, before_alloc_count,
);
}
#[test]
fn allocation_side_reclaim_counts_cross_thread_blocks_exactly() {
let _guard = TEST_LOCK
.lock()
.expect("local allocator test lock was poisoned");
use std::thread;
unsafe {
mnemosyne_arena::purge_segment_pool::<DefaultBackend>();
mnemosyne_arena::purge_segment_pool::<mnemosyne_backend::MemoryBackendWrapper>();
}
let mut owner = ThreadAllocator::<DefaultBackend>::new();
let first = unsafe { owner.alloc::<StandardPolicy>(32) };
assert!(!first.is_null(), "owner anchor allocation failed");
let (segment, page_index) = unsafe { mnemosyne_core::types::locate_segment(first) };
let max_blocks = unsafe { (*segment).pages[page_index].max_blocks() };
assert!(max_blocks >= 2, "size class must hold at least two blocks");
let mut blocks = std::vec::Vec::with_capacity(max_blocks);
blocks.push(first as usize);
for _ in 1..max_blocks {
let p = unsafe { owner.alloc::<StandardPolicy>(32) };
assert!(!p.is_null(), "owner fill allocation failed");
blocks.push(p as usize);
}
assert_eq!(
owner.cross_thread_reclaimed, 0,
"fresh allocator must have reclaimed no cross-thread blocks"
);
let stats_before = owner.stats().cross_thread_reclaimed_blocks;
let freed = blocks.clone();
thread::spawn(move || unsafe {
for addr in freed {
crate::thread_free::<mnemosyne_core::StandardPolicy, DefaultBackend>(addr as *mut u8);
}
})
.join()
.expect("cross-thread free worker panicked");
let reclaimed_ptr = unsafe { owner.alloc::<StandardPolicy>(32) };
assert!(
!reclaimed_ptr.is_null(),
"owner allocation after remote frees failed"
);
assert_eq!(
owner.cross_thread_reclaimed, max_blocks,
"per-thread reclaim counter must equal the number of cross-thread frees"
);
assert_eq!(
owner.stats().cross_thread_reclaimed_blocks,
stats_before + max_blocks,
"stats() must report the exact cross-thread reclaimed delta"
);
unsafe {
crate::thread_free::<mnemosyne_core::StandardPolicy, DefaultBackend>(reclaimed_ptr);
}
owner.reclaim_owned_segments();
}
fn hardened_chain_contains(block: *mut u8) -> bool {
use mnemosyne_core::policy::HardenedPolicy;
let value = block as usize;
let segment = (value & !(mnemosyne_core::constants::SEGMENT_SIZE - 1)) as *mut Segment;
let page_index = (value >> PAGE_SHIFT) & (PAGES_PER_SEGMENT - 1);
let page = unsafe { &raw mut (*segment).pages[page_index] };
let cookie = unsafe { Segment::cookie_for::<HardenedPolicy>(segment, page_index) };
let bound = unsafe { (*page).max_blocks() };
let target = block.cast::<Block>();
unsafe {
mnemosyne_core::types::Page::reclaim_thread_free_if_present_for_policy(segment, page_index);
}
for head in unsafe { [(*page).free, (*page).secondary_free] } {
let mut current = head;
for _ in 0..bound {
let Some(node) = current else { break };
if node.as_ptr() == target {
return true;
}
current = unsafe { (*node.as_ptr()).get_next::<HardenedPolicy>(cookie) };
}
}
false
}