use super::page::{Page, PageType, HEADER_SIZE, PAGE_SIZE};
pub const FREE_IDS_PER_TRUNK: usize = (PAGE_SIZE - HEADER_SIZE - 8) / 4;
const NEXT_TRUNK_OFFSET: usize = HEADER_SIZE;
const COUNT_OFFSET: usize = HEADER_SIZE + 4;
const PAGE_IDS_OFFSET: usize = HEADER_SIZE + 8;
#[derive(Debug, Clone)]
pub enum FreeListError {
Empty,
Corrupted(String),
InvalidTrunk(u32),
}
impl std::fmt::Display for FreeListError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => write!(f, "Freelist is empty"),
Self::Corrupted(msg) => write!(f, "Freelist corrupted: {}", msg),
Self::InvalidTrunk(id) => write!(f, "Invalid trunk page: {}", id),
}
}
}
impl std::error::Error for FreeListError {}
#[derive(Debug)]
pub struct FreeList {
trunk_head: u32,
free_pages: Vec<u32>,
total_free: u32,
dirty: bool,
}
impl FreeList {
pub fn new() -> Self {
Self {
trunk_head: 0,
free_pages: Vec::new(),
total_free: 0,
dirty: false,
}
}
pub fn from_header(trunk_head: u32, total_free: u32) -> Self {
Self {
trunk_head,
free_pages: Vec::new(),
total_free,
dirty: false,
}
}
pub fn trunk_head(&self) -> u32 {
self.trunk_head
}
pub fn total_free(&self) -> u32 {
self.total_free
}
pub fn is_empty(&self) -> bool {
self.free_pages.is_empty() && self.trunk_head == 0
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn mark_clean(&mut self) {
self.dirty = false;
}
pub fn allocate(&mut self) -> Option<u32> {
if let Some(page_id) = self.free_pages.pop() {
self.total_free = self.total_free.saturating_sub(1);
self.dirty = true;
return Some(page_id);
}
None
}
pub fn free(&mut self, page_id: u32) {
self.free_pages.push(page_id);
self.total_free += 1;
self.dirty = true;
}
pub fn free_batch(&mut self, page_ids: &[u32]) {
self.free_pages.extend_from_slice(page_ids);
self.total_free += page_ids.len() as u32;
self.dirty = true;
}
pub fn in_memory_count(&self) -> usize {
self.free_pages.len()
}
pub fn load_from_trunk(&mut self, trunk: &Page) -> Result<Option<u32>, FreeListError> {
if trunk
.page_type()
.map_err(|_| FreeListError::InvalidTrunk(trunk.page_id()))?
!= PageType::FreelistTrunk
{
return Err(FreeListError::InvalidTrunk(trunk.page_id()));
}
let data = trunk.as_bytes();
let next_trunk = u32::from_le_bytes([
data[NEXT_TRUNK_OFFSET],
data[NEXT_TRUNK_OFFSET + 1],
data[NEXT_TRUNK_OFFSET + 2],
data[NEXT_TRUNK_OFFSET + 3],
]);
let count = u32::from_le_bytes([
data[COUNT_OFFSET],
data[COUNT_OFFSET + 1],
data[COUNT_OFFSET + 2],
data[COUNT_OFFSET + 3],
]) as usize;
if count > FREE_IDS_PER_TRUNK {
return Err(FreeListError::Corrupted(format!(
"Trunk has {} entries, max is {}",
count, FREE_IDS_PER_TRUNK
)));
}
self.free_pages.push(trunk.page_id());
for i in 0..count {
let offset = PAGE_IDS_OFFSET + i * 4;
let page_id = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
self.free_pages.push(page_id);
}
self.total_free = self.total_free.saturating_add(count as u32 + 1);
self.dirty = true;
self.trunk_head = next_trunk;
Ok(if next_trunk != 0 {
Some(next_trunk)
} else {
None
})
}
pub fn create_trunk(&mut self, trunk_page_id: u32, next_trunk: u32) -> Page {
let mut trunk = Page::new(PageType::FreelistTrunk, trunk_page_id);
let data = trunk.as_bytes_mut();
data[NEXT_TRUNK_OFFSET..NEXT_TRUNK_OFFSET + 4].copy_from_slice(&next_trunk.to_le_bytes());
let count = self.free_pages.len().min(FREE_IDS_PER_TRUNK);
data[COUNT_OFFSET..COUNT_OFFSET + 4].copy_from_slice(&(count as u32).to_le_bytes());
for i in 0..count {
let page_id = self.free_pages.pop().unwrap();
let offset = PAGE_IDS_OFFSET + i * 4;
data[offset..offset + 4].copy_from_slice(&page_id.to_le_bytes());
}
trunk.update_checksum();
self.dirty = true;
trunk
}
pub fn flush_to_trunks(
&mut self,
threshold: usize,
mut allocate_page: impl FnMut() -> u32,
) -> Vec<Page> {
let mut trunks = Vec::new();
while self.free_pages.len() > threshold {
let trunk_page_id = allocate_page();
let trunk = self.create_trunk(trunk_page_id, self.trunk_head);
self.trunk_head = trunk_page_id;
trunks.push(trunk);
}
trunks
}
pub fn merge_all_trunks(
&mut self,
load_page: impl Fn(u32) -> Option<Page>,
) -> Result<Vec<u32>, FreeListError> {
let mut reclaimed_trunks = Vec::new();
while self.trunk_head != 0 {
let trunk_id = self.trunk_head;
let trunk = load_page(trunk_id).ok_or(FreeListError::InvalidTrunk(trunk_id))?;
self.load_from_trunk(&trunk)?;
reclaimed_trunks.push(trunk_id);
}
Ok(reclaimed_trunks)
}
}
impl Default for FreeList {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_freelist_basic() {
let mut fl = FreeList::new();
assert!(fl.is_empty());
assert_eq!(fl.total_free(), 0);
fl.free(10);
fl.free(20);
fl.free(30);
assert!(!fl.is_empty());
assert_eq!(fl.total_free(), 3);
assert_eq!(fl.allocate(), Some(30));
assert_eq!(fl.allocate(), Some(20));
assert_eq!(fl.allocate(), Some(10));
assert_eq!(fl.allocate(), None);
assert!(fl.is_empty());
}
#[test]
fn test_freelist_batch() {
let mut fl = FreeList::new();
fl.free_batch(&[1, 2, 3, 4, 5]);
assert_eq!(fl.total_free(), 5);
assert_eq!(fl.in_memory_count(), 5);
}
#[test]
fn test_freelist_dirty() {
let mut fl = FreeList::new();
assert!(!fl.is_dirty());
fl.free(1);
assert!(fl.is_dirty());
fl.mark_clean();
assert!(!fl.is_dirty());
fl.allocate();
assert!(fl.is_dirty());
}
#[test]
fn test_trunk_page_creation() {
let mut fl = FreeList::new();
for i in 0..100 {
fl.free(i);
}
let trunk = fl.create_trunk(999, 0);
assert_eq!(trunk.page_type().unwrap(), PageType::FreelistTrunk);
assert_eq!(trunk.page_id(), 999);
assert!(fl.in_memory_count() < 100);
}
#[test]
fn test_trunk_page_load() {
let mut fl = FreeList::new();
for i in 0..50 {
fl.free(i);
}
let trunk = fl.create_trunk(999, 0);
let pages_in_trunk = 50 - fl.in_memory_count();
fl.free_pages.clear();
fl.load_from_trunk(&trunk).unwrap();
assert_eq!(fl.in_memory_count(), pages_in_trunk + 1);
}
#[test]
fn test_trunk_page_reuse() {
let mut original = FreeList::new();
for i in 0..8 {
original.free(i);
}
let trunk = original.create_trunk(999, 0);
let mut fl = FreeList::new();
fl.load_from_trunk(&trunk).unwrap();
let mut ids = Vec::new();
while let Some(id) = fl.allocate() {
ids.push(id);
}
assert!(ids.contains(&999));
}
#[test]
fn test_trunk_chain() {
let mut fl = FreeList::new();
for i in 0..2000 {
fl.free(i);
}
let mut next_trunk_id = 10000u32;
let trunks = fl.flush_to_trunks(100, || {
let id = next_trunk_id;
next_trunk_id += 1;
id
});
assert!(!trunks.is_empty());
assert!(fl.in_memory_count() <= 100);
}
#[test]
fn test_free_ids_per_trunk() {
assert_eq!(FREE_IDS_PER_TRUNK, 1014);
assert_eq!(FREE_IDS_PER_TRUNK, 1014);
}
}