use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct FreeBlock {
addr: u64,
len: u64,
}
pub struct FileAllocator {
eof: AtomicU64,
alignment: u64,
free_list: Mutex<Vec<FreeBlock>>,
free_count: AtomicU64,
}
impl FileAllocator {
pub fn new(initial_eof: u64) -> Self {
Self {
eof: AtomicU64::new(initial_eof),
alignment: 8,
free_list: Mutex::new(Vec::new()),
free_count: AtomicU64::new(0),
}
}
fn align_up(&self, size: u64) -> u64 {
(size + self.alignment - 1) & !(self.alignment - 1)
}
pub fn allocate(&self, size: u64) -> u64 {
if let Some(addr) = self.take_free(size) {
return addr;
}
let mut cur = self.eof.load(Ordering::Acquire);
loop {
let aligned = (cur + self.alignment - 1) & !(self.alignment - 1);
let next = aligned + size;
match self
.eof
.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire)
{
Ok(_) => return aligned,
Err(actual) => cur = actual,
}
}
}
pub fn free(&self, addr: u64, len: u64) {
if len == 0 {
return;
}
let mut list = self.free_list.lock().unwrap();
let pos = list.partition_point(|b| b.addr < addr);
list.insert(pos, FreeBlock { addr, len });
if pos + 1 < list.len() && list[pos].addr + list[pos].len == list[pos + 1].addr {
list[pos].len += list[pos + 1].len;
list.remove(pos + 1);
}
if pos > 0 && list[pos - 1].addr + list[pos - 1].len == list[pos].addr {
list[pos - 1].len += list[pos].len;
list.remove(pos);
}
self.free_count.store(list.len() as u64, Ordering::Release);
}
fn take_free(&self, size: u64) -> Option<u64> {
if size == 0 || self.free_count.load(Ordering::Acquire) == 0 {
return None;
}
let mut list = self.free_list.lock().unwrap();
let pos = list
.iter()
.enumerate()
.filter(|(_, b)| b.len >= size)
.min_by_key(|(_, b)| b.len)
.map(|(i, _)| i)?;
let block = list[pos];
let used = self.align_up(size);
if block.len > used {
list[pos] = FreeBlock {
addr: block.addr + used,
len: block.len - used,
};
} else {
list.remove(pos);
}
self.free_count.store(list.len() as u64, Ordering::Release);
Some(block.addr)
}
pub fn eof(&self) -> u64 {
self.eof.load(Ordering::Acquire)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_allocation() {
let alloc = FileAllocator::new(48);
let a = alloc.allocate(100);
assert_eq!(a, 48);
assert_eq!(alloc.eof(), 148);
}
#[test]
fn alignment() {
let alloc = FileAllocator::new(50); let a = alloc.allocate(10);
assert_eq!(a, 56); assert_eq!(alloc.eof(), 66);
}
#[test]
fn zero_size_allocation() {
let alloc = FileAllocator::new(48);
let a = alloc.allocate(0);
assert_eq!(a, 48);
assert_eq!(alloc.eof(), 48);
}
#[test]
fn successive_allocations() {
let alloc = FileAllocator::new(0);
let a1 = alloc.allocate(10);
let a2 = alloc.allocate(20);
let a3 = alloc.allocate(5);
assert_eq!(a1, 0);
assert_eq!(a2, 16); assert_eq!(a3, 40); }
#[test]
fn concurrent_allocations_are_disjoint() {
use std::sync::Arc;
use std::thread;
let alloc = Arc::new(FileAllocator::new(0));
let n_threads = 8;
let per_thread = 1000;
let size = 7u64;
let mut handles = Vec::new();
for _ in 0..n_threads {
let a = Arc::clone(&alloc);
handles.push(thread::spawn(move || {
let mut offs = Vec::with_capacity(per_thread);
for _ in 0..per_thread {
offs.push(a.allocate(size));
}
offs
}));
}
let mut all: Vec<u64> = handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect();
all.sort_unstable();
for w in all.windows(2) {
assert_eq!(w[0] % 8, 0, "offset {} not 8-aligned", w[0]);
assert!(
w[1] >= w[0] + size,
"ranges overlap: {} + {} > {}",
w[0],
size,
w[1]
);
}
assert_eq!(all.len(), n_threads * per_thread);
let unique = all.iter().collect::<std::collections::HashSet<_>>().len();
assert_eq!(unique, all.len(), "duplicate offsets handed out");
}
fn free_blocks(alloc: &FileAllocator) -> Vec<(u64, u64)> {
alloc
.free_list
.lock()
.unwrap()
.iter()
.map(|b| (b.addr, b.len))
.collect()
}
#[test]
fn freed_block_is_reused_before_the_file_grows() {
let alloc = FileAllocator::new(0);
let a = alloc.allocate(64);
alloc.allocate(64);
let eof_before = alloc.eof();
alloc.free(a, 64);
assert_eq!(alloc.allocate(64), a, "exact-fit reuse");
assert_eq!(alloc.eof(), eof_before, "file must not grow on reuse");
assert!(free_blocks(&alloc).is_empty());
}
#[test]
fn reusing_part_of_a_block_leaves_an_aligned_remainder() {
let alloc = FileAllocator::new(0);
let a = alloc.allocate(64);
alloc.allocate(8);
let eof_before = alloc.eof();
alloc.free(a, 64);
assert_eq!(alloc.allocate(10), a);
assert_eq!(free_blocks(&alloc), vec![(a + 16, 48)]);
assert_eq!(alloc.allocate(48), a + 16);
assert_eq!(alloc.eof(), eof_before);
}
#[test]
fn a_request_larger_than_every_free_block_grows_the_file() {
let alloc = FileAllocator::new(0);
let a = alloc.allocate(32);
alloc.allocate(32);
let eof_before = alloc.eof();
alloc.free(a, 32);
let big = alloc.allocate(33);
assert_eq!(big, eof_before, "must come from the end of the file");
assert_eq!(
free_blocks(&alloc),
vec![(a, 32)],
"the block that did not fit stays available"
);
}
#[test]
fn best_fit_picks_the_smallest_sufficient_block() {
let alloc = FileAllocator::new(0);
let small = alloc.allocate(16);
alloc.allocate(8);
let mid = alloc.allocate(32);
alloc.allocate(8);
let big = alloc.allocate(64);
alloc.allocate(8);
alloc.free(big, 64);
alloc.free(small, 16);
alloc.free(mid, 32);
assert_eq!(alloc.allocate(20), mid, "20 fits 32 more tightly than 64");
assert_eq!(alloc.allocate(16), small);
assert_eq!(alloc.allocate(64), big);
}
#[test]
fn adjacent_freed_blocks_merge() {
let alloc = FileAllocator::new(0);
let a = alloc.allocate(32);
let b = alloc.allocate(32);
let c = alloc.allocate(32);
alloc.allocate(8);
alloc.free(a, 32);
alloc.free(c, 32);
assert_eq!(free_blocks(&alloc), vec![(a, 32), (c, 32)]);
alloc.free(b, 32);
assert_eq!(free_blocks(&alloc), vec![(a, 96)]);
let eof_before = alloc.eof();
assert_eq!(alloc.allocate(96), a);
assert_eq!(alloc.eof(), eof_before);
}
#[test]
fn freeing_nothing_is_a_no_op() {
let alloc = FileAllocator::new(0);
let a = alloc.allocate(16);
alloc.free(a, 0);
assert!(free_blocks(&alloc).is_empty());
alloc.free(a, 16);
assert_eq!(alloc.allocate(0), alloc.eof());
assert_eq!(free_blocks(&alloc), vec![(a, 16)]);
}
}