use std::collections::HashSet;
use crate::error::Result;
use crate::page::PAGE_ID_NONE;
use crate::transaction::TransactionManager;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct DefragOptions {
pub sparse_threshold: f64,
pub max_values: usize,
}
impl Default for DefragOptions {
fn default() -> DefragOptions {
DefragOptions {
sparse_threshold: 0.25,
max_values: 0,
}
}
}
impl DefragOptions {
pub fn sparse_threshold(mut self, threshold: f64) -> Self {
self.sparse_threshold = threshold;
self
}
pub fn max_values(mut self, cap: usize) -> Self {
self.max_values = cap;
self
}
}
#[must_use = "DefragStats reports what the sweep did (pages_examined/pages_freed); inspect it or bind with `let _`"]
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct DefragStats {
pub pages_examined: u64,
pub pages_freed: u64,
pub values_moved: u64,
pub freemap_orphans_reclaimed: u64,
}
pub fn defrag(txm: &mut TransactionManager, options: &DefragOptions) -> Result<DefragStats> {
if !txm.is_active() {
return Err(crate::error::ChiselError::NoActiveTransaction);
}
let mut stats = DefragStats {
pages_examined: 0,
pages_freed: 0,
values_moved: 0,
freemap_orphans_reclaimed: 0,
};
if txm.current_handle_table_root_page() == PAGE_ID_NONE {
return Ok(stats);
}
let sparse_pages: HashSet<u64> = txm.sparse_data_pages(options.sparse_threshold)?;
if !sparse_pages.is_empty() {
let initial_page_ids = txm.data_page_ids_snapshot();
let handles = txm.handles()?;
let mut examined_pages: HashSet<u64> = HashSet::new();
for &handle in &handles {
if options.max_values > 0 && stats.values_moved >= options.max_values as u64 {
break;
}
let page_id = match txm.handle_live_page_id(handle)? {
Some(id) => id,
None => continue, };
if !sparse_pages.contains(&page_id) {
continue; }
examined_pages.insert(page_id);
let value = txm.read(handle)?;
txm.update(handle, &value)?;
stats.values_moved += 1;
}
let final_page_ids = txm.data_page_ids_snapshot();
stats.pages_examined = examined_pages.len() as u64;
stats.pages_freed = initial_page_ids.difference(&final_page_ids).count() as u64;
}
stats.freemap_orphans_reclaimed = txm.reclaim_freemap_orphans()?;
Ok(stats)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::page::PAGE_SIZE;
use crate::page_cache::PageCache;
use crate::page_io::PageIo;
fn fresh_manager() -> TransactionManager {
let file = tempfile::NamedTempFile::new().unwrap();
let io = PageIo::open(file.path(), false).unwrap();
let cache = PageCache::new(
io,
1024 * PAGE_SIZE as u64,
0,
crate::DrainInsertion::LruTail,
crate::SpillwayLocation::InMemory,
);
let mut tm = TransactionManager::create_new(cache, 2, None, None).unwrap();
tm.begin().unwrap();
tm.commit().unwrap();
tm
}
#[test]
fn defrag_reports_freemap_orphans_reclaimed() {
let mut tm = fresh_manager();
let big: Vec<u8> = vec![0xCD; 1 << 14];
tm.begin().unwrap();
let h = tm.allocate(&big).unwrap();
tm.commit().unwrap();
tm.begin().unwrap();
tm.delete(h).unwrap();
tm.commit().unwrap();
let orphan = tm.test_forge_freemap_orphan().unwrap();
tm.begin().unwrap();
let stats = defrag(&mut tm, &DefragOptions::default()).unwrap();
tm.commit().unwrap();
assert!(
stats.freemap_orphans_reclaimed >= 1,
"defrag must report the forged orphan as reclaimed, got {}",
stats.freemap_orphans_reclaimed
);
tm.begin().unwrap();
let again = defrag(&mut tm, &DefragOptions::default()).unwrap();
tm.commit().unwrap();
assert_eq!(
again.freemap_orphans_reclaimed, 0,
"second sweep must find no orphans (idempotent)"
);
let _ = orphan;
}
#[test]
fn defrag_compact_and_sweep_excludes_structural_superseded() {
let mut tm = fresh_manager();
let value: Vec<u8> = vec![0xAB; 400];
let mut all_handles: Vec<u64> = Vec::new();
tm.begin().unwrap();
for _ in 0..60 {
all_handles.push(tm.allocate(&value).unwrap());
}
tm.commit().unwrap();
let kept: Vec<u64> = all_handles
.iter()
.copied()
.enumerate()
.filter(|(i, _)| i % 4 == 0)
.map(|(_, h)| h)
.collect();
let deleted: Vec<u64> = all_handles
.iter()
.copied()
.enumerate()
.filter(|(i, _)| i % 4 != 0)
.map(|(_, h)| h)
.collect();
tm.begin().unwrap();
for h in &deleted {
tm.delete(*h).unwrap();
}
tm.commit().unwrap();
let orphan = tm.test_forge_freemap_orphan().unwrap();
tm.begin().unwrap();
let opts = DefragOptions::default().sparse_threshold(0.5);
let stats = defrag(&mut tm, &opts).unwrap();
assert!(
stats.values_moved > 0,
"precondition: step 5 must have relocated at least one value (got 0); \
increase the value count or lower the kept fraction so pages are sparse"
);
assert_eq!(
stats.freemap_orphans_reclaimed, 1,
"exactly the forged orphan must be reclaimed (not a COW'd leaf from \
step 5); got {}",
stats.freemap_orphans_reclaimed
);
tm.commit().unwrap();
tm.begin().unwrap();
for h in &kept {
let v = tm.read(*h).unwrap();
assert_eq!(
v, value,
"handle {h} returned wrong data after compact+sweep defrag"
);
}
tm.commit().unwrap();
let _ = orphan;
}
}