use std::sync::atomic::{AtomicU64, Ordering};
use ahash::AHashMap;
use parking_lot::Mutex;
use super::page::{Page, PageId, PageManager};
use mentedb_core::error::{MenteError, MenteResult};
use tracing::{debug, trace};
type FrameId = usize;
#[derive(Debug, Clone, Copy, Default)]
pub struct BufferStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub resident_pages: u64,
}
struct Frame {
page: Box<Page>,
page_id: Option<PageId>,
pin_count: u32,
dirty: bool,
reference: bool,
}
impl Frame {
fn new() -> Self {
Self {
page: Box::new(Page::zeroed()),
page_id: None,
pin_count: 0,
dirty: false,
reference: false,
}
}
}
struct BufferPoolInner {
frames: Vec<Frame>,
page_table: AHashMap<PageId, FrameId>,
clock_hand: usize,
capacity: usize,
}
pub struct BufferPool {
inner: Mutex<BufferPoolInner>,
hits: AtomicU64,
misses: AtomicU64,
evictions: AtomicU64,
}
impl BufferPool {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "buffer pool capacity must be > 0");
Self {
inner: Mutex::new(BufferPoolInner {
frames: Vec::new(),
page_table: AHashMap::new(),
clock_hand: 0,
capacity,
}),
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
evictions: AtomicU64::new(0),
}
}
pub fn stats(&self) -> BufferStats {
let resident_pages = self.inner.lock().page_table.len() as u64;
BufferStats {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
evictions: self.evictions.load(Ordering::Relaxed),
resident_pages,
}
}
pub fn fetch_page(&self, page_id: PageId, pm: &mut PageManager) -> MenteResult<Box<Page>> {
let mut inner = self.inner.lock();
if let Some(&frame_id) = inner.page_table.get(&page_id) {
let frame = &mut inner.frames[frame_id];
frame.pin_count += 1;
frame.reference = true;
self.hits.fetch_add(1, Ordering::Relaxed);
trace!(page_id = page_id.0, frame_id, "buffer pool hit");
return Ok(frame.page.clone());
}
self.misses.fetch_add(1, Ordering::Relaxed);
let frame_id = Self::find_victim(&mut inner)?;
if inner.frames[frame_id].dirty
&& let Some(old_pid) = inner.frames[frame_id].page_id
{
pm.write_page(old_pid, &inner.frames[frame_id].page)?;
debug!(page_id = old_pid.0, frame_id, "flushed dirty victim");
}
if let Some(old_pid) = inner.frames[frame_id].page_id {
inner.page_table.remove(&old_pid);
self.evictions.fetch_add(1, Ordering::Relaxed);
}
let page = pm.read_page(page_id)?;
{
let frame = &mut inner.frames[frame_id];
*frame.page = *page;
frame.page_id = Some(page_id);
frame.pin_count = 1;
frame.dirty = false;
frame.reference = true;
}
inner.page_table.insert(page_id, frame_id);
trace!(
page_id = page_id.0,
frame_id, "loaded page into buffer pool"
);
Ok(inner.frames[frame_id].page.clone())
}
pub fn pin_page(&self, page_id: PageId) -> MenteResult<()> {
let mut inner = self.inner.lock();
match inner.page_table.get(&page_id) {
Some(&fid) => {
inner.frames[fid].pin_count += 1;
Ok(())
}
None => Err(MenteError::Storage(format!(
"page {} not in buffer pool",
page_id.0
))),
}
}
pub fn unpin_page(&self, page_id: PageId, dirty: bool) -> MenteResult<()> {
let mut inner = self.inner.lock();
match inner.page_table.get(&page_id) {
Some(&fid) => {
let frame = &mut inner.frames[fid];
if frame.pin_count > 0 {
frame.pin_count -= 1;
}
if dirty {
frame.dirty = true;
}
Ok(())
}
None => Err(MenteError::Storage(format!(
"page {} not in buffer pool",
page_id.0
))),
}
}
pub fn invalidate(&self, page_id: PageId) {
let mut inner = self.inner.lock();
if let Some(fid) = inner.page_table.remove(&page_id) {
let frame = &mut inner.frames[fid];
frame.page_id = None;
frame.pin_count = 0;
frame.dirty = false;
frame.reference = false;
debug!(page_id = page_id.0, "invalidated cached page");
}
}
pub fn update_page(&self, page_id: PageId, page: &Page) -> MenteResult<()> {
let mut inner = self.inner.lock();
match inner.page_table.get(&page_id) {
Some(&fid) => {
let frame = &mut inner.frames[fid];
*frame.page = page.clone();
frame.dirty = true;
Ok(())
}
None => Err(MenteError::Storage(format!(
"page {} not in buffer pool",
page_id.0
))),
}
}
pub fn flush_page(&self, page_id: PageId, pm: &mut PageManager) -> MenteResult<()> {
let mut inner = self.inner.lock();
match inner.page_table.get(&page_id) {
Some(&fid) => {
let frame = &mut inner.frames[fid];
if frame.dirty {
pm.write_page(page_id, &frame.page)?;
frame.dirty = false;
debug!(page_id = page_id.0, "flushed page");
}
Ok(())
}
None => Err(MenteError::Storage(format!(
"page {} not in buffer pool",
page_id.0
))),
}
}
pub fn flush_all(&self, pm: &mut PageManager) -> MenteResult<()> {
let mut inner = self.inner.lock();
for frame in &mut inner.frames {
if frame.dirty
&& let Some(pid) = frame.page_id
{
pm.write_page(pid, &frame.page)?;
frame.dirty = false;
}
}
debug!("flushed all dirty pages");
Ok(())
}
fn find_victim(inner: &mut BufferPoolInner) -> MenteResult<FrameId> {
for i in 0..inner.frames.len() {
if inner.frames[i].page_id.is_none() {
return Ok(i);
}
}
if inner.frames.len() < inner.capacity {
inner.frames.push(Frame::new());
return Ok(inner.frames.len() - 1);
}
let cap = inner.capacity;
let max_sweeps = cap * 2;
for _ in 0..max_sweeps {
let idx = inner.clock_hand;
inner.clock_hand = (inner.clock_hand + 1) % cap;
let frame = &mut inner.frames[idx];
if frame.pin_count == 0 {
if !frame.reference {
return Ok(idx);
}
frame.reference = false;
}
}
Err(MenteError::Storage(
"buffer pool full: all pages are pinned".into(),
))
}
}