use crate::core::{LuciError, Result};
use crate::storage::block::{BlockId, Extent};
#[derive(Debug)]
pub struct BlockAllocator {
total_blocks: u64,
free_list: Vec<Extent>,
}
impl BlockAllocator {
pub fn new() -> Self {
Self {
total_blocks: 0,
free_list: Vec::new(),
}
}
pub fn from_state(total_blocks: u64, free_list: Vec<Extent>) -> Self {
let mut alloc = Self {
total_blocks,
free_list,
};
alloc.free_list.sort_by_key(|e| e.start);
alloc
}
pub fn allocate(&mut self, count: u32) -> Result<Extent> {
if count == 0 {
return Err(LuciError::InvalidQuery(
"cannot allocate zero blocks".into(),
));
}
if let Some(idx) = self.free_list.iter().position(|e| e.count >= count) {
let extent = self.free_list[idx];
if extent.count == count {
self.free_list.remove(idx);
} else {
self.free_list[idx] =
Extent::new(BlockId(extent.start.0 + count as u64), extent.count - count);
}
return Ok(Extent::new(extent.start, count));
}
let start = BlockId(self.total_blocks);
self.total_blocks += count as u64;
Ok(Extent::new(start, count))
}
pub fn free(&mut self, extent: Extent) {
debug_assert!(
extent.end().0 <= self.total_blocks,
"extent {extent} exceeds file bounds (total_blocks = {})",
self.total_blocks
);
let pos = self
.free_list
.binary_search_by_key(&extent.start, |e| e.start)
.unwrap_or_else(|i| i);
debug_assert!(
!self.overlaps_free_list(&extent),
"double-free: {extent} overlaps an existing free extent"
);
self.free_list.insert(pos, extent);
self.coalesce_at(pos);
}
pub fn total_blocks(&self) -> u64 {
self.total_blocks
}
pub fn free_block_count(&self) -> u64 {
self.free_list.iter().map(|e| e.count as u64).sum()
}
pub fn free_list(&self) -> &[Extent] {
&self.free_list
}
fn coalesce_at(&mut self, pos: usize) {
if pos + 1 < self.free_list.len()
&& self.free_list[pos].is_adjacent_to(&self.free_list[pos + 1])
{
self.free_list[pos] = Extent::new(
self.free_list[pos].start,
self.free_list[pos].count + self.free_list[pos + 1].count,
);
self.free_list.remove(pos + 1);
}
if pos > 0 && self.free_list[pos - 1].is_adjacent_to(&self.free_list[pos]) {
self.free_list[pos - 1] = Extent::new(
self.free_list[pos - 1].start,
self.free_list[pos - 1].count + self.free_list[pos].count,
);
self.free_list.remove(pos);
}
}
fn overlaps_free_list(&self, extent: &Extent) -> bool {
self.free_list
.iter()
.any(|free| extent.start.0 < free.end().0 && free.start.0 < extent.end().0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_allocator_is_empty() {
let alloc = BlockAllocator::new();
assert_eq!(alloc.total_blocks(), 0);
assert_eq!(alloc.free_block_count(), 0);
}
#[test]
fn allocate_extends_file() {
let mut alloc = BlockAllocator::new();
let ext = alloc.allocate(4).unwrap();
assert_eq!(ext, Extent::new(BlockId(0), 4));
assert_eq!(alloc.total_blocks(), 4);
}
#[test]
fn sequential_allocations_are_contiguous() {
let mut alloc = BlockAllocator::new();
let a = alloc.allocate(2).unwrap();
let b = alloc.allocate(3).unwrap();
assert_eq!(a, Extent::new(BlockId(0), 2));
assert_eq!(b, Extent::new(BlockId(2), 3));
assert_eq!(alloc.total_blocks(), 5);
}
#[test]
fn allocate_zero_is_error() {
let mut alloc = BlockAllocator::new();
assert!(alloc.allocate(0).is_err());
}
#[test]
fn free_and_reuse() {
let mut alloc = BlockAllocator::new();
let a = alloc.allocate(4).unwrap();
let _b = alloc.allocate(2).unwrap();
alloc.free(a);
assert_eq!(alloc.free_block_count(), 4);
let c = alloc.allocate(4).unwrap();
assert_eq!(c, a);
assert_eq!(alloc.free_block_count(), 0);
}
#[test]
fn free_and_split() {
let mut alloc = BlockAllocator::new();
let a = alloc.allocate(8).unwrap();
alloc.free(a);
let b = alloc.allocate(3).unwrap();
assert_eq!(b, Extent::new(BlockId(0), 3));
assert_eq!(alloc.free_block_count(), 5);
let c = alloc.allocate(5).unwrap();
assert_eq!(c, Extent::new(BlockId(3), 5));
assert_eq!(alloc.free_block_count(), 0);
}
#[test]
fn coalesce_adjacent_frees() {
let mut alloc = BlockAllocator::new();
let a = alloc.allocate(3).unwrap(); let b = alloc.allocate(3).unwrap(); let c = alloc.allocate(3).unwrap();
alloc.free(b);
alloc.free(a);
assert_eq!(alloc.free_list().len(), 1);
assert_eq!(alloc.free_list()[0], Extent::new(BlockId(0), 6));
alloc.free(c);
assert_eq!(alloc.free_list().len(), 1);
assert_eq!(alloc.free_list()[0], Extent::new(BlockId(0), 9));
}
#[test]
fn non_adjacent_frees_stay_separate() {
let mut alloc = BlockAllocator::new();
let a = alloc.allocate(2).unwrap(); let _b = alloc.allocate(2).unwrap(); let c = alloc.allocate(2).unwrap();
alloc.free(a);
alloc.free(c);
assert_eq!(alloc.free_list().len(), 2);
assert_eq!(alloc.free_list()[0], Extent::new(BlockId(0), 2));
assert_eq!(alloc.free_list()[1], Extent::new(BlockId(4), 2));
}
#[test]
fn first_fit_prefers_first_free_extent() {
let mut alloc = BlockAllocator::new();
let a = alloc.allocate(4).unwrap(); let _b = alloc.allocate(2).unwrap(); let c = alloc.allocate(6).unwrap();
alloc.free(a);
alloc.free(c);
let d = alloc.allocate(3).unwrap();
assert_eq!(d, Extent::new(BlockId(0), 3));
}
#[test]
fn allocate_falls_through_to_file_extension_when_free_list_too_small() {
let mut alloc = BlockAllocator::new();
let a = alloc.allocate(2).unwrap();
alloc.free(a);
let b = alloc.allocate(5).unwrap();
assert_eq!(b, Extent::new(BlockId(2), 5));
assert_eq!(alloc.free_block_count(), 2);
}
#[test]
fn from_state_reconstructs_correctly() {
let free_list = vec![Extent::new(BlockId(10), 3), Extent::new(BlockId(5), 2)];
let alloc = BlockAllocator::from_state(20, free_list);
assert_eq!(alloc.total_blocks(), 20);
assert_eq!(alloc.free_block_count(), 5);
assert_eq!(alloc.free_list()[0].start, BlockId(5));
assert_eq!(alloc.free_list()[1].start, BlockId(10));
}
#[test]
fn from_state_allocates_from_free_list() {
let free_list = vec![Extent::new(BlockId(5), 4)];
let mut alloc = BlockAllocator::from_state(20, free_list);
let ext = alloc.allocate(2).unwrap();
assert_eq!(ext, Extent::new(BlockId(5), 2));
assert_eq!(alloc.free_block_count(), 2);
assert_eq!(alloc.total_blocks(), 20);
}
}