use crate::error::{ChiselError, Result};
use crate::freemap::FreeMap;
use crate::page::{
self, PageType, CHECKSUM_OFFSET, DATA_PAGE_HEADER_SIZE, PAGE_BODY_SIZE, PAGE_SIZE,
};
use crate::page_cache::PageCache;
const LEAF_CAPACITY: u64 = (PAGE_BODY_SIZE * 8) as u64;
const PTR_SIZE: usize = 8;
const PTRS_PER_INTERIOR: usize = (CHECKSUM_OFFSET - DATA_PAGE_HEADER_SIZE) / PTR_SIZE;
const MAX_DEPTH: u32 = 5;
fn read_child(buf: &[u8; PAGE_SIZE], index: usize) -> u64 {
let off = DATA_PAGE_HEADER_SIZE + index * PTR_SIZE;
u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
}
fn write_child(buf: &mut [u8; PAGE_SIZE], index: usize, value: u64) {
let off = DATA_PAGE_HEADER_SIZE + index * PTR_SIZE;
buf[off..off + 8].copy_from_slice(&value.to_le_bytes());
}
fn init_interior(buf: &mut [u8; PAGE_SIZE]) {
buf.fill(0);
buf[0] = PageType::FreeMapInterior as u8;
buf[1] = page::current_version(PageType::FreeMapInterior); page::stamp_checksum(buf);
}
fn check_type(buf: &[u8; PAGE_SIZE], expected: PageType, page_id: u64) -> Result<()> {
if buf[0] != expected as u8 {
return Err(ChiselError::CorruptPage { page_id });
}
Ok(())
}
pub(crate) struct FreeMapTree {
pub root: u64,
pub depth: u32,
pub pending_superseded: Vec<u64>,
pub(crate) session_owned: rustc_hash::FxHashSet<u64>,
}
impl FreeMapTree {
pub fn from_roots(root: u64, depth: u32) -> FreeMapTree {
FreeMapTree {
root,
depth,
pending_superseded: Vec::new(),
session_owned: rustc_hash::FxHashSet::default(),
}
}
pub fn create(
cache: &mut PageCache,
extend: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<FreeMapTree> {
let root = extend(cache)?;
let buf = cache.get_mut(root)?;
FreeMap::init_page(buf);
page::stamp_checksum(buf);
let mut session_owned = rustc_hash::FxHashSet::default();
session_owned.insert(root);
Ok(FreeMapTree {
root,
depth: 0,
pending_superseded: Vec::new(),
session_owned,
})
}
fn capacity(&self) -> u64 {
let mut cap = LEAF_CAPACITY;
for _ in 0..self.depth {
cap = cap.saturating_mul(PTRS_PER_INTERIOR as u64);
}
cap
}
fn child_span(&self, level: u32) -> u64 {
let mut span = LEAF_CAPACITY;
for _ in 1..level {
span = span.saturating_mul(PTRS_PER_INTERIOR as u64);
}
span
}
fn find_leaf(&self, cache: &mut PageCache, id: u64) -> Result<Option<u64>> {
let cap = self.capacity();
if cap != u64::MAX && id >= cap {
return Ok(None);
}
if self.depth == 0 {
check_type(cache.get(self.root)?, PageType::FreeMap, self.root)?;
return Ok(Some(self.root));
}
let mut current = self.root;
let mut remaining = id;
for level in (1..=self.depth).rev() {
let buf = cache.get(current)?;
check_type(buf, PageType::FreeMapInterior, current)?;
let span = self.child_span(level);
let child_idx = (remaining / span) as usize;
if child_idx >= PTRS_PER_INTERIOR {
return Ok(None);
}
let child = read_child(buf, child_idx);
if child == 0 {
return Ok(None); }
remaining %= span;
current = child;
}
check_type(cache.get(current)?, PageType::FreeMap, current)?;
Ok(Some(current))
}
pub fn is_free(&self, cache: &mut PageCache, id: u64) -> Result<bool> {
let Some(leaf) = self.find_leaf(cache, id)? else {
return Ok(false);
};
Ok(FreeMap::is_free(cache.get(leaf)?, id % LEAF_CAPACITY))
}
pub fn grow(
&mut self,
cache: &mut PageCache,
extend: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<()> {
if self.depth >= MAX_DEPTH {
return Ok(());
}
let new_root = extend(cache)?;
let buf = cache.get_mut(new_root)?;
init_interior(buf);
write_child(buf, 0, self.root); page::stamp_checksum(buf);
self.session_owned.insert(new_root);
self.root = new_root;
self.depth += 1;
Ok(())
}
fn cow_descend(
&mut self,
cache: &mut PageCache,
id: u64,
extend: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
leaf_op: &mut dyn FnMut(&mut [u8; PAGE_SIZE], u64),
) -> Result<()> {
let root_expected = if self.depth > 0 {
PageType::FreeMapInterior
} else {
PageType::FreeMap
};
let new_root = self.cow_node(cache, self.root, root_expected, extend)?;
self.root = new_root;
let mut current = new_root;
let mut remaining = id;
for level in (1..=self.depth).rev() {
let span = self.child_span(level);
let child_idx = (remaining / span) as usize;
let child = read_child(cache.get(current)?, child_idx);
let child_expected = if level == 1 {
PageType::FreeMap
} else {
PageType::FreeMapInterior
};
let new_child = if child == 0 {
let id_new = extend(cache)?;
let buf = cache.get_mut(id_new)?;
if child_expected == PageType::FreeMap {
FreeMap::init_page(buf);
page::stamp_checksum(buf);
} else {
init_interior(buf);
}
self.session_owned.insert(id_new);
id_new
} else {
self.cow_node(cache, child, child_expected, extend)?
};
let buf = cache.get_mut(current)?;
write_child(buf, child_idx, new_child);
page::stamp_checksum(buf);
remaining %= span;
current = new_child;
}
let buf = cache.get_mut(current)?;
check_type(buf, PageType::FreeMap, current)?;
leaf_op(buf, remaining % LEAF_CAPACITY);
page::stamp_checksum(buf);
Ok(())
}
fn cow_node(
&mut self,
cache: &mut PageCache,
page_id: u64,
expected: PageType,
extend: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<u64> {
if self.session_owned.contains(&page_id) {
return Ok(page_id);
}
check_type(cache.get(page_id)?, expected, page_id)?;
let new_id = extend(cache)?;
cache.copy_page(page_id, new_id)?;
self.session_owned.insert(new_id);
self.pending_superseded.push(page_id);
Ok(new_id)
}
pub fn mark_free(
&mut self,
cache: &mut PageCache,
id: u64,
extend: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<()> {
self.cow_descend(cache, id, extend, &mut |buf, bit| {
FreeMap::mark_free(buf, bit)
})
}
fn clear_bit(
&mut self,
cache: &mut PageCache,
id: u64,
extend: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<()> {
self.cow_descend(cache, id, extend, &mut |buf, bit| {
FreeMap::clear_bit(buf, bit)
})
}
pub fn mark_free_growing(
&mut self,
cache: &mut PageCache,
id: u64,
extend: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<()> {
while self.capacity() <= id && self.depth < MAX_DEPTH {
self.grow(cache, extend)?;
}
self.mark_free(cache, id, extend)
}
pub fn allocate_first(
&mut self,
cache: &mut PageCache,
hint: &mut u64,
extend: &mut dyn FnMut(&mut PageCache) -> Result<u64>,
) -> Result<Option<u64>> {
let Some(found) = self.scan_from(cache, *hint)? else {
return Ok(None);
};
self.clear_bit(cache, found, extend)?;
*hint = found;
Ok(Some(found))
}
fn scan_from(&self, cache: &mut PageCache, lo: u64) -> Result<Option<u64>> {
self.scan_node(cache, self.root, self.depth, 0, lo)
}
fn scan_node(
&self,
cache: &mut PageCache,
page_id: u64,
level: u32,
base: u64,
lo: u64,
) -> Result<Option<u64>> {
if level == 0 {
let buf = cache.get(page_id)?;
check_type(buf, PageType::FreeMap, page_id)?;
let local_lo = lo.saturating_sub(base);
return Ok(FreeMap::first_free_bit_from(buf, local_lo).map(|b| base + b));
}
let buf = cache.get(page_id)?;
check_type(buf, PageType::FreeMapInterior, page_id)?;
let span = self.child_span(level);
let children: Vec<(usize, u64)> = (0..PTRS_PER_INTERIOR)
.map(|i| (i, read_child(buf, i)))
.filter(|(_, c)| *c != 0)
.collect();
for (i, child) in children {
let child_base = base.saturating_add((i as u64).saturating_mul(span));
let child_end = child_base.saturating_add(span);
if child_end <= lo {
continue;
}
if let Some(found) = self.scan_node(cache, child, level - 1, child_base, lo)? {
return Ok(Some(found));
}
}
Ok(None)
}
pub(crate) fn reachable_pages(
&self,
cache: &mut PageCache,
) -> Result<rustc_hash::FxHashSet<u64>> {
let mut set = rustc_hash::FxHashSet::default();
if self.root != crate::page::PAGE_ID_NONE {
self.collect_reachable(cache, self.root, self.depth, &mut set)?;
}
Ok(set)
}
fn collect_reachable(
&self,
cache: &mut PageCache,
page: u64,
level: u32,
set: &mut rustc_hash::FxHashSet<u64>,
) -> Result<()> {
set.insert(page);
if level == 0 {
check_type(cache.get(page)?, PageType::FreeMap, page)?;
return Ok(());
}
check_type(cache.get(page)?, PageType::FreeMapInterior, page)?;
let children: Vec<u64> = {
let buf = cache.get(page)?;
(0..PTRS_PER_INTERIOR)
.map(|i| read_child(buf, i))
.filter(|c| *c != 0)
.collect()
};
for child in children {
self.collect_reachable(cache, child, level - 1, set)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::page_cache::PageCache;
fn make_cache(max_pages: usize) -> PageCache {
let file = tempfile::NamedTempFile::new().unwrap();
let io = crate::page_io::PageIo::open(file.path(), false).unwrap();
let mut cache = PageCache::new(
io,
max_pages as u64 * crate::page::PAGE_SIZE as u64,
0,
crate::DrainInsertion::LruTail,
crate::SpillwayLocation::InMemory,
);
cache.set_next_page_id(2);
cache
}
fn extend(c: &mut PageCache) -> Result<u64> {
c.new_page()
}
#[test]
fn depth0_is_free_round_trip() {
let mut c = make_cache(256);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
assert_eq!(t.depth, 0);
t.mark_free(&mut c, 5, &mut extend).unwrap();
assert!(t.is_free(&mut c, 5).unwrap());
assert!(!t.is_free(&mut c, 6).unwrap());
}
#[test]
fn mark_free_materializes_absent_subtree_at_depth1() {
let mut c = make_cache(256);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
t.grow(&mut c, &mut extend).unwrap();
assert_eq!(t.depth, 1);
let id_a = LEAF_CAPACITY + 42;
let id_b = LEAF_CAPACITY + 99;
t.mark_free(&mut c, id_a, &mut extend).unwrap();
assert!(t.is_free(&mut c, id_a).unwrap());
t.mark_free(&mut c, id_b, &mut extend).unwrap();
assert!(t.is_free(&mut c, id_b).unwrap());
assert!(!t.is_free(&mut c, 7).unwrap());
}
#[test]
fn mark_free_structural_alloc_is_extend_only() {
let mut c = make_cache(256);
let count = std::cell::Cell::new(0usize);
let mut spy = |cache: &mut PageCache| -> Result<u64> {
count.set(count.get() + 1);
cache.new_page()
};
let mut t = FreeMapTree::create(&mut c, &mut spy).unwrap();
t.grow(&mut c, &mut spy).unwrap();
let before = count.get();
t.mark_free(&mut c, LEAF_CAPACITY + 7, &mut spy).unwrap();
assert!(
count.get() > before,
"materializing an absent leaf must allocate via extend"
);
}
#[test]
fn grow_increases_depth_and_preserves_existing_frees() {
let mut c = make_cache(256);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
t.mark_free(&mut c, 7, &mut extend).unwrap();
t.grow(&mut c, &mut extend).unwrap();
assert_eq!(t.depth, 1);
assert!(t.is_free(&mut c, 7).unwrap());
assert!(!t.is_free(&mut c, LEAF_CAPACITY + 3).unwrap());
}
#[test]
fn mark_free_growing_grows_to_reach_high_id() {
let mut c = make_cache(256);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
let id = LEAF_CAPACITY * 3 + 17;
t.mark_free_growing(&mut c, id, &mut extend).unwrap();
assert!(t.depth >= 1, "high id must grow the tree");
assert!(t.is_free(&mut c, id).unwrap());
}
#[test]
fn allocate_first_returns_lowest_free_and_clears_it() {
let mut c = make_cache(256);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
t.mark_free_growing(&mut c, 500, &mut extend).unwrap();
t.mark_free_growing(&mut c, 9, &mut extend).unwrap();
t.mark_free_growing(&mut c, LEAF_CAPACITY + 4, &mut extend)
.unwrap();
let mut hint = 0u64;
assert_eq!(
t.allocate_first(&mut c, &mut hint, &mut extend).unwrap(),
Some(9)
);
assert_eq!(
t.allocate_first(&mut c, &mut hint, &mut extend).unwrap(),
Some(500)
);
assert_eq!(
t.allocate_first(&mut c, &mut hint, &mut extend).unwrap(),
Some(LEAF_CAPACITY + 4)
);
assert_eq!(
t.allocate_first(&mut c, &mut hint, &mut extend).unwrap(),
None
);
assert!(!t.is_free(&mut c, 9).unwrap());
assert!(!t.is_free(&mut c, 500).unwrap());
assert!(!t.is_free(&mut c, LEAF_CAPACITY + 4).unwrap());
}
#[test]
fn descent_rejects_wrong_type_child_as_corrupt_page() {
let mut c = make_cache(256);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
t.grow(&mut c, &mut extend).unwrap(); t.mark_free(&mut c, 5, &mut extend).unwrap();
let leaf = read_child(c.get(t.root).unwrap(), 0);
{
let buf = c.get_mut(leaf).unwrap();
buf[0] = PageType::Data as u8;
page::stamp_checksum(buf);
}
let err = match t.is_free(&mut c, 5) {
Ok(v) => panic!("is_free accepted a wrong-type leaf: {v}"),
Err(e) => e,
};
assert!(
matches!(err, ChiselError::CorruptPage { .. }),
"expected CorruptPage, got {err:?}"
);
}
#[test]
fn mark_free_rejects_wrong_position_page_on_cow_spine() {
let mut c = make_cache(256);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
t.grow(&mut c, &mut extend).unwrap();
t.grow(&mut c, &mut extend).unwrap();
assert_eq!(t.depth, 2);
t.mark_free(&mut c, 5, &mut extend).unwrap();
let interior_child = read_child(c.get(t.root).unwrap(), 0);
{
let buf = c.get_mut(interior_child).unwrap();
FreeMap::init_page(buf);
page::stamp_checksum(buf);
}
let mut t2 = FreeMapTree::from_roots(t.root, t.depth);
let err = match t2.mark_free(&mut c, 5, &mut extend) {
Ok(()) => panic!("mark_free accepted a position-wrong interior child"),
Err(e) => e,
};
assert!(
matches!(err, ChiselError::CorruptPage { page_id } if page_id == interior_child),
"expected CorruptPage {{ page_id: {interior_child} }}, got {err:?}"
);
}
#[test]
fn reachable_pages_collects_every_node() {
let mut c = make_cache(256);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
t.grow(&mut c, &mut extend).unwrap(); assert_eq!(t.depth, 1);
t.mark_free(&mut c, 5, &mut extend).unwrap();
t.mark_free(&mut c, LEAF_CAPACITY + 7, &mut extend).unwrap();
let reachable = t.reachable_pages(&mut c).unwrap();
assert!(reachable.contains(&t.root), "root must be reachable");
assert_eq!(
reachable.len(),
3,
"1 root-interior + 2 materialized leaves (depth 1)"
);
for id in reachable.iter() {
assert!(*id != 0, "the superblock page must never be a tree page");
}
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(64))]
#[test]
fn prop_tree_matches_oracle(
ops in proptest::collection::vec(
(proptest::bool::ANY, 0u64..(LEAF_CAPACITY * 1200)),
1..120usize,
)
) {
let mut c = make_cache(1_000_000);
let mut t = FreeMapTree::create(&mut c, &mut extend).unwrap();
let mut oracle: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
let mut hint = 0u64;
for (is_free_op, id) in ops {
if is_free_op {
t.mark_free_growing(&mut c, id, &mut extend).unwrap();
oracle.insert(id);
hint = hint.min(id);
} else {
if let Some(&min) = oracle.iter().next() {
proptest::prop_assert!(
hint <= min,
"test hint {} ran ahead of oracle min {}", hint, min
);
}
let got = t.allocate_first(&mut c, &mut hint, &mut extend).unwrap();
let expected = oracle.iter().next().copied();
proptest::prop_assert_eq!(got, expected);
if let Some(min) = expected {
oracle.remove(&min);
}
}
t.pending_superseded.clear();
}
for id in &oracle {
proptest::prop_assert!(
t.is_free(&mut c, *id).unwrap(),
"remaining oracle id {} must read free", id
);
}
}
}
}