use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone)]
pub struct Page {
pub page_id: u64,
pub data: Vec<u8>,
pub dirty: bool,
pub access_count: u64,
}
impl Page {
fn new(page_id: u64, data: Vec<u8>) -> Self {
Self {
page_id,
data,
dirty: false,
access_count: 0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PageCacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub dirty_evictions: u64,
}
pub struct PageCache {
capacity: usize,
pages: HashMap<u64, Page>,
lru_order: VecDeque<u64>,
stats: PageCacheStats,
pinned: HashSet<u64>,
}
impl PageCache {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "PageCache capacity must be > 0");
Self {
capacity,
pages: HashMap::new(),
lru_order: VecDeque::new(),
stats: PageCacheStats::default(),
pinned: HashSet::new(),
}
}
fn touch(&mut self, page_id: u64) {
if let Some(pos) = self.lru_order.iter().position(|&id| id == page_id) {
self.lru_order.remove(pos);
}
self.lru_order.push_front(page_id);
}
pub fn get(&mut self, page_id: u64) -> Option<&Page> {
if self.pages.contains_key(&page_id) {
self.stats.hits += 1;
let page = self.pages.get_mut(&page_id)?;
page.access_count += 1;
self.touch(page_id);
self.pages.get(&page_id)
} else {
self.stats.misses += 1;
None
}
}
pub fn get_mut(&mut self, page_id: u64) -> Option<&mut Page> {
if self.pages.contains_key(&page_id) {
self.stats.hits += 1;
self.touch(page_id);
let page = self.pages.get_mut(&page_id)?;
page.access_count += 1;
Some(page)
} else {
self.stats.misses += 1;
None
}
}
pub fn insert(&mut self, page_id: u64, data: Vec<u8>) -> Option<Page> {
if self.pages.contains_key(&page_id) {
self.touch(page_id);
let p = self.pages.get_mut(&page_id)?;
p.data = data;
return None;
}
let evicted = if self.pages.len() >= self.capacity {
self.evict()
} else {
None
};
self.pages.insert(page_id, Page::new(page_id, data));
self.lru_order.push_front(page_id);
evicted
}
pub fn mark_dirty(&mut self, page_id: u64) {
if let Some(p) = self.pages.get_mut(&page_id) {
p.dirty = true;
}
}
pub fn flush_dirty(&mut self) -> Vec<Page> {
let dirty_ids: Vec<u64> = self
.pages
.values()
.filter(|p| p.dirty)
.map(|p| p.page_id)
.collect();
let mut result = Vec::new();
for id in dirty_ids {
if let Some(p) = self.pages.get_mut(&id) {
p.dirty = false;
result.push(p.clone());
}
}
result
}
pub fn evict(&mut self) -> Option<Page> {
let evict_id = self
.lru_order
.iter()
.rev()
.find(|&&id| !self.pinned.contains(&id))
.copied()?;
if let Some(pos) = self.lru_order.iter().position(|&id| id == evict_id) {
self.lru_order.remove(pos);
}
let page = self.pages.remove(&evict_id)?;
self.stats.evictions += 1;
if page.dirty {
self.stats.dirty_evictions += 1;
}
Some(page)
}
pub fn contains(&self, page_id: u64) -> bool {
self.pages.contains_key(&page_id)
}
pub fn size(&self) -> usize {
self.pages.len()
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn stats(&self) -> &PageCacheStats {
&self.stats
}
pub fn dirty_count(&self) -> usize {
self.pages.values().filter(|p| p.dirty).count()
}
pub fn hit_rate(&self) -> f64 {
let total = self.stats.hits + self.stats.misses;
if total == 0 {
0.0
} else {
self.stats.hits as f64 / total as f64
}
}
pub fn clear(&mut self) {
self.pages.clear();
self.lru_order.clear();
self.pinned.clear();
self.stats = PageCacheStats::default();
}
pub fn pin(&mut self, page_id: u64) {
if self.pages.contains_key(&page_id) {
self.pinned.insert(page_id);
}
}
pub fn unpin(&mut self, page_id: u64) {
self.pinned.remove(&page_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_data(seed: u8) -> Vec<u8> {
vec![seed; 64]
}
#[test]
fn test_new_cache_empty() {
let c = PageCache::new(4);
assert_eq!(c.size(), 0);
assert_eq!(c.capacity(), 4);
}
#[test]
#[should_panic]
fn test_zero_capacity_panics() {
let _ = PageCache::new(0);
}
#[test]
fn test_insert_and_contains() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
assert!(c.contains(1));
assert!(!c.contains(2));
}
#[test]
fn test_insert_returns_none_when_below_capacity() {
let mut c = PageCache::new(4);
let ev = c.insert(1, make_data(1));
assert!(ev.is_none());
}
#[test]
fn test_insert_update_existing_no_eviction() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
let ev = c.insert(1, make_data(2));
assert!(ev.is_none());
assert_eq!(c.size(), 1);
}
#[test]
fn test_capacity_enforced() {
let mut c = PageCache::new(3);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.insert(3, make_data(3));
let ev = c.insert(4, make_data(4));
assert_eq!(c.size(), 3);
assert!(ev.is_some());
assert_eq!(ev.unwrap().page_id, 1);
assert!(!c.contains(1));
assert!(c.contains(4));
}
#[test]
fn test_lru_evict_oldest() {
let mut c = PageCache::new(3);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.insert(3, make_data(3));
c.get(1);
let ev = c.insert(4, make_data(4));
assert_eq!(ev.unwrap().page_id, 2);
}
#[test]
fn test_get_touches_lru() {
let mut c = PageCache::new(2);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.get(1);
let ev = c.insert(3, make_data(3));
assert_eq!(ev.unwrap().page_id, 2);
assert!(c.contains(1));
assert!(c.contains(3));
}
#[test]
fn test_get_hit() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
assert!(c.get(1).is_some());
assert_eq!(c.stats().hits, 1);
assert_eq!(c.stats().misses, 0);
}
#[test]
fn test_get_miss() {
let mut c = PageCache::new(4);
assert!(c.get(99).is_none());
assert_eq!(c.stats().misses, 1);
assert_eq!(c.stats().hits, 0);
}
#[test]
fn test_get_mut_modifies_data() {
let mut c = PageCache::new(4);
c.insert(1, vec![0u8; 4]);
{
let p = c.get_mut(1).unwrap();
p.data[0] = 42;
}
let p = c.get(1).unwrap();
assert_eq!(p.data[0], 42);
}
#[test]
fn test_access_count_increments() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.get(1);
c.get(1);
assert_eq!(c.get(1).unwrap().access_count, 3);
}
#[test]
fn test_mark_dirty() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.mark_dirty(1);
assert_eq!(c.dirty_count(), 1);
assert!(c.get(1).unwrap().dirty);
}
#[test]
fn test_mark_dirty_noop_missing() {
let mut c = PageCache::new(4);
c.mark_dirty(99); assert_eq!(c.dirty_count(), 0);
}
#[test]
fn test_flush_dirty_clears_flag() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.mark_dirty(1);
c.mark_dirty(2);
let flushed = c.flush_dirty();
assert_eq!(flushed.len(), 2);
assert_eq!(c.dirty_count(), 0);
}
#[test]
fn test_flush_dirty_returns_only_dirty() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.mark_dirty(1);
let flushed = c.flush_dirty();
assert_eq!(flushed.len(), 1);
assert_eq!(flushed[0].page_id, 1);
}
#[test]
fn test_dirty_eviction_counted() {
let mut c = PageCache::new(2);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.mark_dirty(1);
c.get(2);
c.insert(3, make_data(3));
assert_eq!(c.stats().dirty_evictions, 1);
assert_eq!(c.stats().evictions, 1);
}
#[test]
fn test_evict_manual() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
let ev = c.evict();
assert!(ev.is_some());
assert_eq!(c.size(), 1);
}
#[test]
fn test_evict_empty_returns_none() {
let mut c = PageCache::new(4);
assert!(c.evict().is_none());
}
#[test]
fn test_pin_prevents_eviction() {
let mut c = PageCache::new(2);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.pin(1);
let ev = c.insert(3, make_data(3));
assert_eq!(ev.unwrap().page_id, 2);
assert!(c.contains(1));
}
#[test]
fn test_unpin_allows_eviction() {
let mut c = PageCache::new(2);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.pin(1);
c.unpin(1);
c.get(1);
let ev = c.insert(3, make_data(3));
assert_eq!(ev.unwrap().page_id, 2);
}
#[test]
fn test_pin_noop_when_not_cached() {
let mut c = PageCache::new(4);
c.pin(99); assert!(!c.pinned.contains(&99));
}
#[test]
fn test_all_pinned_evict_returns_none() {
let mut c = PageCache::new(2);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.pin(1);
c.pin(2);
assert!(c.evict().is_none());
}
#[test]
fn test_hit_rate_no_accesses() {
let c = PageCache::new(4);
assert_eq!(c.hit_rate(), 0.0);
}
#[test]
fn test_hit_rate_all_hits() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.get(1);
c.get(1);
assert!((c.hit_rate() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_hit_rate_mixed() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.get(1); c.get(2); assert!((c.hit_rate() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_clear_empties_cache() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.mark_dirty(1);
c.get(1);
c.clear();
assert_eq!(c.size(), 0);
assert_eq!(c.dirty_count(), 0);
assert_eq!(c.stats().hits, 0);
assert_eq!(c.stats().misses, 0);
}
#[test]
fn test_reuse_after_clear() {
let mut c = PageCache::new(2);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.clear();
c.insert(3, make_data(3));
assert!(c.contains(3));
assert_eq!(c.size(), 1);
}
#[test]
fn test_size_tracks_insertions() {
let mut c = PageCache::new(10);
for i in 0..5u64 {
c.insert(i, make_data(i as u8));
}
assert_eq!(c.size(), 5);
}
#[test]
fn test_capacity_unchanged() {
let c = PageCache::new(42);
assert_eq!(c.capacity(), 42);
}
#[test]
fn test_eviction_stat_increments() {
let mut c = PageCache::new(1);
c.insert(1, make_data(1));
c.insert(2, make_data(2)); assert_eq!(c.stats().evictions, 1);
}
#[test]
fn test_multiple_evictions_counted() {
let mut c = PageCache::new(1);
for i in 0..5u64 {
c.insert(i, make_data(i as u8));
}
assert_eq!(c.stats().evictions, 4);
}
#[test]
fn test_page_data_stored_correctly() {
let mut c = PageCache::new(4);
let data = vec![1u8, 2, 3, 4];
c.insert(10, data.clone());
assert_eq!(c.get(10).unwrap().data, data);
}
#[test]
fn test_page_id_matches() {
let mut c = PageCache::new(4);
c.insert(42, make_data(7));
assert_eq!(c.get(42).unwrap().page_id, 42);
}
#[test]
fn test_page_not_dirty_initially() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
assert!(!c.get(1).unwrap().dirty);
}
#[test]
fn test_dirty_count_zero_initially() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
assert_eq!(c.dirty_count(), 0);
}
#[test]
fn test_dirty_count_tracks_multiple() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.insert(3, make_data(3));
c.mark_dirty(1);
c.mark_dirty(3);
assert_eq!(c.dirty_count(), 2);
}
#[test]
fn test_evict_returns_lru_page() {
let mut c = PageCache::new(3);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.insert(3, make_data(3));
c.get(2);
c.get(3);
let ev = c.evict().unwrap();
assert_eq!(ev.page_id, 1);
}
#[test]
fn test_contains_after_eviction() {
let mut c = PageCache::new(1);
c.insert(1, make_data(1));
c.insert(2, make_data(2)); assert!(!c.contains(1));
assert!(c.contains(2));
}
#[test]
fn test_stats_hits_misses_after_multiple_ops() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.get(1); c.get(1); c.get(2); c.get(3); assert_eq!(c.stats().hits, 2);
assert_eq!(c.stats().misses, 2);
}
#[test]
fn test_large_capacity_cache() {
let mut c = PageCache::new(1000);
for i in 0..500u64 {
c.insert(i, make_data((i % 256) as u8));
}
assert_eq!(c.size(), 500);
assert!(c.stats().evictions == 0);
}
#[test]
fn test_flush_dirty_returns_clones_not_removed() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.mark_dirty(1);
let flushed = c.flush_dirty();
assert_eq!(flushed.len(), 1);
assert!(c.contains(1));
}
#[test]
fn test_pin_after_clear_does_nothing() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.clear();
c.pin(1); assert!(!c.pinned.contains(&1));
}
#[test]
fn test_get_mut_hit_increments_access_count() {
let mut c = PageCache::new(4);
c.insert(1, make_data(1));
c.get_mut(1);
assert_eq!(c.get(1).unwrap().access_count, 2);
}
#[test]
fn test_get_mut_miss_increments_miss_stat() {
let mut c = PageCache::new(4);
assert!(c.get_mut(99).is_none());
assert_eq!(c.stats().misses, 1);
}
#[test]
fn test_insert_at_exact_capacity_triggers_eviction() {
let mut c = PageCache::new(3);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
c.insert(3, make_data(3));
let ev = c.insert(4, make_data(4));
assert!(ev.is_some());
assert_eq!(c.size(), 3);
}
#[test]
fn test_dirty_eviction_stat_not_incremented_for_clean() {
let mut c = PageCache::new(1);
c.insert(1, make_data(1));
c.insert(2, make_data(2));
assert_eq!(c.stats().dirty_evictions, 0);
assert_eq!(c.stats().evictions, 1);
}
}