#[cfg(target_arch = "wasm32")]
use std::sync::Mutex;
use std::{
num::NonZeroUsize,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use lru::LruCache;
#[cfg(not(target_arch = "wasm32"))]
use parking_lot::Mutex;
use crate::{
page::{Page, PageId, PageManager},
StorageError,
};
macro_rules! lock {
($mutex:expr) => {{
#[cfg(not(target_arch = "wasm32"))]
{
$mutex.lock()
}
#[cfg(target_arch = "wasm32")]
{
$mutex.lock().unwrap()
}
}};
}
#[derive(Debug, Default)]
pub struct BufferPoolStats {
hits: AtomicU64,
misses: AtomicU64,
evictions: AtomicU64,
}
impl BufferPoolStats {
pub fn new() -> Self {
Self::default()
}
pub fn hits(&self) -> u64 {
self.hits.load(Ordering::Relaxed)
}
pub fn misses(&self) -> u64 {
self.misses.load(Ordering::Relaxed)
}
pub fn evictions(&self) -> u64 {
self.evictions.load(Ordering::Relaxed)
}
pub fn hit_rate(&self) -> f64 {
let hits = self.hits();
let misses = self.misses();
let total = hits + misses;
if total == 0 {
0.0
} else {
hits as f64 / total as f64
}
}
fn record_hit(&self) {
self.hits.fetch_add(1, Ordering::Relaxed);
}
fn record_miss(&self) {
self.misses.fetch_add(1, Ordering::Relaxed);
}
fn record_eviction(&self) {
self.evictions.fetch_add(1, Ordering::Relaxed);
}
}
#[derive(Debug)]
pub struct BufferPool {
cache: Arc<Mutex<LruCache<PageId, Page>>>,
page_manager: Arc<PageManager>,
capacity: usize,
stats: BufferPoolStats,
}
impl BufferPool {
pub fn new(page_manager: Arc<PageManager>, capacity: usize) -> Self {
let capacity = if capacity == 0 { 1000 } else { capacity };
let cache = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(capacity).unwrap())));
BufferPool { cache, page_manager, capacity, stats: BufferPoolStats::new() }
}
pub fn get_page(&self, page_id: PageId) -> Result<Page, StorageError> {
{
let mut cache = lock!(self.cache);
if let Some(page) = cache.get(&page_id) {
self.stats.record_hit();
return Ok(page.clone());
}
}
self.stats.record_miss();
let page = self.page_manager.read_page(page_id)?;
self.put_page_internal(page.clone())?;
Ok(page)
}
pub fn put_page(&self, page: Page) -> Result<(), StorageError> {
self.put_page_internal(page)
}
fn put_page_internal(&self, page: Page) -> Result<(), StorageError> {
let mut cache = lock!(self.cache);
if let Some((_evicted_id, mut evicted_page)) = cache.push(page.id, page) {
if evicted_page.dirty {
drop(cache); self.page_manager.write_page(&mut evicted_page)?;
}
self.stats.record_eviction();
}
Ok(())
}
pub fn flush_dirty(&self) -> Result<(), StorageError> {
let cache = lock!(self.cache);
let dirty_pages: Vec<(PageId, Page)> = cache
.iter()
.filter(|(_, page)| page.dirty)
.map(|(id, page)| (*id, page.clone()))
.collect();
drop(cache);
for (_id, mut page) in dirty_pages {
self.page_manager.write_page(&mut page)?;
let mut cache = lock!(self.cache);
if let Some(cached_page) = cache.get_mut(&page.id) {
cached_page.mark_clean();
}
}
Ok(())
}
pub fn evict(&self, page_id: PageId) -> Result<(), StorageError> {
let mut cache = lock!(self.cache);
if let Some(mut page) = cache.pop(&page_id) {
if page.dirty {
drop(cache); self.page_manager.write_page(&mut page)?;
}
self.stats.record_eviction();
}
Ok(())
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn size(&self) -> Result<usize, StorageError> {
let cache = lock!(self.cache);
Ok(cache.len())
}
pub fn stats(&self) -> &BufferPoolStats {
&self.stats
}
}
#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
mod tests {
use tempfile::TempDir;
use super::*;
use crate::NativeStorage;
#[test]
fn test_buffer_pool_creation() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager, 10);
assert_eq!(buffer_pool.capacity(), 10);
assert_eq!(buffer_pool.size().unwrap(), 0);
}
#[test]
fn test_cache_hit() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager.clone(), 10);
let mut page = Page::new(1);
page.data[0] = 42;
page.mark_dirty();
page_manager.write_page(&mut page).unwrap();
let page1 = buffer_pool.get_page(1).unwrap();
assert_eq!(page1.data[0], 42);
assert_eq!(buffer_pool.stats().misses(), 1);
assert_eq!(buffer_pool.stats().hits(), 0);
let page2 = buffer_pool.get_page(1).unwrap();
assert_eq!(page2.data[0], 42);
assert_eq!(buffer_pool.stats().misses(), 1);
assert_eq!(buffer_pool.stats().hits(), 1);
}
#[test]
fn test_cache_miss() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager.clone(), 10);
let page = buffer_pool.get_page(1).unwrap();
assert_eq!(page.id, 1);
assert_eq!(buffer_pool.stats().misses(), 1);
assert_eq!(buffer_pool.stats().hits(), 0);
}
#[test]
fn test_eviction() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager.clone(), 3);
for i in 1..=3 {
let mut page = Page::new(i);
page.data[0] = i as u8;
buffer_pool.put_page(page).unwrap();
}
assert_eq!(buffer_pool.size().unwrap(), 3);
assert_eq!(buffer_pool.stats().evictions(), 0);
let page4 = Page::new(4);
buffer_pool.put_page(page4).unwrap();
assert_eq!(buffer_pool.size().unwrap(), 3);
assert_eq!(buffer_pool.stats().evictions(), 1);
}
#[test]
fn test_dirty_page_write_on_eviction() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager.clone(), 2);
let mut page1 = Page::new(1);
page1.data[0] = 42;
page1.mark_dirty();
buffer_pool.put_page(page1).unwrap();
let page2 = Page::new(2);
buffer_pool.put_page(page2).unwrap();
let page3 = Page::new(3);
buffer_pool.put_page(page3).unwrap();
assert_eq!(buffer_pool.stats().evictions(), 1);
let read_page = page_manager.read_page(1).unwrap();
assert_eq!(read_page.data[0], 42);
}
#[test]
fn test_flush_dirty_pages() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager.clone(), 10);
for i in 1..=5 {
let mut page = Page::new(i);
page.data[0] = i as u8;
page.mark_dirty();
buffer_pool.put_page(page).unwrap();
}
buffer_pool.flush_dirty().unwrap();
for i in 1..=5 {
let page = page_manager.read_page(i).unwrap();
assert_eq!(page.data[0], i as u8);
}
}
#[test]
fn test_cache_statistics() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager.clone(), 10);
buffer_pool.get_page(1).unwrap(); buffer_pool.get_page(1).unwrap(); buffer_pool.get_page(2).unwrap(); buffer_pool.get_page(1).unwrap(); buffer_pool.get_page(2).unwrap();
assert_eq!(buffer_pool.stats().hits(), 3);
assert_eq!(buffer_pool.stats().misses(), 2);
assert_eq!(buffer_pool.stats().hit_rate(), 0.6);
}
#[test]
fn test_manual_eviction() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager.clone(), 10);
let mut page = Page::new(1);
page.data[0] = 42;
page.mark_dirty();
buffer_pool.put_page(page).unwrap();
assert_eq!(buffer_pool.size().unwrap(), 1);
buffer_pool.evict(1).unwrap();
assert_eq!(buffer_pool.size().unwrap(), 0);
assert_eq!(buffer_pool.stats().evictions(), 1);
let read_page = page_manager.read_page(1).unwrap();
assert_eq!(read_page.data[0], 42);
}
#[test]
fn test_zero_capacity_defaults_to_1000() {
let temp_dir = TempDir::new().unwrap();
let storage = Arc::new(NativeStorage::new(temp_dir.path()).unwrap());
let page_manager = Arc::new(PageManager::new("test.db", storage).unwrap());
let buffer_pool = BufferPool::new(page_manager, 0);
assert_eq!(buffer_pool.capacity(), 1000);
}
}