use super::range_alloc::RangeAllocator;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Placement {
pub block: usize,
pub offset: u64,
}
struct BlockState {
size: u64,
ranges: RangeAllocator,
in_use: u64,
dedicated: bool,
}
impl BlockState {
fn new(size: u64, dedicated: bool) -> Self {
let mut ranges = RangeAllocator::new();
ranges.free(0, size, 0);
ranges.reclaim(0);
Self {
size,
ranges,
in_use: 0,
dedicated,
}
}
fn is_empty(&self) -> bool {
self.in_use == 0 && self.ranges.free_bytes() == self.size
}
}
pub(crate) struct BlockAllocator {
blocks: Vec<Option<BlockState>>,
block_size: u64,
}
impl BlockAllocator {
pub(crate) fn new(block_size: u64) -> Self {
Self {
blocks: Vec::new(),
block_size: block_size.max(1),
}
}
pub(crate) fn alloc(&mut self, size: u64, align: u64) -> Option<Placement> {
for (block, state) in self.blocks.iter_mut().enumerate() {
let Some(state) = state.as_mut() else {
continue;
};
if state.dedicated {
continue;
}
if let Some(offset) = state.ranges.alloc_aligned(size, align) {
state.in_use += size;
return Some(Placement { block, offset });
}
}
None
}
pub(crate) fn alloc_in(&mut self, block: usize, size: u64, align: u64) -> Option<Placement> {
let state = self.blocks.get_mut(block)?.as_mut()?;
let offset = state.ranges.alloc_aligned(size, align)?;
state.in_use += size;
Some(Placement { block, offset })
}
pub(crate) fn add_block(&mut self, size: u64) -> usize {
let dedicated = size > self.block_size;
let state = BlockState::new(size, dedicated);
if let Some(index) = self.blocks.iter().position(Option::is_none) {
self.blocks[index] = Some(state);
index
} else {
self.blocks.push(Some(state));
self.blocks.len() - 1
}
}
pub(crate) fn free(&mut self, placement: Placement, size: u64, retire_frame: u64) {
let Some(Some(state)) = self.blocks.get_mut(placement.block) else {
return;
};
state.ranges.free(placement.offset, size, retire_frame);
state.in_use = state.in_use.saturating_sub(size);
}
pub(crate) fn reclaim(&mut self, current_frame: u64) {
for state in self.blocks.iter_mut().flatten() {
state.ranges.reclaim(current_frame);
}
}
pub(crate) fn take_empty_blocks(&mut self) -> Vec<usize> {
let mut released = Vec::new();
for (index, slot) in self.blocks.iter_mut().enumerate() {
if slot.as_ref().is_some_and(BlockState::is_empty) {
*slot = None;
released.push(index);
}
}
released
}
pub(crate) fn reserved_bytes(&self) -> u64 {
self.blocks.iter().flatten().map(|b| b.size).sum()
}
pub(crate) fn in_use_bytes(&self) -> u64 {
self.blocks.iter().flatten().map(|b| b.in_use).sum()
}
pub(crate) fn block_count(&self) -> usize {
self.blocks.iter().flatten().count()
}
}
#[cfg(test)]
mod tests {
use super::*;
const BLOCK: u64 = 1024;
fn place(pool: &mut BlockAllocator, size: u64, align: u64) -> Placement {
if let Some(p) = pool.alloc(size, align) {
return p;
}
let bytes = size
.saturating_add(align.max(1).saturating_sub(1))
.max(pool.block_size);
let block = pool.add_block(bytes);
pool.alloc_in(block, size, align)
.expect("fresh block must host it")
}
#[test]
fn many_resources_share_one_block() {
let mut pool = BlockAllocator::new(BLOCK);
for _ in 0..16 {
place(&mut pool, 64, 1);
}
assert_eq!(pool.block_count(), 1);
assert_eq!(pool.in_use_bytes(), 1024);
assert_eq!(pool.reserved_bytes(), 1024);
}
#[test]
fn a_second_block_opens_only_when_the_first_is_full() {
let mut pool = BlockAllocator::new(BLOCK);
for _ in 0..16 {
place(&mut pool, 64, 1);
}
assert_eq!(pool.block_count(), 1);
let overflow = place(&mut pool, 64, 1);
assert_eq!(pool.block_count(), 2);
assert_eq!(overflow.block, 1);
assert_eq!(overflow.offset, 0);
}
#[test]
fn an_oversized_request_gets_its_own_dedicated_block() {
let mut pool = BlockAllocator::new(BLOCK);
let big = place(&mut pool, 4096, 1);
assert_eq!(pool.reserved_bytes(), 4096);
let small = place(&mut pool, 64, 1);
assert_ne!(small.block, big.block);
assert_eq!(pool.reserved_bytes(), 4096 + BLOCK);
}
#[test]
fn freed_space_is_reused_within_the_same_block() {
let mut pool = BlockAllocator::new(BLOCK);
let first = place(&mut pool, 512, 1);
let second = place(&mut pool, 512, 1);
assert_eq!(pool.block_count(), 1);
pool.free(first, 512, 0);
pool.reclaim(0);
let third = place(&mut pool, 512, 1);
assert_eq!(third, first);
assert_eq!(pool.block_count(), 1);
assert_ne!(third, second);
}
#[test]
fn a_free_is_withheld_until_its_retire_frame() {
let mut pool = BlockAllocator::new(BLOCK);
let only = place(&mut pool, 1024, 1);
pool.free(only, 1024, 5);
pool.reclaim(4);
assert!(pool.alloc(1024, 1).is_none());
pool.reclaim(5);
assert_eq!(pool.alloc(1024, 1), Some(only));
}
#[test]
fn in_use_drops_on_free_while_reserved_holds() {
let mut pool = BlockAllocator::new(BLOCK);
let p = place(&mut pool, 256, 1);
assert_eq!(pool.in_use_bytes(), 256);
assert_eq!(pool.reserved_bytes(), BLOCK);
pool.free(p, 256, 0);
assert_eq!(pool.in_use_bytes(), 0);
assert_eq!(pool.reserved_bytes(), BLOCK);
}
#[test]
fn alignment_is_honoured_within_a_block() {
let mut pool = BlockAllocator::new(BLOCK);
place(&mut pool, 8, 1);
let aligned = place(&mut pool, 64, 256);
assert_eq!(aligned.offset % 256, 0);
assert_eq!(pool.block_count(), 1);
}
#[test]
fn a_block_sized_for_a_request_can_always_place_it() {
let mut pool = BlockAllocator::new(BLOCK);
place(&mut pool, 64, 1);
let p = place(&mut pool, BLOCK, 256);
assert_eq!(p.offset % 256, 0);
}
#[test]
fn an_emptied_block_is_handed_back_for_release() {
let mut pool = BlockAllocator::new(BLOCK);
let a = place(&mut pool, 512, 1);
let b = place(&mut pool, 512, 1);
pool.free(a, 512, 0);
assert!(pool.take_empty_blocks().is_empty());
pool.free(b, 512, 0);
assert!(pool.take_empty_blocks().is_empty());
pool.reclaim(0);
assert_eq!(pool.take_empty_blocks(), vec![0]);
assert_eq!(pool.block_count(), 0);
assert_eq!(pool.reserved_bytes(), 0);
}
#[test]
fn a_released_slot_is_reused_by_the_next_block() {
let mut pool = BlockAllocator::new(BLOCK);
let first = place(&mut pool, 1024, 1);
pool.free(first, 1024, 0);
pool.reclaim(0);
assert_eq!(pool.take_empty_blocks(), vec![0]);
let next = place(&mut pool, 256, 1);
assert_eq!(next.block, 0);
assert_eq!(pool.block_count(), 1);
}
#[test]
fn freeing_an_unknown_block_is_ignored() {
let mut pool = BlockAllocator::new(BLOCK);
let p = place(&mut pool, 256, 1);
pool.free(p, 256, 0);
pool.reclaim(0);
pool.take_empty_blocks();
pool.free(p, 256, 0);
assert_eq!(pool.in_use_bytes(), 0);
assert_eq!(pool.block_count(), 0);
}
#[test]
fn resource_count_does_not_drive_block_count() {
let mut pool = BlockAllocator::new(64 * 1024);
for _ in 0..4096 {
place(&mut pool, 256, 256);
}
assert_eq!(pool.in_use_bytes(), 4096 * 256);
assert_eq!(pool.block_count(), 16);
}
}