use std::{
sync::atomic::{AtomicU32, AtomicU64, Ordering},
time::{Duration, Instant},
};
use squib_arch::PageGeometry;
use tracing::{debug, info};
use crate::error::SnapshotError;
pub const DEFAULT_STEP_DOWN_THRESHOLD: u32 = 32;
pub const ADAPTIVE_WINDOW: Duration = Duration::from_millis(100);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackingGranule {
Coarse,
Fine,
}
impl TrackingGranule {
#[must_use]
pub const fn page_size(self, geom: PageGeometry) -> u64 {
match self {
Self::Coarse => geom.tracking_page_default,
Self::Fine => geom.tracking_page_hot,
}
}
}
#[derive(Debug)]
pub struct DirtyBitmap {
ram_start: u64,
ram_size: u64,
page_size: u64,
page_count: u64,
words: Box<[AtomicU64]>,
}
impl DirtyBitmap {
pub fn new(ram_start: u64, ram_size: u64, page_size: u64) -> Result<Self, SnapshotError> {
if !page_size.is_power_of_two() {
return Err(SnapshotError::InvalidPath(format!(
"page_size must be a power of two: {page_size}"
)));
}
if ram_size == 0 {
return Err(SnapshotError::InvalidPath(
"dirty bitmap requires a non-empty RAM range".into(),
));
}
if ram_start.checked_add(ram_size).is_none() {
return Err(SnapshotError::InvalidPath(format!(
"RAM range overflows: start={ram_start:#x}, size={ram_size:#x}"
)));
}
let page_count = ram_size.div_ceil(page_size);
let word_count = usize::try_from(page_count.div_ceil(64)).map_err(|_| {
SnapshotError::InvalidPath(format!(
"bitmap word count exceeds usize::MAX (page_count={page_count})"
))
})?;
let words: Vec<AtomicU64> = (0..word_count).map(|_| AtomicU64::new(0)).collect();
Ok(Self {
ram_start,
ram_size,
page_size,
page_count,
words: words.into_boxed_slice(),
})
}
#[must_use]
pub const fn ram_start(&self) -> u64 {
self.ram_start
}
#[must_use]
pub const fn ram_size(&self) -> u64 {
self.ram_size
}
#[must_use]
pub const fn page_size(&self) -> u64 {
self.page_size
}
#[must_use]
pub const fn page_count(&self) -> u64 {
self.page_count
}
#[must_use]
pub fn page_shift(&self) -> u32 {
self.page_size.trailing_zeros()
}
#[must_use]
pub fn page_index_of(&self, addr: u64) -> Option<u64> {
if addr < self.ram_start {
return None;
}
let offset = addr - self.ram_start;
if offset >= self.ram_size {
return None;
}
Some(offset >> self.page_shift())
}
pub fn set_dirty(&self, addr: u64) -> bool {
let Some(page) = self.page_index_of(addr) else {
return false;
};
self.set_dirty_by_index(page)
}
pub fn set_dirty_by_index(&self, page: u64) -> bool {
if page >= self.page_count {
return false;
}
let word_idx = (page / 64) as usize;
let bit = page % 64;
let mask = 1u64 << bit;
let prev = self.words[word_idx].fetch_or(mask, Ordering::Relaxed);
prev & mask == 0
}
#[must_use]
pub fn is_dirty(&self, addr: u64) -> bool {
let Some(page) = self.page_index_of(addr) else {
return false;
};
self.is_dirty_by_index(page)
}
#[must_use]
pub fn is_dirty_by_index(&self, page: u64) -> bool {
if page >= self.page_count {
return false;
}
let word_idx = (page / 64) as usize;
let bit = page % 64;
let word = self.words[word_idx].load(Ordering::Acquire);
word & (1u64 << bit) != 0
}
pub fn drain(&self) -> Vec<u64> {
let mut out = Vec::new();
for (word_idx, word) in self.words.iter().enumerate() {
let mut bits = word.swap(0, Ordering::Acquire);
while bits != 0 {
let bit = u64::from(bits.trailing_zeros());
bits &= bits - 1;
let page = (word_idx as u64) * 64 + bit;
if page < self.page_count {
out.push(page);
}
}
}
out
}
pub fn drain_into<F: FnMut(u64)>(&self, mut callback: F) {
for (word_idx, word) in self.words.iter().enumerate() {
let mut bits = word.swap(0, Ordering::Acquire);
while bits != 0 {
let bit = u64::from(bits.trailing_zeros());
bits &= bits - 1;
let page = (word_idx as u64) * 64 + bit;
if page < self.page_count {
callback(page);
}
}
}
}
pub fn fold_into_finer(&self, fine: &DirtyBitmap) -> Result<(), SnapshotError> {
if fine.ram_start != self.ram_start || fine.ram_size != self.ram_size {
return Err(SnapshotError::InvalidPath(
"fold_into_finer requires identical ranges".into(),
));
}
if fine.page_size >= self.page_size {
return Err(SnapshotError::InvalidPath(
"fold_into_finer expects a strictly smaller page size".into(),
));
}
if !self.page_size.is_multiple_of(fine.page_size) {
return Err(SnapshotError::InvalidPath(
"coarse page must be a multiple of fine page".into(),
));
}
let ratio = self.page_size / fine.page_size;
for (word_idx, word) in self.words.iter().enumerate() {
let bits = word.load(Ordering::Acquire);
if bits == 0 {
continue;
}
let mut bits = bits;
while bits != 0 {
let bit = u64::from(bits.trailing_zeros());
bits &= bits - 1;
let coarse_page = (word_idx as u64) * 64 + bit;
if coarse_page >= self.page_count {
break;
}
let fine_start = coarse_page * ratio;
let fine_end = (fine_start + ratio).min(fine.page_count);
for fine_page in fine_start..fine_end {
fine.set_dirty_by_index(fine_page);
}
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct AdaptiveController {
threshold: u32,
window: Duration,
block_faults: Box<[AtomicU32]>,
last_reset: parking_lot::Mutex<Instant>,
granule: parking_lot::RwLock<TrackingGranule>,
}
impl AdaptiveController {
#[must_use]
pub fn new(block_count: u64) -> Self {
Self::with_threshold(block_count, DEFAULT_STEP_DOWN_THRESHOLD)
}
#[must_use]
pub fn with_threshold(block_count: u64, threshold: u32) -> Self {
let len = usize::try_from(block_count).unwrap_or(usize::MAX);
let blocks: Vec<AtomicU32> = (0..len).map(|_| AtomicU32::new(0)).collect();
Self {
threshold,
window: ADAPTIVE_WINDOW,
block_faults: blocks.into_boxed_slice(),
last_reset: parking_lot::Mutex::new(Instant::now()),
granule: parking_lot::RwLock::new(TrackingGranule::Coarse),
}
}
pub fn granule(&self) -> TrackingGranule {
*self.granule.read()
}
pub fn record_fault(&self, block_idx: u64) {
let Ok(idx) = usize::try_from(block_idx) else {
return;
};
let Some(slot) = self.block_faults.get(idx) else {
return;
};
slot.fetch_add(1, Ordering::Relaxed);
}
pub fn tick(&self) -> TrackingGranule {
let mut last = self.last_reset.lock();
let now = Instant::now();
if now.duration_since(*last) < self.window {
return self.granule();
}
let mut max_in_window: u32 = 0;
for slot in &self.block_faults {
let count = slot.swap(0, Ordering::Acquire);
if count > max_in_window {
max_in_window = count;
}
}
*last = now;
drop(last);
let mut current = self.granule.write();
if max_in_window > self.threshold && *current == TrackingGranule::Coarse {
info!(
hottest_block_faults = max_in_window,
threshold = self.threshold,
"dirty tracker stepping down to fine granule (16 KiB)"
);
*current = TrackingGranule::Fine;
} else {
debug!(
hottest_block_faults = max_in_window,
granule = ?*current,
"dirty tracker tick"
);
}
*current
}
}
impl Default for AdaptiveController {
fn default() -> Self {
Self::new(1)
}
}
#[derive(Debug)]
pub struct TrackedRegion {
bitmap: parking_lot::RwLock<std::sync::Arc<DirtyBitmap>>,
controller: AdaptiveController,
geometry: PageGeometry,
}
impl TrackedRegion {
pub fn new(
ram_start: u64,
ram_size: u64,
geometry: PageGeometry,
) -> Result<Self, SnapshotError> {
let bitmap = DirtyBitmap::new(ram_start, ram_size, geometry.tracking_page_default)?;
let block_count = bitmap.page_count();
Ok(Self {
bitmap: parking_lot::RwLock::new(std::sync::Arc::new(bitmap)),
controller: AdaptiveController::new(block_count),
geometry,
})
}
pub fn with_threshold(
ram_start: u64,
ram_size: u64,
geometry: PageGeometry,
threshold: u32,
) -> Result<Self, SnapshotError> {
let bitmap = DirtyBitmap::new(ram_start, ram_size, geometry.tracking_page_default)?;
let block_count = bitmap.page_count();
Ok(Self {
bitmap: parking_lot::RwLock::new(std::sync::Arc::new(bitmap)),
controller: AdaptiveController::with_threshold(block_count, threshold),
geometry,
})
}
pub fn bitmap(&self) -> std::sync::Arc<DirtyBitmap> {
self.bitmap.read().clone()
}
pub fn granule(&self) -> TrackingGranule {
self.controller.granule()
}
pub fn mark_dirty(&self, addr: u64) -> bool {
let coarse_block_idx = addr
.checked_sub(self.bitmap.read().ram_start())
.map(|off| off / self.geometry.tracking_page_default);
if let Some(block_idx) = coarse_block_idx {
self.controller.record_fault(block_idx);
}
let bitmap = self.bitmap.read().clone();
bitmap.set_dirty(addr)
}
pub fn tick_adaptive(&self) -> TrackingGranule {
let new_granule = self.controller.tick();
let mut current = self.bitmap.write();
let active_size = current.page_size();
let target_size = new_granule.page_size(self.geometry);
if target_size != active_size {
let Ok(fine) = DirtyBitmap::new(current.ram_start(), current.ram_size(), target_size)
else {
return self.controller.granule();
};
if current.fold_into_finer(&fine).is_ok() {
*current = std::sync::Arc::new(fine);
}
}
new_granule
}
}
#[cfg(test)]
mod tests {
use super::*;
const REGION_BASE: u64 = 0x8000_0000;
const REGION_SIZE: u64 = 0x1000_0000; const PAGE_2M: u64 = 2 * 1024 * 1024;
const PAGE_16K: u64 = 16 * 1024;
#[test]
fn test_should_count_pages_with_2mib_default() {
let bm = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
assert_eq!(bm.page_count(), 128); }
#[test]
fn test_should_track_a_dirty_page_at_an_aligned_address() {
let bm = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
let target = REGION_BASE + 5 * PAGE_2M;
assert!(!bm.is_dirty(target));
let transitioned = bm.set_dirty(target);
assert!(transitioned);
assert!(bm.is_dirty(target));
let again = bm.set_dirty(target);
assert!(!again, "set_dirty must report transition only once");
}
#[test]
fn test_should_round_an_unaligned_address_into_its_page() {
let bm = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
let aligned = REGION_BASE + 3 * PAGE_2M;
let unaligned = aligned + 0x1234;
bm.set_dirty(unaligned);
assert!(bm.is_dirty(aligned));
}
#[test]
fn test_should_reject_addresses_outside_the_tracked_range() {
let bm = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
assert!(!bm.set_dirty(REGION_BASE - 1));
assert!(!bm.set_dirty(REGION_BASE + REGION_SIZE));
}
#[test]
fn test_should_drain_dirty_pages_in_index_order() {
let bm = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
for &page in &[3u64, 100, 0, 64] {
bm.set_dirty_by_index(page);
}
let mut drained = bm.drain();
drained.sort_unstable();
assert_eq!(drained, vec![0, 3, 64, 100]);
assert!(!bm.is_dirty(REGION_BASE));
assert_eq!(bm.drain(), Vec::<u64>::new());
}
#[test]
fn test_should_drain_into_callback_without_allocation() {
let bm = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
bm.set_dirty_by_index(0);
bm.set_dirty_by_index(127);
let mut count = 0;
bm.drain_into(|_p| count += 1);
assert_eq!(count, 2);
}
#[test]
fn test_should_reject_non_power_of_two_page_size() {
let res = DirtyBitmap::new(REGION_BASE, REGION_SIZE, 3000);
assert!(matches!(res, Err(SnapshotError::InvalidPath(_))));
}
#[test]
fn test_should_reject_zero_size_range() {
let res = DirtyBitmap::new(REGION_BASE, 0, PAGE_2M);
assert!(matches!(res, Err(SnapshotError::InvalidPath(_))));
}
#[test]
fn test_should_fold_coarse_bits_into_fine_bitmap() {
let coarse = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
let fine = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_16K).unwrap();
coarse.set_dirty_by_index(2); coarse.fold_into_finer(&fine).unwrap();
assert!(fine.is_dirty(REGION_BASE + 2 * PAGE_2M));
assert!(fine.is_dirty(REGION_BASE + 2 * PAGE_2M + 100 * PAGE_16K));
assert!(!fine.is_dirty(REGION_BASE + PAGE_2M));
}
#[test]
fn test_should_reject_fold_with_mismatched_ranges() {
let coarse = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
let fine = DirtyBitmap::new(REGION_BASE + PAGE_16K, REGION_SIZE, PAGE_16K).unwrap();
let res = coarse.fold_into_finer(&fine);
assert!(matches!(res, Err(SnapshotError::InvalidPath(_))));
}
#[test]
fn test_should_reject_fold_when_target_is_not_finer() {
let a = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
let b = DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_2M).unwrap();
let res = a.fold_into_finer(&b);
assert!(matches!(res, Err(SnapshotError::InvalidPath(_))));
}
#[test]
fn test_should_promote_to_fine_when_one_block_exceeds_threshold() {
let region =
TrackedRegion::with_threshold(REGION_BASE, REGION_SIZE, PageGeometry::APPLE_SILICON, 2)
.unwrap();
assert_eq!(region.granule(), TrackingGranule::Coarse);
for _ in 0..10 {
region.mark_dirty(REGION_BASE);
}
{
let mut last = region.controller.last_reset.lock();
*last = Instant::now()
.checked_sub(ADAPTIVE_WINDOW * 2)
.expect("test fixture: monotonic clock starts above the window length");
}
let g = region.tick_adaptive();
assert_eq!(g, TrackingGranule::Fine);
let bm = region.bitmap();
assert_eq!(bm.page_size(), PAGE_16K);
}
#[test]
fn test_should_not_promote_when_faults_are_spread_uniformly_across_blocks() {
let region =
TrackedRegion::with_threshold(REGION_BASE, REGION_SIZE, PageGeometry::APPLE_SILICON, 4)
.unwrap();
assert_eq!(region.granule(), TrackingGranule::Coarse);
for block in 0..64u64 {
region.mark_dirty(REGION_BASE + block * PAGE_2M);
}
{
let mut last = region.controller.last_reset.lock();
*last = Instant::now()
.checked_sub(ADAPTIVE_WINDOW * 2)
.expect("test fixture: monotonic clock starts above the window length");
}
let g = region.tick_adaptive();
assert_eq!(
g,
TrackingGranule::Coarse,
"step-down must use per-block counts, not aggregate fault count"
);
}
#[test]
fn test_should_be_thread_safe_under_concurrent_set_dirty() {
let bm = std::sync::Arc::new(DirtyBitmap::new(REGION_BASE, REGION_SIZE, PAGE_16K).unwrap());
let mut handles = vec![];
for t in 0..8u64 {
let bm = bm.clone();
handles.push(std::thread::spawn(move || {
for i in 0..1000u64 {
let page_idx = t * 1000 + i;
let addr = REGION_BASE + page_idx * PAGE_16K;
if addr < REGION_BASE + REGION_SIZE {
bm.set_dirty(addr);
}
}
}));
}
for h in handles {
h.join().unwrap();
}
let drained = bm.drain();
assert_eq!(drained.len(), 8000);
}
}