use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug)]
pub struct DirtyTracker {
dirty_arenas: HashSet<u32>,
dirty_slots: HashMap<u32, HashSet<u32>>,
epoch: AtomicU64,
track_slots: bool,
total_marks: u64,
checkpoint_count: u64,
}
impl DirtyTracker {
pub fn new(track_slots: bool) -> Self {
Self {
dirty_arenas: HashSet::new(),
dirty_slots: HashMap::new(),
epoch: AtomicU64::new(0),
track_slots,
total_marks: 0,
checkpoint_count: 0,
}
}
pub fn arena_level() -> Self {
Self::new(false)
}
pub fn slot_level() -> Self {
Self::new(true)
}
pub fn mark_arena_dirty(&mut self, arena_id: u32) {
self.dirty_arenas.insert(arena_id);
self.total_marks += 1;
}
pub fn mark_arenas_dirty(&mut self, arena_ids: impl IntoIterator<Item = u32>) {
for arena_id in arena_ids {
self.dirty_arenas.insert(arena_id);
self.total_marks += 1;
}
}
pub fn mark_slot_dirty(&mut self, arena_id: u32, slot_id: u32) {
self.dirty_arenas.insert(arena_id);
if self.track_slots {
self.dirty_slots
.entry(arena_id)
.or_insert_with(HashSet::new)
.insert(slot_id);
}
self.total_marks += 1;
}
pub fn is_arena_dirty(&self, arena_id: u32) -> bool {
self.dirty_arenas.contains(&arena_id)
}
pub fn is_slot_dirty(&self, arena_id: u32, slot_id: u32) -> bool {
if !self.dirty_arenas.contains(&arena_id) {
return false;
}
if !self.track_slots {
return true;
}
self.dirty_slots
.get(&arena_id)
.is_some_and(|slots| slots.contains(&slot_id))
}
pub fn dirty_arena_ids(&self) -> impl Iterator<Item = u32> + '_ {
self.dirty_arenas.iter().copied()
}
pub fn dirty_slot_ids(&self, arena_id: u32) -> Option<impl Iterator<Item = u32> + '_> {
if !self.track_slots {
return None;
}
self.dirty_slots
.get(&arena_id)
.map(|slots| slots.iter().copied())
}
pub fn dirty_arena_count(&self) -> usize {
self.dirty_arenas.len()
}
pub fn dirty_slot_count(&self) -> usize {
if !self.track_slots {
return 0;
}
self.dirty_slots.values().map(|s| s.len()).sum()
}
pub fn checkpoint_complete(&mut self) {
self.dirty_arenas.clear();
self.dirty_slots.clear();
self.epoch.fetch_add(1, Ordering::AcqRel);
self.checkpoint_count += 1;
}
pub fn clear_arenas(&mut self, arena_ids: &[u32]) {
for &arena_id in arena_ids {
self.dirty_arenas.remove(&arena_id);
self.dirty_slots.remove(&arena_id);
}
}
pub fn epoch(&self) -> u64 {
self.epoch.load(Ordering::Acquire)
}
pub fn stats(&self) -> DirtyTrackerStats {
DirtyTrackerStats {
dirty_arenas: self.dirty_arenas.len(),
dirty_slots: self.dirty_slot_count(),
total_marks: self.total_marks,
checkpoint_count: self.checkpoint_count,
epoch: self.epoch(),
track_slots: self.track_slots,
}
}
pub fn merge(&mut self, other: &DirtyTracker) {
for &arena_id in &other.dirty_arenas {
self.dirty_arenas.insert(arena_id);
}
if self.track_slots && other.track_slots {
for (&arena_id, slots) in &other.dirty_slots {
self.dirty_slots
.entry(arena_id)
.or_insert_with(HashSet::new)
.extend(slots.iter().copied());
}
}
self.total_marks += other.total_marks;
}
}
impl Default for DirtyTracker {
fn default() -> Self {
Self::arena_level()
}
}
#[derive(Debug, Clone)]
pub struct DirtyTrackerStats {
pub dirty_arenas: usize,
pub dirty_slots: usize,
pub total_marks: u64,
pub checkpoint_count: u64,
pub epoch: u64,
pub track_slots: bool,
}
impl DirtyTrackerStats {
pub fn estimate_io_savings(
&self,
total_arenas: usize,
arena_size: usize,
) -> (usize, usize, f64) {
let full_bytes = total_arenas * arena_size;
let incremental_bytes = self.dirty_arenas * arena_size;
let savings = if full_bytes > 0 {
(1.0 - (incremental_bytes as f64 / full_bytes as f64)) * 100.0
} else {
0.0
};
(full_bytes, incremental_bytes, savings)
}
}
#[derive(Debug)]
pub struct BatchDirtyTracker {
local: DirtyTracker,
batch_threshold: usize,
}
impl BatchDirtyTracker {
pub fn new(track_slots: bool, batch_threshold: usize) -> Self {
Self {
local: DirtyTracker::new(track_slots),
batch_threshold,
}
}
pub fn mark_arena_dirty(&mut self, arena_id: u32) {
self.local.mark_arena_dirty(arena_id);
}
pub fn mark_slot_dirty(&mut self, arena_id: u32, slot_id: u32) {
self.local.mark_slot_dirty(arena_id, slot_id);
}
pub fn should_merge(&self) -> bool {
self.local.dirty_arena_count() >= self.batch_threshold
}
pub fn take(&mut self) -> DirtyTracker {
let track_slots = self.local.track_slots;
std::mem::replace(&mut self.local, DirtyTracker::new(track_slots))
}
pub fn dirty_count(&self) -> usize {
self.local.dirty_arena_count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dirty_tracker_creation() {
let tracker = DirtyTracker::arena_level();
assert_eq!(tracker.dirty_arena_count(), 0);
assert_eq!(tracker.epoch(), 0);
}
#[test]
fn test_mark_arena_dirty() {
let mut tracker = DirtyTracker::arena_level();
tracker.mark_arena_dirty(1);
tracker.mark_arena_dirty(2);
tracker.mark_arena_dirty(1);
assert_eq!(tracker.dirty_arena_count(), 2);
assert!(tracker.is_arena_dirty(1));
assert!(tracker.is_arena_dirty(2));
assert!(!tracker.is_arena_dirty(3));
}
#[test]
fn test_mark_slot_dirty() {
let mut tracker = DirtyTracker::slot_level();
tracker.mark_slot_dirty(1, 100);
tracker.mark_slot_dirty(1, 200);
tracker.mark_slot_dirty(2, 50);
assert_eq!(tracker.dirty_arena_count(), 2);
assert_eq!(tracker.dirty_slot_count(), 3);
assert!(tracker.is_slot_dirty(1, 100));
assert!(tracker.is_slot_dirty(1, 200));
assert!(tracker.is_slot_dirty(2, 50));
assert!(!tracker.is_slot_dirty(1, 300));
}
#[test]
fn test_checkpoint_complete() {
let mut tracker = DirtyTracker::arena_level();
tracker.mark_arena_dirty(1);
tracker.mark_arena_dirty(2);
assert_eq!(tracker.dirty_arena_count(), 2);
assert_eq!(tracker.epoch(), 0);
tracker.checkpoint_complete();
assert_eq!(tracker.dirty_arena_count(), 0);
assert_eq!(tracker.epoch(), 1);
}
#[test]
fn test_io_savings_estimate() {
let mut tracker = DirtyTracker::arena_level();
for i in 0..5 {
tracker.mark_arena_dirty(i);
}
let stats = tracker.stats();
let (full, incremental, savings) = stats.estimate_io_savings(100, 256 * 1024);
assert_eq!(full, 100 * 256 * 1024);
assert_eq!(incremental, 5 * 256 * 1024);
assert!((savings - 95.0).abs() < 0.01);
}
#[test]
fn test_dirty_tracker_merge() {
let mut tracker1 = DirtyTracker::slot_level();
let mut tracker2 = DirtyTracker::slot_level();
tracker1.mark_slot_dirty(1, 100);
tracker1.mark_slot_dirty(2, 200);
tracker2.mark_slot_dirty(2, 300); tracker2.mark_slot_dirty(3, 400);
tracker1.merge(&tracker2);
assert_eq!(tracker1.dirty_arena_count(), 3); assert_eq!(tracker1.dirty_slot_count(), 4); }
#[test]
fn test_batch_dirty_tracker() {
let mut batch = BatchDirtyTracker::new(false, 10);
for i in 0..15 {
batch.mark_arena_dirty(i);
}
assert!(batch.should_merge());
assert_eq!(batch.dirty_count(), 15);
let taken = batch.take();
assert_eq!(taken.dirty_arena_count(), 15);
assert_eq!(batch.dirty_count(), 0); }
#[test]
fn test_clear_specific_arenas() {
let mut tracker = DirtyTracker::arena_level();
tracker.mark_arena_dirty(1);
tracker.mark_arena_dirty(2);
tracker.mark_arena_dirty(3);
tracker.clear_arenas(&[1, 3]);
assert!(!tracker.is_arena_dirty(1));
assert!(tracker.is_arena_dirty(2));
assert!(!tracker.is_arena_dirty(3));
}
#[test]
fn test_mark_arenas_dirty_bulk() {
let mut tracker = DirtyTracker::slot_level();
tracker.mark_arenas_dirty([1, 5, 10, 42]);
assert!(tracker.is_arena_dirty(1));
assert!(tracker.is_arena_dirty(5));
assert!(tracker.is_arena_dirty(10));
assert!(tracker.is_arena_dirty(42));
assert!(!tracker.is_arena_dirty(0));
assert!(!tracker.is_arena_dirty(100));
assert_eq!(tracker.dirty_arena_count(), 4);
assert_eq!(tracker.stats().total_marks, 4);
}
#[test]
fn test_mark_arenas_dirty_empty() {
let mut tracker = DirtyTracker::slot_level();
tracker.mark_arenas_dirty(std::iter::empty());
assert_eq!(tracker.dirty_arena_count(), 0);
assert_eq!(tracker.stats().total_marks, 0);
}
#[test]
fn test_mark_arenas_dirty_duplicates() {
let mut tracker = DirtyTracker::slot_level();
tracker.mark_arenas_dirty([1, 2, 1, 2, 1]);
assert_eq!(tracker.dirty_arena_count(), 2);
assert_eq!(tracker.stats().total_marks, 5);
}
}