use std::collections::HashMap;
struct Node {
page: u64,
payload: Vec<u8>,
dirty: bool,
prev: Option<usize>,
next: Option<usize>,
}
pub struct PageCache {
slots: Vec<Node>,
free: Vec<usize>,
map: HashMap<u64, usize>,
head: Option<usize>, tail: Option<usize>, capacity: usize,
clean: usize,
}
impl PageCache {
pub fn new(capacity: usize) -> Self {
Self {
slots: Vec::new(),
free: Vec::new(),
map: HashMap::new(),
head: None,
tail: None,
capacity: capacity.max(1),
clean: 0,
}
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn set_capacity(&mut self, capacity: usize) {
self.capacity = capacity.max(1);
self.trim();
}
fn detach(&mut self, i: usize) {
let (p, n) = (self.slots[i].prev, self.slots[i].next);
match p {
Some(pi) => self.slots[pi].next = n,
None => self.head = n,
}
match n {
Some(ni) => self.slots[ni].prev = p,
None => self.tail = p,
}
self.slots[i].prev = None;
self.slots[i].next = None;
}
fn push_front(&mut self, i: usize) {
self.slots[i].prev = None;
self.slots[i].next = self.head;
match self.head {
Some(h) => self.slots[h].prev = Some(i),
None => self.tail = Some(i),
}
self.head = Some(i);
}
fn touch(&mut self, i: usize) {
self.detach(i);
self.push_front(i);
}
fn alloc(&mut self, node: Node) -> usize {
if let Some(i) = self.free.pop() {
self.slots[i] = node;
i
} else {
self.slots.push(node);
self.slots.len() - 1
}
}
fn remove_node(&mut self, i: usize) {
self.detach(i);
let page = self.slots[i].page;
self.map.remove(&page);
if !self.slots[i].dirty {
self.clean -= 1;
}
self.slots[i].payload = Vec::new();
self.slots[i].dirty = false;
self.free.push(i);
}
fn evict_one(&mut self) -> bool {
if self.clean == 0 {
return false;
}
let mut cur = self.tail;
while let Some(i) = cur {
if !self.slots[i].dirty {
self.remove_node(i);
return true;
}
cur = self.slots[i].prev;
}
false
}
pub fn trim(&mut self) {
while self.map.len() > self.capacity {
if !self.evict_one() {
break;
}
}
}
pub fn get(&mut self, page: u64) -> Option<Vec<u8>> {
let i = *self.map.get(&page)?;
self.touch(i);
Some(self.slots[i].payload.clone())
}
pub fn peek(&self, page: u64) -> Option<&[u8]> {
self.map.get(&page).map(|&i| self.slots[i].payload.as_slice())
}
pub fn insert(&mut self, page: u64, payload: Vec<u8>, dirty: bool) {
if let Some(&i) = self.map.get(&page) {
match (self.slots[i].dirty, dirty) {
(true, false) => self.clean += 1,
(false, true) => self.clean -= 1,
_ => {}
}
self.slots[i].payload = payload;
self.slots[i].dirty = dirty;
self.touch(i);
return;
}
let i = self.alloc(Node {
page,
payload,
dirty,
prev: None,
next: None,
});
self.map.insert(page, i);
self.push_front(i);
if !dirty {
self.clean += 1;
}
self.trim();
}
pub fn dirty_pages(&self) -> Vec<u64> {
let mut v: Vec<u64> = self
.map
.iter()
.filter(|&(_, &i)| self.slots[i].dirty)
.map(|(&p, _)| p)
.collect();
v.sort_unstable();
v
}
pub fn mark_all_clean(&mut self) {
for &i in self.map.values() {
self.slots[i].dirty = false;
}
self.clean = self.map.len();
}
}
#[cfg(test)]
mod tests {
use super::PageCache;
fn page(n: u8) -> Vec<u8> {
vec![n; 4]
}
#[test]
fn evicts_least_recently_used_clean_page() {
let mut c = PageCache::new(3);
for n in 1..=3u64 {
c.insert(n, page(n as u8), false);
}
c.insert(4, page(4), false); assert!(c.peek(1).is_none());
assert!(c.peek(4).is_some());
c.get(2); c.insert(5, page(5), false); assert!(c.peek(3).is_none());
assert_eq!(c.len(), 3);
for n in [2u64, 4, 5] {
assert!(c.peek(n).is_some());
}
}
#[test]
fn dirty_pages_are_never_evicted() {
let mut c = PageCache::new(2);
for n in 1..=3u64 {
c.insert(n, page(n as u8), true);
}
assert_eq!(c.len(), 3);
for n in 1..=3u64 {
assert!(c.peek(n).is_some());
}
assert_eq!(c.dirty_pages(), vec![1, 2, 3]);
c.mark_all_clean();
c.trim();
assert_eq!(c.len(), 2);
assert!(c.dirty_pages().is_empty());
}
#[test]
fn shrinking_capacity_trims_immediately() {
let mut c = PageCache::new(8);
for n in 1..=8u64 {
c.insert(n, page(n as u8), false);
}
assert_eq!(c.len(), 8);
c.set_capacity(3);
assert_eq!(c.len(), 3);
for n in [6u64, 7, 8] {
assert!(c.peek(n).is_some());
}
}
#[test]
fn reinsert_updates_payload_and_recency() {
let mut c = PageCache::new(2);
c.insert(1, page(1), false);
c.insert(2, page(2), false);
c.insert(1, page(9), false); c.insert(3, page(3), false); assert_eq!(c.get(1), Some(page(9)));
assert!(c.peek(2).is_none());
assert!(c.peek(3).is_some());
}
}