use super::{BStackAllocator, BStackBulkAllocator, BStackSlice};
use crate::BStack;
#[cfg(not(feature = "atomic"))]
use std::cell::Cell;
use std::fmt;
use std::io;
#[cfg(not(feature = "atomic"))]
use std::marker::PhantomData;
#[cfg(feature = "atomic")]
use std::sync::Mutex;
const ALGT_MAGIC: [u8; 8] = *b"ALGT\x00\x01\x02\x00";
const ALGT_MAGIC_PREFIX: [u8; 6] = *b"ALGT\x00\x01";
const MAGIC_OFFSET: u64 = 32;
const ROOT_OFFSET: u64 = 40;
const ARENA_START: u64 = 48;
const MIN_ALLOC: u64 = 32;
const NULL_PTR: u64 = 0;
const MAX_AVL_DEPTH: u32 = 128;
const NODE_SIZE_OFF: u64 = 0;
const NODE_BF_OFF: u64 = 8; const NODE_HEIGHT_OFF: u64 = 9; const NODE_LEFT_OFF: u64 = 16;
const NODE_RIGHT_OFF: u64 = 24;
struct PathEntry {
ptr: u64,
size: u64,
left: u64,
right: u64,
went_left: bool,
}
#[cfg_attr(not(feature = "atomic"), doc = "```compile_fail")]
#[cfg_attr(feature = "atomic", doc = "```")]
pub struct GhostTreeBstackAllocator {
stack: BStack,
#[cfg(feature = "atomic")]
lock: Mutex<()>,
#[cfg(not(feature = "atomic"))]
_not_sync: PhantomData<Cell<()>>,
}
impl fmt::Debug for GhostTreeBstackAllocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GhostTreeBstackAllocator")
.field("stack", &self.stack)
.finish_non_exhaustive()
}
}
impl GhostTreeBstackAllocator {
pub fn new(stack: BStack) -> io::Result<Self> {
let size = stack.len()?;
if size == 0 {
stack.extend(ARENA_START)?;
stack.set(MAGIC_OFFSET, ALGT_MAGIC)?;
return Ok(Self {
stack,
#[cfg(feature = "atomic")]
lock: Mutex::new(()),
#[cfg(not(feature = "atomic"))]
_not_sync: PhantomData,
});
}
if size < ARENA_START {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"GhostTreeBstackAllocator: payload is {size} B, \
too small for the {ARENA_START}-byte header"
),
));
}
let mut magic_buf = [0u8; 6];
stack.get_into(MAGIC_OFFSET, &mut magic_buf)?;
if magic_buf != ALGT_MAGIC_PREFIX {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"GhostTreeBstackAllocator: magic number mismatch",
));
}
let arena_used = size - ARENA_START;
let remainder = arena_used % 32;
if remainder != 0 {
stack.extend(32 - remainder)?;
}
let this = Self {
stack,
#[cfg(feature = "atomic")]
lock: Mutex::new(()),
#[cfg(not(feature = "atomic"))]
_not_sync: PhantomData,
};
this.coalesce_and_rebalance()?;
Ok(this)
}
}
impl GhostTreeBstackAllocator {
#[inline]
fn read_root(&self) -> io::Result<u64> {
let buf = &mut [0u8; 8];
self.stack.get_into(ROOT_OFFSET, buf)?;
Ok(read_buf_le!(buf, 0 => u64))
}
#[inline]
fn write_root(&self, ptr: u64) -> io::Result<()> {
let mut buf = [0u8; 8];
write_buf!(ptr => buf, 0);
self.stack.set(ROOT_OFFSET, buf)?;
Ok(())
}
fn read_node(&self, ptr: u64) -> io::Result<(u64, i8, u8, u64, u64)> {
let buf = &mut [0u8; 32];
self.stack.get_into(ptr, buf)?;
let size = read_buf_le!(buf, NODE_SIZE_OFF => u64);
let bf = read_buf_le!(buf, NODE_BF_OFF => i8);
let height = read_buf_le!(buf, NODE_HEIGHT_OFF => u8);
let left = read_buf_le!(buf, NODE_LEFT_OFF => u64);
let right = read_buf_le!(buf, NODE_RIGHT_OFF => u64);
Ok((size, bf, height, left, right))
}
fn write_node(
&self,
ptr: u64,
size: u64,
bf: i8,
height: u8,
left: u64,
right: u64,
) -> io::Result<()> {
let mut buf = [0u8; 32];
write_buf!(size => buf, NODE_SIZE_OFF);
write_buf!(bf => buf, NODE_BF_OFF);
write_buf!(height => buf, NODE_HEIGHT_OFF);
write_buf!(left => buf, NODE_LEFT_OFF);
write_buf!(right => buf, NODE_RIGHT_OFF);
self.stack.set(ptr, buf)?;
Ok(())
}
#[inline]
fn align_up_ptr(ptr: u64) -> u64 {
((ptr + 15) & !31) + 16
}
#[inline]
fn align_up_len(len: u64) -> u64 {
((len + 31) & !31).max(MIN_ALLOC)
}
#[inline]
fn avl_height(&self, ptr: u64) -> io::Result<u8> {
if ptr == NULL_PTR {
return Ok(0);
}
let (_, _, height, _, _) = self.read_node(ptr)?;
Ok(height)
}
#[inline]
fn avl_write_and_update(&self, ptr: u64, size: u64, left: u64, right: u64) -> io::Result<i8> {
let lh = self.avl_height(left)? as i16;
let rh = self.avl_height(right)? as i16;
let bf = (rh - lh) as i8;
let height = (1 + lh.max(rh)) as u8;
self.write_node(ptr, size, bf, height, left, right)?;
Ok(bf)
}
#[inline]
fn avl_update_bf(&self, node: u64) -> io::Result<i8> {
let (size, _, _, left, right) = self.read_node(node)?;
self.avl_write_and_update(node, size, left, right)
}
fn avl_rotate_right(&self, node: u64) -> io::Result<u64> {
let (node_sz, _, _, pivot, node_r) = self.read_node(node)?;
let (pivot_sz, _, _, pivot_l, pivot_r) = self.read_node(pivot)?;
self.avl_write_and_update(node, node_sz, pivot_r, node_r)?;
self.avl_write_and_update(pivot, pivot_sz, pivot_l, node)?;
Ok(pivot)
}
fn avl_rotate_left(&self, node: u64) -> io::Result<u64> {
let (node_sz, _, _, node_l, pivot) = self.read_node(node)?;
let (pivot_sz, _, _, pivot_l, pivot_r) = self.read_node(pivot)?;
self.avl_write_and_update(node, node_sz, node_l, pivot_l)?;
self.avl_write_and_update(pivot, pivot_sz, node, pivot_r)?;
Ok(pivot)
}
fn avl_rebalance(&self, node: u64) -> io::Result<u64> {
let bf = self.avl_update_bf(node)?;
if bf < -1 {
let (_, _, _, left, _) = self.read_node(node)?;
let (_, left_bf, _, _, _) = self.read_node(left)?;
if left_bf > 0 {
let new_left = self.avl_rotate_left(left)?;
let (node_sz, _, _, _, node_r) = self.read_node(node)?;
self.avl_write_and_update(node, node_sz, new_left, node_r)?;
}
self.avl_rotate_right(node)
} else if bf > 1 {
let (_, _, _, _, right) = self.read_node(node)?;
let (_, right_bf, _, _, _) = self.read_node(right)?;
if right_bf < 0 {
let new_right = self.avl_rotate_right(right)?;
let (node_sz, _, _, node_l, _) = self.read_node(node)?;
self.avl_write_and_update(node, node_sz, node_l, new_right)?;
}
self.avl_rotate_left(node)
} else {
Ok(node)
}
}
fn avl_insert(&self, ptr: u64, size: u64) -> io::Result<()> {
let root = self.read_root()?;
let mut path: Vec<PathEntry> = Vec::with_capacity(MAX_AVL_DEPTH as usize);
let mut current = root;
while current != NULL_PTR {
if path.len() >= MAX_AVL_DEPTH as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"AVL insert exceeded maximum depth: corrupted tree (possible cycle)",
));
}
let (root_sz, _, _, left, right) = self.read_node(current)?;
let went_left = (size, ptr) < (root_sz, current);
path.push(PathEntry {
ptr: current,
size: root_sz,
left,
right,
went_left,
});
current = if went_left { left } else { right };
}
self.write_node(ptr, size, 0, 1, NULL_PTR, NULL_PTR)?;
let mut child = ptr;
for entry in path.iter().rev() {
let (new_left, new_right) = if entry.went_left {
(child, entry.right)
} else {
(entry.left, child)
};
self.avl_write_and_update(entry.ptr, entry.size, new_left, new_right)?;
child = self.avl_rebalance(entry.ptr)?;
}
self.write_root(child)
}
fn avl_remove_min(&self, root: u64) -> io::Result<(u64, u64, u64)> {
let mut path: Vec<(u64, u64, u64)> = Vec::with_capacity(MAX_AVL_DEPTH as usize);
let mut current = root;
loop {
let (size, _, _, left, right) = self.read_node(current)?;
if left == NULL_PTR {
let mut child = right;
for &(anc_ptr, anc_sz, anc_right) in path.iter().rev() {
self.avl_write_and_update(anc_ptr, anc_sz, child, anc_right)?;
child = self.avl_rebalance(anc_ptr)?;
}
return Ok((current, size, child));
}
if path.len() >= MAX_AVL_DEPTH as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"AVL min exceeded maximum depth: corrupted tree (possible cycle)",
));
}
path.push((current, size, right));
current = left;
}
}
fn avl_find_best_fit_and_remove(&self, min_size: u64) -> io::Result<Option<(u64, u64)>> {
let root = self.read_root()?;
if root == NULL_PTR {
return Ok(None);
}
let mut path: Vec<PathEntry> = Vec::with_capacity(MAX_AVL_DEPTH as usize);
let mut last_fit_idx: Option<usize> = None;
let mut current = root;
while current != NULL_PTR {
if path.len() >= MAX_AVL_DEPTH as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"AVL find exceeded maximum depth: corrupted tree (possible cycle)",
));
}
let (root_sz, _, _, left, right) = self.read_node(current)?;
if root_sz >= min_size {
last_fit_idx = Some(path.len());
path.push(PathEntry {
ptr: current,
size: root_sz,
left,
right,
went_left: true,
});
current = left;
} else {
path.push(PathEntry {
ptr: current,
size: root_sz,
left,
right,
went_left: false,
});
current = right;
}
}
let fit_idx = match last_fit_idx {
None => return Ok(None),
Some(i) => i,
};
let found_ptr = path[fit_idx].ptr;
let found_size = path[fit_idx].size;
let found_left = path[fit_idx].left;
let found_right = path[fit_idx].right;
let replacement = if found_left == NULL_PTR {
found_right
} else if found_right == NULL_PTR {
found_left
} else {
let (succ, succ_sz, new_right) = self.avl_remove_min(found_right)?;
self.avl_write_and_update(succ, succ_sz, found_left, new_right)?;
self.avl_rebalance(succ)?
};
let mut child = replacement;
for entry in path[..fit_idx].iter().rev() {
let (new_left, new_right) = if entry.went_left {
(child, entry.right)
} else {
(entry.left, child)
};
self.avl_write_and_update(entry.ptr, entry.size, new_left, new_right)?;
child = self.avl_rebalance(entry.ptr)?;
}
self.write_root(child)?;
Ok(Some((found_ptr, found_size)))
}
fn avl_walk_inorder(
&self,
root: u64,
f: &mut dyn FnMut(u64, u64) -> io::Result<()>,
) -> io::Result<()> {
let mut stack: Vec<(u64, u64, u64)> = Vec::new();
let mut current = root;
loop {
while current != NULL_PTR {
if stack.len() >= MAX_AVL_DEPTH as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"AVL walk exceeded maximum depth: corrupted tree (possible cycle)",
));
}
let (size, _, _, left, right) = self.read_node(current)?;
stack.push((current, right, size));
current = left;
}
match stack.pop() {
None => return Ok(()),
Some((ptr, right, size)) => {
f(ptr, size)?;
current = right;
}
}
}
}
fn coalesce_and_rebalance(&self) -> io::Result<()> {
let root = self.read_root()?;
let mut blocks: Vec<(u64, u64)> = Vec::new(); self.avl_walk_inorder(root, &mut |ptr, size| {
blocks.push((ptr, size));
Ok(())
})?;
if blocks.is_empty() {
return Ok(());
}
blocks.sort_by_key(|&(ptr, _)| ptr);
blocks.dedup_by_key(|b| b.0);
let mut coalesced: Vec<(u64, u64)> = Vec::new();
let mut seams: Vec<u64> = Vec::new();
for (ptr, size) in blocks {
if let Some(last) = coalesced.last_mut()
&& last.0 + last.1 == ptr
{
seams.push(ptr);
last.1 += size;
continue;
}
coalesced.push((ptr, size));
}
for seam in seams {
self.stack.zero(seam, MIN_ALLOC)?;
}
coalesced.sort_by_key(|&(ptr, size)| (size, ptr));
enum BuildOp {
Enter(usize, usize),
Combine(usize),
}
let mut ops: Vec<BuildOp> = vec![BuildOp::Enter(0, coalesced.len())];
let mut results: Vec<u64> = Vec::new();
while let Some(op) = ops.pop() {
match op {
BuildOp::Enter(lo, hi) => {
if lo >= hi {
results.push(NULL_PTR);
} else {
let mid = lo + (hi - lo) / 2;
ops.push(BuildOp::Combine(mid));
ops.push(BuildOp::Enter(mid + 1, hi));
ops.push(BuildOp::Enter(lo, mid));
}
}
BuildOp::Combine(i) => {
let right_root = results.pop().unwrap();
let left_root = results.pop().unwrap();
let (ptr, size) = coalesced[i];
self.avl_write_and_update(ptr, size, left_root, right_root)?;
results.push(ptr);
}
}
}
let new_root = results
.pop()
.expect("build invariant: results must have exactly one element");
debug_assert!(
results.is_empty(),
"build invariant: excess elements on results stack"
);
self.write_root(new_root)
}
}
impl BStackAllocator for GhostTreeBstackAllocator {
type Error = io::Error;
type Allocated<'a> = BStackSlice<'a, Self>;
fn stack(&self) -> &BStack {
&self.stack
}
fn into_stack(self) -> BStack {
self.stack
}
fn alloc(&self, len: u64) -> io::Result<BStackSlice<'_, Self>> {
if len == 0 {
return Ok(unsafe { BStackSlice::from_raw_parts(self, 0, 0) });
}
let aligned = Self::align_up_len(len);
{
#[cfg(feature = "atomic")]
let guard = self.lock.lock().unwrap();
if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(aligned)? {
let remainder = block_size - aligned;
if remainder >= MIN_ALLOC {
self.avl_insert(ptr, remainder)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, ptr + remainder, len) });
} else {
#[cfg(feature = "atomic")]
drop(guard);
self.stack.zero(ptr, MIN_ALLOC)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, ptr, len) });
}
}
}
let start = self.stack.extend(aligned)?;
Ok(unsafe { BStackSlice::from_raw_parts(self, start, len) })
}
fn realloc<'a>(
&'a self,
slice: BStackSlice<'a, Self>,
new_len: u64,
) -> io::Result<BStackSlice<'a, Self>> {
if slice.is_empty() {
return self.alloc(new_len);
}
if slice.start() < ARENA_START || slice.start() != Self::align_up_ptr(slice.start()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"realloc: slice origin is not a valid allocator address",
));
}
if new_len == 0 {
self.dealloc(slice)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, 0, 0) });
}
let old_len = slice.len();
let aligned_old = Self::align_up_len(old_len);
let aligned_new = Self::align_up_len(new_len);
if aligned_new == aligned_old {
if new_len < old_len {
let tail_ptr = slice.start() + new_len;
let tail_len = old_len - new_len;
self.stack.zero(tail_ptr, tail_len)?;
}
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
if aligned_new < aligned_old {
let freed_tail = aligned_old - aligned_new;
let tail_ptr = slice.start() + aligned_new;
#[cfg(feature = "atomic")]
if self
.stack
.try_discard(slice.start() + aligned_old, freed_tail)?
{
if new_len < aligned_new {
self.stack
.zero(slice.start() + new_len, aligned_new - new_len)?;
}
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
#[cfg(not(feature = "atomic"))]
if slice.start() + aligned_old == self.stack.len()? {
if new_len < aligned_new {
self.stack
.zero(slice.start() + new_len, aligned_new - new_len)?;
}
self.stack.discard(freed_tail)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
self.stack
.zero(slice.start() + new_len, aligned_old - new_len)?;
#[cfg(feature = "atomic")]
let _guard = self.lock.lock().unwrap();
self.avl_insert(tail_ptr, freed_tail)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
#[cfg(feature = "atomic")]
if self
.stack
.try_extend_zeros(slice.start() + aligned_old, aligned_new - aligned_old)?
{
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
#[cfg(not(feature = "atomic"))]
if slice.start() + aligned_old == self.stack.len()? {
self.stack.extend(aligned_new - aligned_old)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
let new_slice = self.alloc(new_len)?;
let data = self.stack.get(slice.start(), slice.start() + old_len)?;
self.stack.set(new_slice.start(), &data)?;
self.dealloc(slice)?;
Ok(new_slice)
}
fn dealloc(&self, slice: BStackSlice<'_, Self>) -> io::Result<()> {
if slice.is_empty() {
return Ok(());
}
if slice.start() < ARENA_START || slice.start() != Self::align_up_ptr(slice.start()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"dealloc: slice origin is not a valid allocator address",
));
}
let ptr = slice.start();
let true_len = Self::align_up_len(slice.len());
#[cfg(feature = "atomic")]
if self.stack.try_discard(ptr + true_len, true_len)? {
return Ok(());
}
#[cfg(not(feature = "atomic"))]
if ptr + true_len == self.stack.len()? {
return self.stack.discard(true_len);
}
self.stack.zero(ptr, true_len)?;
#[cfg(feature = "atomic")]
let _guard = self.lock.lock().unwrap();
self.avl_insert(ptr, true_len)
}
}
impl BStackBulkAllocator for GhostTreeBstackAllocator {
fn alloc_bulk(
&self,
lengths: impl AsRef<[u64]>,
) -> Result<Vec<Self::Allocated<'_>>, Self::Error> {
let lengths = lengths.as_ref();
if lengths.is_empty() {
return Ok(Vec::new());
}
let aligned: Vec<u64> = lengths
.iter()
.map(|&l| if l == 0 { 0 } else { Self::align_up_len(l) })
.collect();
let total = aligned
.iter()
.copied()
.try_fold(0u64, |acc, a| acc.checked_add(a))
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"alloc_bulk: total size overflows u64",
)
})?;
if total == 0 {
return Ok(lengths
.iter()
.map(|_| unsafe { BStackSlice::from_raw_parts(self, 0, 0) })
.collect());
}
let block_ptr = {
#[cfg(feature = "atomic")]
let guard = self.lock.lock().unwrap();
if let Some((ptr, block_size)) = self.avl_find_best_fit_and_remove(total)? {
let remainder = block_size - total;
if remainder >= MIN_ALLOC {
self.avl_insert(ptr, remainder)?;
ptr + remainder
} else {
#[cfg(feature = "atomic")]
drop(guard); self.stack.zero(ptr, MIN_ALLOC)?;
ptr
}
} else {
NULL_PTR }
};
let block_ptr = if block_ptr == NULL_PTR {
self.stack.extend(total)?
} else {
block_ptr
};
let mut result = Vec::with_capacity(lengths.len());
let mut offset = 0u64;
for (&len, &al) in lengths.iter().zip(aligned.iter()) {
if len == 0 {
result.push(unsafe { BStackSlice::from_raw_parts(self, 0, 0) });
} else {
result.push(unsafe { BStackSlice::from_raw_parts(self, block_ptr + offset, len) });
offset += al;
}
}
Ok(result)
}
fn dealloc_bulk<'a>(
&'a self,
slices: impl AsRef<[Self::Allocated<'a>]>,
) -> Result<(), Self::Error> {
let slices = slices.as_ref();
let mut entries: Vec<(u64, u64)> = Vec::new();
for s in slices {
if s.is_empty() {
continue;
}
if s.start() < ARENA_START || s.start() != Self::align_up_ptr(s.start()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"dealloc_bulk: invalid slice origin",
));
}
entries.push((s.start(), Self::align_up_len(s.len())));
}
if entries.is_empty() {
return Ok(());
}
entries.sort_by_key(|&(ptr, _)| ptr);
let mut merged: Vec<(u64, u64)> = Vec::new();
for (ptr, size) in entries {
if let Some(last) = merged.last_mut()
&& last.0 + last.1 == ptr
{
last.1 += size;
} else {
merged.push((ptr, size));
}
}
let last = merged.pop().unwrap();
let last_discarded;
#[cfg(feature = "atomic")]
{
last_discarded = self.stack.try_discard(last.0 + last.1, last.1)?;
}
#[cfg(not(feature = "atomic"))]
{
if last.0 + last.1 == self.stack.len()? {
self.stack.discard(last.1)?;
last_discarded = true;
} else {
last_discarded = false;
}
}
if !last_discarded {
merged.push(last);
}
for &(ptr, size) in &merged {
self.stack.zero(ptr, size)?;
}
if !merged.is_empty() {
#[cfg(feature = "atomic")]
let _guard = self.lock.lock().unwrap();
for (ptr, size) in merged {
self.avl_insert(ptr, size)?;
}
}
Ok(())
}
}
#[cfg(all(test, feature = "alloc", feature = "set"))]
mod tests {
use super::*;
use crate::BStack;
use crate::alloc::{BStackAllocator, BStackBulkAllocator, BStackSlice};
use std::sync::atomic::{AtomicU64, Ordering};
fn open_fresh() -> (GhostTreeBstackAllocator, std::path::PathBuf) {
static CTR: AtomicU64 = AtomicU64::new(0);
let id = CTR.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("bstack_gt_{pid}_{id}.bin"));
let alloc = GhostTreeBstackAllocator::new(BStack::open(&path).unwrap()).unwrap();
(alloc, path)
}
struct Guard(std::path::PathBuf);
impl Drop for Guard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn reopen(path: &std::path::Path) -> GhostTreeBstackAllocator {
GhostTreeBstackAllocator::new(BStack::open(path).unwrap()).unwrap()
}
#[test]
fn alloc_returns_zeroed_slice() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(64).unwrap();
assert_eq!(s.len(), 64);
assert!(s.read().unwrap().iter().all(|&b| b == 0));
}
#[test]
fn alloc_zero_len_returns_null_slice() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(0).unwrap();
assert_eq!(s.len(), 0);
assert_eq!(s.start(), 0);
}
#[test]
fn dealloc_zero_len_is_noop() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let before = alloc.stack().len().unwrap();
let s = alloc.alloc(0).unwrap();
alloc.dealloc(s).unwrap();
assert_eq!(alloc.stack().len().unwrap(), before);
}
#[test]
fn dealloc_tail_shrinks_stack() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let before = alloc.stack().len().unwrap();
let s = alloc.alloc(64).unwrap();
let after_alloc = alloc.stack().len().unwrap();
assert!(after_alloc > before);
alloc.dealloc(s).unwrap();
assert_eq!(alloc.stack().len().unwrap(), before);
}
#[test]
fn dealloc_nontail_reuses_on_next_alloc() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let a = alloc.alloc(64).unwrap();
let b = alloc.alloc(64).unwrap();
let a_start = a.start();
alloc.dealloc(a).unwrap();
let stack_len = alloc.stack().len().unwrap();
let c = alloc.alloc(64).unwrap();
assert_eq!(c.start(), a_start);
assert_eq!(alloc.stack().len().unwrap(), stack_len);
alloc.dealloc(c).unwrap();
alloc.dealloc(b).unwrap();
}
#[test]
fn freed_block_is_zeroed() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let a = alloc.alloc(64).unwrap();
let b = alloc.alloc(64).unwrap();
let a_start = a.start();
a.write(&[0xAAu8; 64]).unwrap();
alloc.dealloc(a).unwrap();
let raw = alloc.stack().get(a_start, a_start + 64).unwrap();
assert!(raw[32..].iter().all(|&b| b == 0));
alloc.dealloc(b).unwrap();
}
#[test]
fn all_pointers_are_32_byte_aligned() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let slices: Vec<_> = (0..16).map(|i| alloc.alloc(i * 7 + 1).unwrap()).collect();
for s in &slices {
if s.len() > 0 {
assert_eq!(
s.start() % 32,
16,
"start {} not 32-byte aligned on disk",
s.start()
);
}
}
for s in slices {
alloc.dealloc(s).unwrap();
}
}
#[test]
fn alloc_rounds_up_to_min_alloc() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let before = alloc.stack().len().unwrap();
let s = alloc.alloc(1).unwrap();
assert_eq!(alloc.stack().len().unwrap() - before, MIN_ALLOC);
alloc.dealloc(s).unwrap();
}
#[test]
fn large_free_block_is_split() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let a = alloc.alloc(128).unwrap();
let anchor = alloc.alloc(32).unwrap(); let a_start = a.start();
alloc.dealloc(a).unwrap();
let b = alloc.alloc(32).unwrap();
assert_eq!(b.start(), a_start + 96);
let c = alloc.alloc(96).unwrap();
assert_eq!(c.start(), a_start);
alloc.dealloc(b).unwrap();
alloc.dealloc(c).unwrap();
alloc.dealloc(anchor).unwrap();
}
#[test]
fn realloc_to_zero_deallocates() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let before = alloc.stack().len().unwrap();
let s = alloc.alloc(64).unwrap();
let z = alloc.realloc(s, 0).unwrap();
assert_eq!(z.len(), 0);
assert_eq!(alloc.stack().len().unwrap(), before);
}
#[test]
fn realloc_same_aligned_size_preserves_data() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(32).unwrap();
s.write(&[0x5Au8; 32]).unwrap();
let start = s.start();
let s2 = alloc.realloc(s, 16).unwrap();
assert_eq!(s2.start(), start);
let buf = s2.read().unwrap();
assert!(buf[..16].iter().all(|&b| b == 0x5A));
assert!(buf[16..].iter().all(|&b| b == 0));
alloc.dealloc(s2).unwrap();
}
#[test]
fn realloc_shrink_tail_discards() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(128).unwrap();
let start = s.start();
s.write(&[0xBBu8; 128]).unwrap();
let s2 = alloc.realloc(s, 32).unwrap();
assert_eq!(s2.start(), start);
assert_eq!(alloc.stack().len().unwrap(), start + 32);
let buf = s2.read().unwrap();
assert!(buf[..32].iter().all(|&b| b == 0xBB));
alloc.dealloc(s2).unwrap();
}
#[test]
fn realloc_shrink_nontail_inserts_remainder() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(128).unwrap();
let anchor = alloc.alloc(32).unwrap();
let start = s.start();
s.write(&[0xCCu8; 128]).unwrap();
let stack_len = alloc.stack().len().unwrap();
let s2 = alloc.realloc(s, 32).unwrap();
assert_eq!(s2.start(), start);
assert_eq!(alloc.stack().len().unwrap(), stack_len);
let r = alloc.alloc(96).unwrap();
assert_eq!(r.start(), start + 32);
alloc.dealloc(s2).unwrap();
alloc.dealloc(r).unwrap();
alloc.dealloc(anchor).unwrap();
}
#[test]
fn realloc_grow_tail_extends_in_place() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(32).unwrap();
let start = s.start();
s.write(&[0xDDu8; 32]).unwrap();
let s2 = alloc.realloc(s, 96).unwrap();
assert_eq!(s2.start(), start);
let buf = s2.read().unwrap();
assert!(buf[..32].iter().all(|&b| b == 0xDD));
assert!(buf[32..].iter().all(|&b| b == 0));
alloc.dealloc(s2).unwrap();
}
#[test]
fn realloc_grow_nontail_copies_data() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(32).unwrap();
let anchor = alloc.alloc(32).unwrap();
s.write(&[0xEEu8; 32]).unwrap();
let s2 = alloc.realloc(s, 96).unwrap();
assert_ne!(s2.start(), anchor.start());
let buf = s2.read().unwrap();
assert!(buf[..32].iter().all(|&b| b == 0xEE));
assert!(buf[32..].iter().all(|&b| b == 0));
alloc.dealloc(s2).unwrap();
alloc.dealloc(anchor).unwrap();
}
#[test]
fn dealloc_misaligned_ptr_returns_error() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(64).unwrap();
let bad = unsafe { BStackSlice::from_raw_parts(&alloc, s.start() + 1, 32) };
assert!(alloc.dealloc(bad).is_err());
alloc.dealloc(s).unwrap();
}
#[test]
fn realloc_misaligned_ptr_returns_error() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let s = alloc.alloc(64).unwrap();
let bad = unsafe { BStackSlice::from_raw_parts(&alloc, s.start() + 1, 32) };
assert!(alloc.realloc(bad, 64).is_err());
alloc.dealloc(s).unwrap();
}
#[test]
fn alloc_bulk_contiguous() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let slices = alloc.alloc_bulk([32u64, 64, 32]).unwrap();
assert_eq!(slices.len(), 3);
assert_eq!(slices[0].len(), 32);
assert_eq!(slices[1].len(), 64);
assert_eq!(slices[2].len(), 32);
assert_eq!(slices[1].start(), slices[0].start() + 32);
assert_eq!(slices[2].start(), slices[1].start() + 64);
for s in slices {
alloc.dealloc(s).unwrap();
}
}
#[test]
fn alloc_bulk_with_zeros_returns_null_for_zeros() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let slices = alloc.alloc_bulk([0u64, 32, 0]).unwrap();
assert_eq!(slices[0].len(), 0);
assert_eq!(slices[0].start(), 0);
assert_eq!(slices[2].len(), 0);
assert_eq!(slices[2].start(), 0);
assert_eq!(slices[1].len(), 32);
for s in slices {
alloc.dealloc(s).unwrap();
}
}
#[test]
fn dealloc_bulk_merges_adjacent_slices() {
let (alloc, path) = open_fresh();
let _g = Guard(path);
let before = alloc.stack().len().unwrap();
let slices = alloc.alloc_bulk([64u64, 64, 64]).unwrap();
alloc.dealloc_bulk(slices).unwrap();
assert_eq!(alloc.stack().len().unwrap(), before);
}
#[test]
fn coalesce_on_reopen_merges_adjacent_free_blocks() {
let (alloc, path) = open_fresh();
let _g = Guard(path.clone());
let a = alloc.alloc(64).unwrap();
let b = alloc.alloc(64).unwrap();
let anchor = alloc.alloc(32).unwrap();
let a_start = a.start();
let anchor_start = anchor.start();
alloc.dealloc(a).unwrap();
alloc.dealloc(b).unwrap();
let _ = anchor;
drop(alloc.into_stack());
let alloc2 = reopen(&path);
let c = alloc2.alloc(128).unwrap();
assert_eq!(c.start(), a_start);
alloc2.dealloc(c).unwrap();
let anchor2 = unsafe { BStackSlice::from_raw_parts(&alloc2, anchor_start, 32) };
alloc2.dealloc(anchor2).unwrap();
}
#[test]
fn data_survives_reopen() {
let (alloc, path) = open_fresh();
let _g = Guard(path.clone());
let s = alloc.alloc(64).unwrap();
let start = s.start();
s.write(&[0xABu8; 64]).unwrap();
drop(alloc.into_stack());
let alloc2 = reopen(&path);
let s2 = unsafe { BStackSlice::from_raw_parts(&alloc2, start, 64) };
assert!(s2.read().unwrap().iter().all(|&b| b == 0xAB));
alloc2.dealloc(s2).unwrap();
}
#[test]
fn new_rejects_partial_header() {
use std::io::ErrorKind;
static CTR: AtomicU64 = AtomicU64::new(0);
let id = CTR.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("bstack_gt_partial_{pid}_{id}.bin"));
let _g = Guard(path.clone());
let stack = BStack::open(&path).unwrap();
stack.extend(4).unwrap(); let err = GhostTreeBstackAllocator::new(stack).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData);
}
#[cfg(feature = "atomic")]
#[test]
fn concurrent_alloc_dealloc_no_live_duplicates() {
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::thread;
const THREADS: usize = 8;
const ROUNDS: usize = 200;
let (alloc, path) = open_fresh();
let _g = Guard(path);
let alloc = Arc::new(alloc);
let live: Arc<Mutex<HashSet<u64>>> = Arc::new(Mutex::new(HashSet::new()));
let handles: Vec<_> = (0..THREADS)
.map(|tid| {
let alloc = Arc::clone(&alloc);
let live = Arc::clone(&live);
thread::spawn(move || {
let a: &GhostTreeBstackAllocator = &alloc;
for _ in 0..ROUNDS {
let slice = a.alloc(32).unwrap();
let off = slice.start();
{
let mut set = live.lock().unwrap();
assert!(set.insert(off), "duplicate live offset {off}");
}
slice.write(&[tid as u8; 32]).unwrap();
let data = slice.read().unwrap();
assert_eq!(data, vec![tid as u8; 32]);
{
let mut set = live.lock().unwrap();
set.remove(&off);
}
a.dealloc(slice).unwrap();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[cfg(feature = "atomic")]
#[test]
fn concurrent_realloc_hammers_tail_paths() {
use std::sync::Arc;
use std::thread;
const THREADS: usize = 6;
const ROUNDS: usize = 150;
const SMALL: u64 = 32;
const LARGE: u64 = 96;
let (alloc, path) = open_fresh();
let _g = Guard(path);
let alloc = Arc::new(alloc);
let handles: Vec<_> = (0..THREADS)
.map(|tid| {
let alloc = Arc::clone(&alloc);
thread::spawn(move || {
let a: &GhostTreeBstackAllocator = &alloc;
let mut slice = a.alloc(SMALL).unwrap();
slice.write(&[tid as u8; SMALL as usize]).unwrap();
for _ in 0..ROUNDS {
slice = a.realloc(slice, LARGE).unwrap();
let data = slice.read().unwrap();
assert_eq!(
&data[..SMALL as usize],
&[tid as u8; SMALL as usize],
"data corrupted after grow (tid {tid})",
);
slice = a.realloc(slice, SMALL).unwrap();
let data = slice.read().unwrap();
assert_eq!(
data,
vec![tid as u8; SMALL as usize],
"data corrupted after shrink (tid {tid})",
);
}
a.dealloc(slice).unwrap();
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[cfg(feature = "atomic")]
#[test]
fn concurrent_alloc_bulk_dealloc_bulk_no_live_duplicates() {
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::thread;
const THREADS: usize = 6;
const ROUNDS: usize = 100;
const SIZES: [u64; 3] = [32, 64, 32];
let (alloc, path) = open_fresh();
let _g = Guard(path);
let alloc = Arc::new(alloc);
let live: Arc<Mutex<HashSet<u64>>> = Arc::new(Mutex::new(HashSet::new()));
let handles: Vec<_> = (0..THREADS)
.map(|tid| {
let alloc = Arc::clone(&alloc);
let live = Arc::clone(&live);
thread::spawn(move || {
let a: &GhostTreeBstackAllocator = &alloc;
for _ in 0..ROUNDS {
let slices = a.alloc_bulk(SIZES).unwrap();
{
let mut set = live.lock().unwrap();
for s in &slices {
assert!(
set.insert(s.start()),
"duplicate live offset {}",
s.start()
);
}
}
for (s, &sz) in slices.iter().zip(SIZES.iter()) {
s.write(&vec![tid as u8; sz as usize]).unwrap();
let data = s.read().unwrap();
assert_eq!(data, vec![tid as u8; sz as usize]);
}
{
let mut set = live.lock().unwrap();
for s in &slices {
set.remove(&s.start());
}
}
a.dealloc_bulk(slices).unwrap();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
}