1#![allow(clippy::trivial_regex, clippy::collapsible_if)]
4
5use crate::page::{DEFAULT_PAGE_SIZE, Frame, PageNum};
6use akar_common::memory::MemoryManager;
7use akar_common::memory_account::BUFFER_POOL;
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::sync::Arc;
11
12#[derive(Debug, Clone)]
14pub struct NumaInfo {
15 pub num_nodes: u32,
17}
18
19impl NumaInfo {
20 pub fn detect() -> Self {
26 let num_nodes = Self::detect_num_nodes();
27 Self { num_nodes }
28 }
29
30 #[cfg(target_os = "linux")]
31 fn detect_num_nodes() -> u32 {
32 use std::fs;
33 let node_dir = "/sys/devices/system/node";
34 if let Ok(entries) = fs::read_dir(node_dir) {
35 let count = entries
36 .filter_map(|e| e.ok())
37 .filter(|e| {
38 e.file_name()
39 .to_str()
40 .map(|s| s.starts_with("node") && s != "node")
41 .unwrap_or(false)
42 })
43 .count();
44 if count > 0 {
45 return count as u32;
46 }
47 }
48 1
49 }
50
51 #[cfg(not(target_os = "linux"))]
52 fn detect_num_nodes() -> u32 {
53 1
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ReadaheadPolicy {
60 pub enabled: bool,
62 pub window: usize,
64}
65
66impl Default for ReadaheadPolicy {
67 fn default() -> Self {
68 Self {
69 enabled: true,
70 window: 4,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct BufferManagerConfig {
78 pub max_memory: u64,
80 pub page_size: usize,
82 pub use_mmap: bool,
84 pub numa_aware: bool,
86 pub readahead: ReadaheadPolicy,
88}
89
90impl Default for BufferManagerConfig {
91 fn default() -> Self {
92 Self {
93 max_memory: 64 * 1024 * 1024, page_size: DEFAULT_PAGE_SIZE,
95 use_mmap: false,
96 numa_aware: false,
97 readahead: ReadaheadPolicy::default(),
98 }
99 }
100}
101
102#[derive(Debug, Default, Clone, Copy)]
104pub struct BufferManagerStats {
105 pub page_faults: u64,
107 pub page_writes: u64,
109 pub num_frames: usize,
111 pub dirty_frames: usize,
113 pub pinned_frames: usize,
115}
116
117#[derive(Debug)]
119#[allow(dead_code)]
120pub struct BufferManager {
121 db_path: PathBuf,
123 page_size: usize,
125 max_frames: usize,
127 frames: HashMap<(String, PageNum), Frame>,
130 files: HashMap<String, FileHandleInfo>,
132 clock_hand: usize,
134 clock_order: Vec<(String, PageNum)>,
136 memory_manager: Arc<MemoryManager>,
138 stats: BufferManagerStats,
140 use_mmap: bool,
142 #[cfg(not(target_arch = "wasm32"))]
144 mmap_regions: HashMap<String, memmap2::Mmap>,
145 numa_info: NumaInfo,
147 readahead: ReadaheadPolicy,
149 last_accessed: HashMap<String, PageNum>,
151 prev_last_accessed: HashMap<String, PageNum>,
153}
154
155#[allow(dead_code)]
156#[derive(Debug, Clone)]
157struct FileHandleInfo {
158 path: PathBuf,
159 num_pages: u64,
160}
161
162impl BufferManager {
163 pub fn new(db_path: PathBuf, memory_manager: Arc<MemoryManager>, config: BufferManagerConfig) -> Self {
164 let max_frames = if config.max_memory > 0 {
165 (config.max_memory / config.page_size as u64) as usize
166 } else {
167 1000
168 };
169
170 let numa_info = if config.numa_aware {
171 NumaInfo::detect()
172 } else {
173 NumaInfo { num_nodes: 1 }
174 };
175
176 Self {
177 db_path,
178 page_size: config.page_size,
179 max_frames,
180 frames: HashMap::new(),
181 files: HashMap::new(),
182 clock_hand: 0,
183 clock_order: Vec::new(),
184 memory_manager,
185 stats: BufferManagerStats::default(),
186 use_mmap: config.use_mmap,
187 #[cfg(not(target_arch = "wasm32"))]
188 mmap_regions: HashMap::new(),
189 numa_info,
190 readahead: config.readahead,
191 last_accessed: HashMap::new(),
192 prev_last_accessed: HashMap::new(),
193 }
194 }
195
196 fn key(file_name: &str, page_num: PageNum) -> (String, PageNum) {
198 (file_name.to_string(), page_num)
199 }
200
201 pub fn stats(&self) -> &BufferManagerStats {
202 &self.stats
203 }
204
205 pub fn page_size(&self) -> usize {
206 self.page_size
207 }
208
209 pub fn num_frames(&self) -> usize {
210 self.frames.len()
211 }
212
213 pub fn is_file_registered(&self, name: &str) -> bool {
215 self.files.contains_key(name)
216 }
217
218 pub fn register_file(&mut self, name: &str, path: PathBuf) {
220 let num_pages = if path.exists() {
221 let len = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
222 len / self.page_size as u64
223 } else {
224 0
225 };
226 self.files.insert(name.to_string(), FileHandleInfo { path, num_pages });
227 }
228
229 pub fn pin(&mut self, file_name: &str, page_num: PageNum) -> std::io::Result<Frame> {
231 let k = Self::key(file_name, page_num);
232 if let Some(frame) = self.frames.get_mut(&k) {
233 frame.pin();
234 frame.clock_ref = true;
235
236 let prev = self.last_accessed.remove(file_name);
238 if let Some(p) = prev {
239 self.prev_last_accessed.insert(file_name.to_string(), p);
240 }
241 self.last_accessed.insert(file_name.to_string(), page_num);
242
243 return Ok(frame.clone());
244 }
245
246 self.stats.page_faults += 1;
248
249 if self.frames.len() >= self.max_frames {
250 self.evict()?;
251 }
252
253 let data = self.read_from_disk(file_name, page_num)?;
254 let mut frame = Frame::new(page_num, data);
255 frame.pin();
256
257 if self.numa_info.num_nodes > 1 {
258 frame.numa_node = self.current_numa_node();
259 }
260
261 let k = Self::key(file_name, page_num);
262 self.clock_order.push(k.clone());
263 self.frames.insert(k.clone(), frame.clone());
264 self.memory_manager.allocate_with(BUFFER_POOL, self.page_size as u64);
265
266 self.update_stats();
267
268 let prev = self.last_accessed.remove(file_name);
270 if let Some(p) = prev {
271 self.prev_last_accessed.insert(file_name.to_string(), p);
272 }
273 self.last_accessed.insert(file_name.to_string(), page_num);
274
275 self.maybe_readahead(file_name, page_num);
277
278 Ok(frame)
279 }
280
281 pub fn pin_mut(&mut self, file_name: &str, page_num: PageNum) -> std::io::Result<&mut Frame> {
283 let k = Self::key(file_name, page_num);
284 if !self.frames.contains_key(&k) {
285 let _ = self.pin(file_name, page_num)?;
287 }
288 if let Some(frame) = self.frames.get_mut(&k) {
289 frame.pin();
290 frame.clock_ref = true;
291 return Ok(frame);
292 }
293 unreachable!()
294 }
295
296 pub fn unpin(&mut self, file_name: &str, page_num: PageNum) {
298 let k = Self::key(file_name, page_num);
299 if let Some(frame) = self.frames.get_mut(&k) {
300 frame.unpin();
301 }
302 self.update_stats();
303 }
304
305 pub fn flush(&mut self, file_name: &str, page_num: PageNum) -> std::io::Result<()> {
307 let k = Self::key(file_name, page_num);
308 if let Some(frame) = self.frames.get(&k) {
309 if frame.is_dirty {
310 self.write_to_disk(file_name, page_num, &frame.data)?;
311 if let Some(f) = self.frames.get_mut(&k) {
312 f.is_dirty = false;
313 }
314 self.stats.page_writes += 1;
315 }
316 }
317 self.update_stats();
318 Ok(())
319 }
320
321 pub fn flush_all(&mut self) -> std::io::Result<()> {
323 let dirty: Vec<(String, PageNum)> = self
324 .frames
325 .iter()
326 .filter(|(_, f)| f.is_dirty)
327 .map(|(k, _)| k.clone())
328 .collect();
329
330 for (file_name, page_num) in dirty {
331 self.flush(&file_name, page_num)?;
332 }
333 Ok(())
334 }
335
336 pub fn dirty_page_nums_for_file(&self, file_name: &str) -> Vec<PageNum> {
338 self.frames
339 .iter()
340 .filter(|(k, f)| k.0 == file_name && f.is_dirty)
341 .map(|(k, _)| k.1)
342 .collect()
343 }
344
345 pub fn drop_file(&mut self, file_name: &str) {
350 let path = self.files.get(file_name).map(|f| f.path.clone());
351 self.frames.retain(|(f, _), _| f != file_name);
352 self.clock_order.retain(|(f, _)| f != file_name);
353 self.files.remove(file_name);
354 self.last_accessed.remove(file_name);
355 self.prev_last_accessed.remove(file_name);
356 #[cfg(not(target_arch = "wasm32"))]
357 if let Some(p) = path {
358 let pstr = p.to_string_lossy().to_string();
359 self.mmap_regions.remove(&pstr);
360 }
361 self.update_stats();
362 }
363
364 pub fn numa_info(&self) -> &NumaInfo {
366 &self.numa_info
367 }
368
369 fn maybe_readahead(&mut self, file_name: &str, page_num: PageNum) {
374 if !self.readahead.enabled || self.readahead.window == 0 {
375 return;
376 }
377
378 if let Some(&prev) = self.prev_last_accessed.get(file_name) {
379 if page_num > 0 && prev == page_num - 1 {
380 for offset in 1..=self.readahead.window as u64 {
382 let prefetch_page = page_num + offset;
383 let pk = Self::key(file_name, prefetch_page);
384 if self.frames.contains_key(&pk) {
385 continue; }
387 if self.frames.len() >= self.max_frames {
388 break; }
390 if let Ok(data) = self.read_from_disk(file_name, prefetch_page) {
391 self.stats.page_faults += 1;
392 let mut frame = Frame::new(prefetch_page, data);
393 if self.numa_info.num_nodes > 1 {
394 frame.numa_node = self.current_numa_node();
395 }
396 self.clock_order.push(pk.clone());
397 self.frames.insert(pk, frame);
398 self.memory_manager.allocate_with(BUFFER_POOL, self.page_size as u64);
399 }
400 }
401 }
402 }
403 }
404
405 fn evict(&mut self) -> std::io::Result<()> {
408 let n = self.clock_order.len();
409 if n == 0 {
410 return Ok(());
411 }
412
413 for _ in 0..=n {
414 if self.clock_hand >= n {
415 self.clock_hand = 0;
416 }
417 let (ref file_name, ref page_num) = self.clock_order[self.clock_hand];
418 let k = Self::key(file_name, *page_num);
419
420 if let Some(frame) = self.frames.get(&k) {
421 if frame.is_pinned() {
422 self.clock_hand += 1;
423 continue;
424 }
425 if frame.clock_ref {
426 self.frames.get_mut(&k).unwrap().clock_ref = false;
427 self.clock_hand += 1;
428 continue;
429 }
430 if frame.is_dirty {
432 self.write_to_disk(file_name, *page_num, &frame.data)?;
433 self.stats.page_writes += 1;
434 }
435 self.frames.remove(&k);
436 self.clock_order.remove(self.clock_hand);
437 self.memory_manager.deallocate_with(BUFFER_POOL, self.page_size as u64);
438 return Ok(());
439 }
440 }
441 Ok(())
442 }
443
444 fn read_from_disk(&mut self, file_name: &str, page_num: PageNum) -> std::io::Result<Vec<u8>> {
448 if self.use_mmap {
449 #[cfg(not(target_arch = "wasm32"))]
450 {
451 return self.read_mmap(file_name, page_num);
452 }
453 #[cfg(target_arch = "wasm32")]
454 {
455 return self.read_syscall(file_name, page_num);
456 }
457 }
458 self.read_syscall(file_name, page_num)
459 }
460
461 fn read_syscall(&self, file_name: &str, page_num: PageNum) -> std::io::Result<Vec<u8>> {
463 if let Some(fh) = self.files.get(file_name) {
464 use std::io::{Read, Seek, SeekFrom};
465 let mut buf = vec![0u8; self.page_size];
466 match std::fs::File::open(&fh.path) {
467 Ok(mut file) => {
468 file.seek(SeekFrom::Start(page_num * self.page_size as u64))?;
469 let _ = file.read(&mut buf);
473 Ok(buf)
474 }
475 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(buf),
476 Err(e) => Err(e),
477 }
478 } else {
479 Ok(vec![0u8; self.page_size])
480 }
481 }
482
483 #[cfg(not(target_arch = "wasm32"))]
485 fn read_mmap(&mut self, file_name: &str, page_num: PageNum) -> std::io::Result<Vec<u8>> {
486 if let Some(fh) = self.files.get(file_name) {
487 let path_str = fh.path.to_string_lossy().to_string();
488 let page_size = self.page_size;
489
490 if !self.mmap_regions.contains_key(&path_str) {
491 match std::fs::File::open(&fh.path) {
492 Ok(file) => {
493 let mmap = unsafe { memmap2::Mmap::map(&file)? };
494 self.mmap_regions.insert(path_str.clone(), mmap);
495 }
496 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
497 return Ok(vec![0u8; page_size]);
499 }
500 Err(e) => return Err(e),
501 }
502 }
503
504 let mmap = &self.mmap_regions[&path_str];
505 let offset = page_num as usize * page_size;
506 let end = offset + page_size;
507
508 if end > mmap.len() {
509 return Ok(vec![0u8; page_size]);
512 }
513
514 let slice = unsafe { std::slice::from_raw_parts(mmap.as_ptr().add(offset), page_size) };
518 Ok(slice.to_vec())
519 } else {
520 Ok(vec![0u8; self.page_size])
521 }
522 }
523
524 fn write_to_disk(&self, file_name: &str, page_num: PageNum, data: &[u8]) -> std::io::Result<()> {
525 if let Some(fh) = self.files.get(file_name) {
526 use std::fs::OpenOptions;
527 use std::io::{Seek, SeekFrom, Write};
528 let mut file = OpenOptions::new()
529 .create(true)
530 .write(true)
531 .truncate(false)
532 .open(&fh.path)?;
533 file.seek(SeekFrom::Start(page_num * self.page_size as u64))?;
534 file.write_all(data)?;
535 }
536 Ok(())
537 }
538
539 fn current_numa_node(&self) -> u32 {
541 0
544 }
545
546 fn update_stats(&mut self) {
547 self.stats.num_frames = self.frames.len();
548 self.stats.dirty_frames = self.frames.values().filter(|f| f.is_dirty).count();
549 self.stats.pinned_frames = self.frames.values().filter(|f| f.is_pinned()).count();
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 const TEST_FILE: &str = "main";
558
559 fn create_test_bm() -> (BufferManager, tempfile::TempDir) {
560 let dir = tempfile::tempdir().unwrap();
561 let mm = Arc::new(MemoryManager::new(1024 * 1024));
562 let config = BufferManagerConfig {
563 max_memory: 256 * 1024,
564 page_size: DEFAULT_PAGE_SIZE,
565 readahead: ReadaheadPolicy {
566 enabled: false,
567 ..Default::default()
568 },
569 ..Default::default()
570 };
571 let mut bm = BufferManager::new(dir.path().to_path_buf(), mm, config);
572 let db_path = dir.path().join("test.db");
573 std::fs::write(&db_path, vec![0u8; 8192 * 10]).unwrap();
574 bm.register_file(TEST_FILE, db_path);
575 (bm, dir)
576 }
577
578 #[test]
579 fn test_pin_unpin() {
580 let (mut bm, _dir) = create_test_bm();
581 let frame = bm.pin(TEST_FILE, 0).unwrap();
582 assert_eq!(frame.page_num, 0);
583 assert!(frame.is_pinned());
584 bm.unpin(TEST_FILE, 0);
585 assert_eq!(bm.stats().page_faults, 1);
586 }
587
588 #[test]
589 fn test_cached_page_no_fault() {
590 let (mut bm, _dir) = create_test_bm();
591 bm.pin(TEST_FILE, 0).unwrap();
592 bm.unpin(TEST_FILE, 0);
593 bm.pin(TEST_FILE, 0).unwrap();
594 bm.unpin(TEST_FILE, 0);
595 assert_eq!(bm.stats().page_faults, 1);
596 }
597
598 #[test]
599 fn test_multiple_pages() {
600 let (mut bm, _dir) = create_test_bm();
601 for i in 0..5 {
602 bm.pin(TEST_FILE, i).unwrap();
603 bm.unpin(TEST_FILE, i);
604 }
605 assert_eq!(bm.stats().page_faults, 5);
606 assert_eq!(bm.num_frames(), 5);
607 }
608
609 #[test]
610 fn test_dirty_and_flush() {
611 let (mut bm, _dir) = create_test_bm();
612 let frame = bm.pin_mut(TEST_FILE, 1).unwrap();
613 frame.mark_dirty();
614 bm.unpin(TEST_FILE, 1);
615 assert_eq!(bm.stats().dirty_frames, 1);
616 bm.flush(TEST_FILE, 1).unwrap();
617 assert_eq!(bm.stats().dirty_frames, 0);
618 assert_eq!(bm.stats().page_writes, 1);
619 }
620
621 #[test]
622 fn test_clock_eviction() {
623 let dir = tempfile::tempdir().unwrap();
624 let mm = Arc::new(MemoryManager::new(3 * DEFAULT_PAGE_SIZE as u64));
625 let config = BufferManagerConfig {
626 max_memory: 3 * DEFAULT_PAGE_SIZE as u64,
627 page_size: DEFAULT_PAGE_SIZE,
628 ..Default::default()
629 };
630 let mut bm = BufferManager::new(dir.path().to_path_buf(), mm, config);
631 let db_path = dir.path().join("test.db");
632 std::fs::write(&db_path, vec![0u8; 8192 * 20]).unwrap();
633 bm.register_file(TEST_FILE, db_path);
634
635 for i in 0..3 {
636 bm.pin(TEST_FILE, i).unwrap();
637 bm.unpin(TEST_FILE, i);
638 }
639 assert_eq!(bm.num_frames(), 3);
640
641 bm.pin(TEST_FILE, 3).unwrap();
642 bm.unpin(TEST_FILE, 3);
643 assert_eq!(bm.num_frames(), 3);
644 }
645
646 #[test]
647 fn test_flush_all() {
648 let (mut bm, _dir) = create_test_bm();
649 for i in 0..3 {
650 let frame = bm.pin_mut(TEST_FILE, i).unwrap();
651 frame.mark_dirty();
652 bm.unpin(TEST_FILE, i);
653 }
654 bm.flush_all().unwrap();
655 assert_eq!(bm.stats().dirty_frames, 0);
656 }
657
658 #[test]
661 fn test_mmap_read() {
662 let dir = tempfile::tempdir().unwrap();
663 let mm = Arc::new(MemoryManager::new(1024 * 1024));
664 let config = BufferManagerConfig {
665 max_memory: 256 * 1024,
666 page_size: DEFAULT_PAGE_SIZE,
667 use_mmap: true,
668 ..Default::default()
669 };
670 let mut bm = BufferManager::new(dir.path().to_path_buf(), mm, config);
671
672 let db_path = dir.path().join("mmap_test.db");
674 let mut data = vec![0u8; DEFAULT_PAGE_SIZE * 4];
675 for i in 0..DEFAULT_PAGE_SIZE {
676 data[i] = (i % 256) as u8;
677 }
678 std::fs::write(&db_path, &data).unwrap();
679 bm.register_file(TEST_FILE, db_path);
680
681 let frame = bm.pin(TEST_FILE, 0).unwrap();
683 assert_eq!(&frame.data[..], &data[..DEFAULT_PAGE_SIZE]);
684 bm.unpin(TEST_FILE, 0);
685
686 let frame = bm.pin(TEST_FILE, 1).unwrap();
688 assert_eq!(&frame.data[..], &data[DEFAULT_PAGE_SIZE..DEFAULT_PAGE_SIZE * 2]);
689 bm.unpin(TEST_FILE, 1);
690 }
691
692 #[test]
693 fn test_readahead_sequential() {
694 let dir = tempfile::tempdir().unwrap();
695 let mm = Arc::new(MemoryManager::new(1024 * 1024));
696 let config = BufferManagerConfig {
697 max_memory: 512 * 1024,
698 page_size: DEFAULT_PAGE_SIZE,
699 readahead: ReadaheadPolicy {
700 enabled: true,
701 window: 4,
702 },
703 ..Default::default()
704 };
705 let mut bm = BufferManager::new(dir.path().to_path_buf(), mm, config);
706
707 let db_path = dir.path().join("seq_test.db");
708 std::fs::write(&db_path, vec![42u8; DEFAULT_PAGE_SIZE * 20]).unwrap();
709 bm.register_file(TEST_FILE, db_path);
710
711 bm.pin(TEST_FILE, 0).unwrap();
713 bm.unpin(TEST_FILE, 0);
714 let faults_after_0 = bm.stats().page_faults;
715
716 bm.pin(TEST_FILE, 1).unwrap();
718 bm.unpin(TEST_FILE, 1);
719 let faults_after_1 = bm.stats().page_faults;
720
721 assert!(
723 faults_after_1 > faults_after_0 + 1,
724 "Expected readahead faults: after_page_0={}, after_page_1={}",
725 faults_after_0,
726 faults_after_1
727 );
728
729 for p in 2..=5 {
731 let k = BufferManager::key(TEST_FILE, p);
732 assert!(
733 bm.frames.contains_key(&k),
734 "Page {} should be in cache after readahead",
735 p
736 );
737 }
738
739 let faults_before = bm.stats().page_faults;
741 bm.pin(TEST_FILE, 2).unwrap();
742 bm.unpin(TEST_FILE, 2);
743 assert_eq!(bm.stats().page_faults, faults_before);
744 }
745
746 #[test]
747 fn test_readahead_random() {
748 let dir = tempfile::tempdir().unwrap();
749 let mm = Arc::new(MemoryManager::new(1024 * 1024));
750 let config = BufferManagerConfig {
751 max_memory: 512 * 1024,
752 page_size: DEFAULT_PAGE_SIZE,
753 readahead: ReadaheadPolicy {
754 enabled: true,
755 window: 4,
756 },
757 ..Default::default()
758 };
759 let mut bm = BufferManager::new(dir.path().to_path_buf(), mm, config);
760
761 let db_path = dir.path().join("rand_test.db");
762 std::fs::write(&db_path, vec![0u8; DEFAULT_PAGE_SIZE * 20]).unwrap();
763 bm.register_file(TEST_FILE, db_path);
764
765 bm.pin(TEST_FILE, 0).unwrap();
767 bm.unpin(TEST_FILE, 0);
768
769 bm.pin(TEST_FILE, 5).unwrap();
771 bm.unpin(TEST_FILE, 5);
772 let faults = bm.stats().page_faults;
773 assert_eq!(faults, 2, "Random access should not trigger readahead");
774
775 for p in 6..=9 {
777 let k = BufferManager::key(TEST_FILE, p);
778 assert!(
779 !bm.frames.contains_key(&k),
780 "Page {} should NOT be in cache after random access",
781 p
782 );
783 }
784 }
785
786 #[test]
787 fn test_numa_detection() {
788 let numa = NumaInfo::detect();
789 assert!(
790 numa.num_nodes >= 1,
791 "NUMA detection should return at least 1 node, got {}",
792 numa.num_nodes
793 );
794 }
795}