1use std::collections::{HashMap, VecDeque};
22use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
23use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
24
25use super::page::Page;
26
27fn cache_read<'a, T>(lock: &'a RwLock<T>) -> RwLockReadGuard<'a, T> {
28 lock.read().unwrap_or_else(|poisoned| poisoned.into_inner())
29}
30
31fn cache_write<'a, T>(lock: &'a RwLock<T>) -> RwLockWriteGuard<'a, T> {
32 lock.write()
33 .unwrap_or_else(|poisoned| poisoned.into_inner())
34}
35
36fn cache_lock<'a, T>(lock: &'a Mutex<T>) -> MutexGuard<'a, T> {
37 lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
38}
39
40pub const DEFAULT_CACHE_CAPACITY: usize = 100_000;
43
44pub const MIN_CACHE_CAPACITY: usize = 2;
46
47struct CacheEntry {
57 page: Page,
59 pin_count: usize,
61 dirty: bool,
63}
64
65impl CacheEntry {
66 fn new(page: Page) -> Self {
67 Self {
68 page,
69 pin_count: 0,
70 dirty: false,
71 }
72 }
73}
74
75const CACHE_LINE: usize = 64;
82
83const TAG_EMPTY: u32 = u32::MAX;
88
89#[repr(align(64))]
94struct VisitedLine([AtomicBool; CACHE_LINE]);
95
96const TAGS_PER_LINE: usize = CACHE_LINE / 4;
98
99#[repr(align(64))]
103struct TagLine([AtomicU32; TAGS_PER_LINE]);
104
105struct ShardMeta {
123 visited: Box<[VisitedLine]>,
124 tag: Box<[TagLine]>,
125 capacity: usize,
126}
127
128impl ShardMeta {
129 fn new(capacity: usize) -> Self {
130 let visited = (0..capacity.div_ceil(CACHE_LINE))
131 .map(|_| VisitedLine(std::array::from_fn(|_| AtomicBool::new(false))))
132 .collect::<Vec<_>>()
133 .into_boxed_slice();
134 let tag = (0..capacity.div_ceil(TAGS_PER_LINE))
135 .map(|_| TagLine(std::array::from_fn(|_| AtomicU32::new(TAG_EMPTY))))
136 .collect::<Vec<_>>()
137 .into_boxed_slice();
138 Self {
139 visited,
140 tag,
141 capacity,
142 }
143 }
144
145 #[inline]
150 fn mark_visited(&self, slot: usize) {
151 if let Some(b) = self.visited_bit(slot) {
152 b.store(true, Ordering::Relaxed);
153 }
154 }
155
156 #[inline]
157 fn is_visited(&self, slot: usize) -> bool {
158 self.visited_bit(slot)
159 .map(|b| b.load(Ordering::Relaxed))
160 .unwrap_or(false)
161 }
162
163 #[inline]
167 fn clear_visited(&self, slot: usize) {
168 if let Some(b) = self.visited_bit(slot) {
169 b.store(false, Ordering::Relaxed);
170 }
171 }
172
173 #[inline]
175 fn tag(&self, slot: usize) -> u32 {
176 self.tag_slot(slot)
177 .map(|t| t.load(Ordering::Relaxed))
178 .unwrap_or(TAG_EMPTY)
179 }
180
181 #[inline]
185 fn occupy(&self, slot: usize, page_id: u32) {
186 if let Some(t) = self.tag_slot(slot) {
187 t.store(page_id, Ordering::Relaxed);
188 }
189 self.clear_visited(slot);
190 }
191
192 #[inline]
194 fn vacate(&self, slot: usize) {
195 if let Some(t) = self.tag_slot(slot) {
196 t.store(TAG_EMPTY, Ordering::Relaxed);
197 }
198 self.clear_visited(slot);
199 }
200
201 fn reset(&self) {
203 for line in self.visited.iter() {
204 for b in line.0.iter() {
205 b.store(false, Ordering::Relaxed);
206 }
207 }
208 for line in self.tag.iter() {
209 for t in line.0.iter() {
210 t.store(TAG_EMPTY, Ordering::Relaxed);
211 }
212 }
213 }
214
215 #[inline]
216 fn visited_bit(&self, slot: usize) -> Option<&AtomicBool> {
217 if slot >= self.capacity {
218 return None;
219 }
220 self.visited
221 .get(slot / CACHE_LINE)
222 .map(|line| &line.0[slot % CACHE_LINE])
223 }
224
225 #[inline]
226 fn tag_slot(&self, slot: usize) -> Option<&AtomicU32> {
227 if slot >= self.capacity {
228 return None;
229 }
230 self.tag
231 .get(slot / TAGS_PER_LINE)
232 .map(|line| &line.0[slot % TAGS_PER_LINE])
233 }
234}
235
236#[derive(Debug, Default, Clone)]
238pub struct CacheStats {
239 pub hits: u64,
241 pub misses: u64,
243 pub evictions: u64,
245 pub writebacks: u64,
247}
248
249impl CacheStats {
250 pub fn hit_rate(&self) -> f64 {
252 let total = self.hits + self.misses;
253 if total == 0 {
254 0.0
255 } else {
256 self.hits as f64 / total as f64
257 }
258 }
259}
260
261pub struct PageCacheShard {
265 capacity: usize,
267 index: RwLock<HashMap<u32, usize>>,
269 fifo: Mutex<VecDeque<u32>>,
271 entries: RwLock<Vec<Option<CacheEntry>>>,
273 meta: ShardMeta,
277 free_slots: Mutex<Vec<usize>>,
279 hand: Mutex<usize>,
281 stats: Mutex<CacheStats>,
283}
284
285impl PageCacheShard {
286 pub fn new(capacity: usize) -> Self {
288 let capacity = capacity.max(MIN_CACHE_CAPACITY);
289
290 Self {
291 capacity,
292 index: RwLock::new(HashMap::with_capacity(capacity)),
293 fifo: Mutex::new(VecDeque::with_capacity(capacity)),
294 entries: RwLock::new(Vec::with_capacity(capacity)),
295 meta: ShardMeta::new(capacity),
296 free_slots: Mutex::new(Vec::new()),
297 hand: Mutex::new(0),
298 stats: Mutex::new(CacheStats::default()),
299 }
300 }
301
302 pub fn with_default_capacity() -> Self {
304 Self::new(DEFAULT_CACHE_CAPACITY)
305 }
306
307 pub fn len(&self) -> usize {
309 cache_read(&self.index).len()
310 }
311
312 pub fn is_empty(&self) -> bool {
314 self.len() == 0
315 }
316
317 pub fn capacity(&self) -> usize {
319 self.capacity
320 }
321
322 pub fn stats(&self) -> CacheStats {
324 cache_lock(&self.stats).clone()
325 }
326
327 pub fn reset_stats(&self) {
329 *cache_lock(&self.stats) = CacheStats::default();
330 }
331
332 pub fn get(&self, page_id: u32) -> Option<Page> {
337 let index = cache_read(&self.index);
339 let slot = match index.get(&page_id) {
340 Some(&s) => s,
341 None => {
342 drop(index);
343 cache_lock(&self.stats).misses += 1;
345 return None;
346 }
347 };
348 drop(index);
349
350 let entries = cache_read(&self.entries);
361 if let Some(entry) = entries.get(slot).and_then(|e| e.as_ref()) {
362 let page = entry.page.clone();
363 drop(entries);
364
365 if self.meta.tag(slot) == page_id {
372 self.meta.mark_visited(slot);
373 }
374
375 cache_lock(&self.stats).hits += 1;
376
377 Some(page)
378 } else {
379 cache_lock(&self.stats).misses += 1;
380 None
381 }
382 }
383
384 pub fn insert(&self, page_id: u32, page: Page) -> Option<Page> {
389 {
391 let index = cache_read(&self.index);
392 if let Some(&slot) = index.get(&page_id) {
393 drop(index);
394
395 let mut entries = cache_write(&self.entries);
400 if let Some(Some(entry)) = entries.get_mut(slot) {
401 entry.page = page;
402 }
403 drop(entries);
404 self.meta.mark_visited(slot);
405 return None;
406 }
407 }
408
409 let mut evicted = None;
411
412 let current_len = self.len();
414 if current_len >= self.capacity {
415 evicted = self.evict();
417 }
418
419 let slot = {
421 let mut free_slots = cache_lock(&self.free_slots);
422 if let Some(slot) = free_slots.pop() {
423 slot
424 } else {
425 drop(free_slots);
426 let mut entries = cache_write(&self.entries);
428 let slot = entries.len();
429 entries.push(None);
430 slot
431 }
432 };
433
434 {
436 let mut entries = cache_write(&self.entries);
437
438 while entries.len() <= slot {
440 entries.push(None);
441 }
442
443 entries[slot] = Some(CacheEntry::new(page));
444 }
445
446 {
448 let mut index = cache_write(&self.index);
449 index.insert(page_id, slot);
450 }
451
452 self.meta.occupy(slot, page_id);
456
457 {
459 let mut fifo = cache_lock(&self.fifo);
460 fifo.push_back(page_id);
461 }
462
463 evicted
464 }
465
466 fn evict(&self) -> Option<Page> {
470 let mut fifo = cache_lock(&self.fifo);
471 let mut hand = cache_lock(&self.hand);
472
473 if fifo.is_empty() {
474 return None;
475 }
476
477 let fifo_len = fifo.len();
478 let mut attempts = 0;
479
480 loop {
482 if attempts >= fifo_len * 2 {
483 return None;
485 }
486
487 if *hand >= fifo_len {
489 *hand = 0;
490 }
491
492 let page_id = fifo[*hand];
493 attempts += 1;
494
495 let slot = {
497 let index = cache_read(&self.index);
498 match index.get(&page_id) {
499 Some(&s) => s,
500 None => {
501 *hand += 1;
503 continue;
504 }
505 }
506 };
507
508 let (should_evict, dirty) = {
512 let entries = cache_read(&self.entries);
513 match entries.get(slot).and_then(|e| e.as_ref()) {
514 Some(entry) => {
515 if entry.pin_count > 0 {
516 (false, false)
518 } else if self.meta.is_visited(slot) {
519 (false, false)
521 } else {
522 (true, entry.dirty)
524 }
525 }
526 None => {
527 *hand += 1;
528 continue;
529 }
530 }
531 };
532
533 if !should_evict {
534 self.meta.clear_visited(slot);
537 *hand += 1;
538 continue;
539 }
540
541 let evicted_page = {
546 let mut entries = cache_write(&self.entries);
547 debug_assert!(
548 entries
549 .get(slot)
550 .and_then(|e| e.as_ref())
551 .map(|e| e.pin_count == 0)
552 .unwrap_or(true),
553 "ADR 0033 violation: eviction selected a pinned page (slot {slot})"
554 );
555 let entry = entries[slot].take();
556 entry.map(|e| e.page)
557 };
558
559 self.meta.vacate(slot);
561
562 {
564 let mut index = cache_write(&self.index);
565 index.remove(&page_id);
566 }
567
568 fifo.remove(*hand);
570
571 {
573 let mut free_slots = cache_lock(&self.free_slots);
574 free_slots.push(slot);
575 }
576
577 {
579 let mut stats = cache_lock(&self.stats);
580 stats.evictions += 1;
581 if dirty {
582 stats.writebacks += 1;
583 }
584 }
585
586 if dirty {
588 return evicted_page;
589 } else {
590 return None;
591 }
592 }
593 }
594
595 pub fn mark_dirty(&self, page_id: u32) {
597 let index = cache_read(&self.index);
598 if let Some(&slot) = index.get(&page_id) {
599 drop(index);
600
601 let mut entries = cache_write(&self.entries);
602 if let Some(Some(entry)) = entries.get_mut(slot) {
603 entry.dirty = true;
604 }
605 }
606 }
607
608 pub fn mark_clean(&self, page_id: u32) {
610 let index = cache_read(&self.index);
611 if let Some(&slot) = index.get(&page_id) {
612 drop(index);
613
614 let mut entries = cache_write(&self.entries);
615 if let Some(Some(entry)) = entries.get_mut(slot) {
616 entry.dirty = false;
617 }
618 }
619 }
620
621 pub fn pin(&self, page_id: u32) -> bool {
623 let index = cache_read(&self.index);
624 if let Some(&slot) = index.get(&page_id) {
625 drop(index);
626
627 let mut entries = cache_write(&self.entries);
628 if let Some(Some(entry)) = entries.get_mut(slot) {
629 entry.pin_count += 1;
630 return true;
631 }
632 }
633 false
634 }
635
636 pub fn unpin(&self, page_id: u32) -> bool {
638 let index = cache_read(&self.index);
639 if let Some(&slot) = index.get(&page_id) {
640 drop(index);
641
642 let mut entries = cache_write(&self.entries);
643 if let Some(Some(entry)) = entries.get_mut(slot) {
644 if entry.pin_count > 0 {
645 entry.pin_count -= 1;
646 return true;
647 }
648 }
649 }
650 false
651 }
652
653 pub fn remove(&self, page_id: u32) -> Option<Page> {
655 let slot = {
657 let mut index = cache_write(&self.index);
658 index.remove(&page_id)?
659 };
660
661 let entry = {
663 let mut entries = cache_write(&self.entries);
664 entries.get_mut(slot).and_then(|e| e.take())
665 };
666
667 self.meta.vacate(slot);
669
670 {
672 let mut fifo = cache_lock(&self.fifo);
673 fifo.retain(|&id| id != page_id);
674 }
675
676 {
678 let mut free_slots = cache_lock(&self.free_slots);
679 free_slots.push(slot);
680 }
681
682 entry.map(|e| e.page)
683 }
684
685 pub fn flush_dirty(&self) -> Vec<(u32, Page)> {
689 let mut dirty_pages = Vec::new();
690
691 let index = cache_read(&self.index);
692 let entries = cache_read(&self.entries);
693
694 for (&page_id, &slot) in index.iter() {
695 if let Some(Some(entry)) = entries.get(slot) {
696 if entry.dirty {
697 dirty_pages.push((page_id, entry.page.clone()));
698 }
699 }
700 }
701
702 drop(entries);
703 drop(index);
704
705 for (page_id, _) in &dirty_pages {
707 self.mark_clean(*page_id);
708 }
709
710 let count = dirty_pages.len();
711 cache_lock(&self.stats).writebacks += count as u64;
712
713 dirty_pages
714 }
715
716 fn take_dirty_page(&self, page_id: u32) -> Option<Page> {
719 let index = cache_read(&self.index);
720 let slot = *index.get(&page_id)?;
721 let mut entries = cache_write(&self.entries);
722 let entry = entries.get_mut(slot)?.as_mut()?;
723 if !entry.dirty {
724 return None;
725 }
726 let page = entry.page.clone();
727 entry.dirty = false;
728 drop(entries);
729 drop(index);
730 cache_lock(&self.stats).writebacks += 1;
731 Some(page)
732 }
733
734 pub fn flush_some_dirty(&self, max: usize) -> Vec<(u32, Page)> {
740 if max == 0 {
741 return Vec::new();
742 }
743 let mut dirty_pages = Vec::with_capacity(max);
744
745 let index = cache_read(&self.index);
746 let entries = cache_read(&self.entries);
747
748 for (&page_id, &slot) in index.iter() {
749 if dirty_pages.len() >= max {
750 break;
751 }
752 if let Some(Some(entry)) = entries.get(slot) {
753 if entry.dirty {
754 dirty_pages.push((page_id, entry.page.clone()));
755 }
756 }
757 }
758
759 drop(entries);
760 drop(index);
761
762 for (page_id, _) in &dirty_pages {
763 self.mark_clean(*page_id);
764 }
765
766 let count = dirty_pages.len();
767 cache_lock(&self.stats).writebacks += count as u64;
768
769 dirty_pages
770 }
771
772 pub fn dirty_count(&self) -> usize {
775 let index = cache_read(&self.index);
776 let entries = cache_read(&self.entries);
777 let mut count = 0;
778 for (_, &slot) in index.iter() {
779 if let Some(Some(entry)) = entries.get(slot) {
780 if entry.dirty {
781 count += 1;
782 }
783 }
784 }
785 count
786 }
787
788 pub fn clear(&self) {
790 let mut index = cache_write(&self.index);
791 let mut entries = cache_write(&self.entries);
792 let mut fifo = cache_lock(&self.fifo);
793 let mut free_slots = cache_lock(&self.free_slots);
794
795 index.clear();
796 entries.clear();
797 fifo.clear();
798 free_slots.clear();
799 self.meta.reset();
800 *cache_lock(&self.hand) = 0;
801 }
802
803 pub fn contains(&self, page_id: u32) -> bool {
805 cache_read(&self.index).contains_key(&page_id)
806 }
807
808 pub fn page_ids(&self) -> Vec<u32> {
810 cache_read(&self.index).keys().copied().collect()
811 }
812
813 fn dirty_page_ids(&self, max: usize) -> Vec<u32> {
815 let index = cache_read(&self.index);
816 let entries = cache_read(&self.entries);
817 index
818 .iter()
819 .filter_map(|(&page_id, &slot)| {
820 let entry = entries.get(slot)?.as_ref()?;
821 entry.dirty.then_some(page_id)
822 })
823 .take(max)
824 .collect()
825 }
826}
827
828impl Default for PageCacheShard {
829 fn default() -> Self {
830 Self::with_default_capacity()
831 }
832}
833
834const NUM_SHARDS: usize = 8;
840
841pub struct PageCache {
851 shards: Box<[PageCacheShard]>,
852 capacity: usize,
853}
854
855impl PageCache {
856 pub fn new(capacity: usize) -> Self {
857 let per_shard = capacity.div_ceil(NUM_SHARDS).max(MIN_CACHE_CAPACITY);
861 let total = per_shard * NUM_SHARDS;
862 let shards: Vec<PageCacheShard> = (0..NUM_SHARDS)
863 .map(|_| PageCacheShard::new(per_shard))
864 .collect();
865 Self {
866 shards: shards.into_boxed_slice(),
867 capacity: total,
868 }
869 }
870
871 pub fn with_default_capacity() -> Self {
872 Self::new(DEFAULT_CACHE_CAPACITY)
873 }
874
875 #[inline]
876 fn shard_for(&self, page_id: u32) -> &PageCacheShard {
877 &self.shards[(page_id as usize) & (NUM_SHARDS - 1)]
878 }
879
880 pub fn len(&self) -> usize {
881 self.shards.iter().map(|s| s.len()).sum()
882 }
883
884 pub fn is_empty(&self) -> bool {
885 self.shards.iter().all(|s| s.is_empty())
886 }
887
888 pub fn capacity(&self) -> usize {
889 self.capacity
890 }
891
892 pub fn stats(&self) -> CacheStats {
893 let mut agg = CacheStats::default();
894 for s in self.shards.iter() {
895 let cs = s.stats();
896 agg.hits += cs.hits;
897 agg.misses += cs.misses;
898 agg.evictions += cs.evictions;
899 agg.writebacks += cs.writebacks;
900 }
901 agg
902 }
903
904 pub fn reset_stats(&self) {
905 for s in self.shards.iter() {
906 s.reset_stats();
907 }
908 }
909
910 pub fn get(&self, page_id: u32) -> Option<Page> {
911 self.shard_for(page_id).get(page_id)
912 }
913
914 pub fn insert(&self, page_id: u32, page: Page) -> Option<Page> {
915 self.shard_for(page_id).insert(page_id, page)
916 }
917
918 pub fn mark_dirty(&self, page_id: u32) {
919 self.shard_for(page_id).mark_dirty(page_id)
920 }
921
922 pub fn mark_clean(&self, page_id: u32) {
923 self.shard_for(page_id).mark_clean(page_id)
924 }
925
926 pub fn pin(&self, page_id: u32) -> bool {
927 self.shard_for(page_id).pin(page_id)
928 }
929
930 pub fn unpin(&self, page_id: u32) -> bool {
931 self.shard_for(page_id).unpin(page_id)
932 }
933
934 pub fn remove(&self, page_id: u32) -> Option<Page> {
935 self.shard_for(page_id).remove(page_id)
936 }
937
938 pub fn contains(&self, page_id: u32) -> bool {
939 self.shard_for(page_id).contains(page_id)
940 }
941
942 pub fn flush_dirty(&self) -> Vec<(u32, Page)> {
943 let mut out = Vec::new();
944 for s in self.shards.iter() {
945 out.extend(s.flush_dirty());
946 }
947 out
948 }
949
950 pub fn flush_dirty_pages(&self, page_ids: &[u32]) -> Vec<(u32, Page)> {
953 let mut pages = Vec::with_capacity(page_ids.len());
954 for &page_id in page_ids {
955 if let Some(page) = self.shard_for(page_id).take_dirty_page(page_id) {
956 pages.push((page_id, page));
957 }
958 }
959 pages
960 }
961
962 pub fn flush_some_dirty(&self, max: usize) -> Vec<(u32, Page)> {
963 if max == 0 {
964 return Vec::new();
965 }
966 let mut out = Vec::with_capacity(max);
967 for s in self.shards.iter() {
968 if out.len() >= max {
969 break;
970 }
971 let budget = max - out.len();
972 out.extend(s.flush_some_dirty(budget));
973 }
974 out
975 }
976
977 pub fn dirty_count(&self) -> usize {
978 self.shards.iter().map(|s| s.dirty_count()).sum()
979 }
980
981 pub fn clear(&self) {
982 for s in self.shards.iter() {
983 s.clear();
984 }
985 }
986
987 pub fn page_ids(&self) -> Vec<u32> {
988 let mut out = Vec::with_capacity(self.len());
989 for s in self.shards.iter() {
990 out.extend(s.page_ids());
991 }
992 out
993 }
994
995 pub fn dirty_page_ids(&self, max: usize) -> Vec<u32> {
997 let mut ids = Vec::new();
998 for shard in self.shards.iter() {
999 if ids.len() == max {
1000 break;
1001 }
1002 ids.extend(shard.dirty_page_ids(max - ids.len()));
1003 }
1004 ids
1005 }
1006}
1007
1008impl Default for PageCache {
1009 fn default() -> Self {
1010 Self::with_default_capacity()
1011 }
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017 use crate::storage::page::PageType;
1018
1019 fn make_page(id: u32) -> Page {
1020 Page::new(PageType::BTreeLeaf, id)
1021 }
1022
1023 #[test]
1024 fn test_cache_basic() {
1025 let cache = PageCacheShard::new(100);
1026
1027 assert!(cache.is_empty());
1028 assert_eq!(cache.capacity(), 100);
1029
1030 cache.insert(1, make_page(1));
1032 assert_eq!(cache.len(), 1);
1033 assert!(cache.contains(1));
1034
1035 let page = cache.get(1);
1037 assert!(page.is_some());
1038
1039 let page = cache.get(999);
1041 assert!(page.is_none());
1042 }
1043
1044 #[test]
1045 fn test_cache_eviction() {
1046 let cache = PageCacheShard::new(4);
1047
1048 for i in 0..4 {
1050 cache.insert(i, make_page(i));
1051 }
1052
1053 assert_eq!(cache.len(), 4);
1054
1055 cache.insert(100, make_page(100));
1057
1058 assert_eq!(cache.len(), 4);
1060 assert!(cache.contains(100));
1061 }
1062
1063 #[test]
1064 fn test_cache_sieve_visited() {
1065 let cache = PageCacheShard::new(4);
1066
1067 for i in 0..4 {
1069 cache.insert(i, make_page(i));
1070 }
1071
1072 cache.get(0);
1074
1075 cache.insert(100, make_page(100));
1077
1078 assert!(cache.contains(0));
1080 assert!(cache.contains(100));
1081 }
1082
1083 #[test]
1084 fn test_cache_dirty() {
1085 let cache = PageCacheShard::new(100);
1086
1087 cache.insert(1, make_page(1));
1088 cache.mark_dirty(1);
1089
1090 let dirty = cache.flush_dirty();
1091 assert_eq!(dirty.len(), 1);
1092 assert_eq!(dirty[0].0, 1);
1093
1094 let dirty = cache.flush_dirty();
1096 assert_eq!(dirty.len(), 0);
1097 }
1098
1099 #[test]
1100 fn test_cache_pin() {
1101 let cache = PageCacheShard::new(2);
1102
1103 cache.insert(1, make_page(1));
1104 cache.insert(2, make_page(2));
1105
1106 assert!(cache.pin(1));
1108
1109 cache.insert(3, make_page(3));
1111
1112 assert!(cache.contains(1));
1114
1115 assert!(cache.unpin(1));
1117 }
1118
1119 #[test]
1120 fn test_cache_remove() {
1121 let cache = PageCacheShard::new(100);
1122
1123 cache.insert(1, make_page(1));
1124 assert!(cache.contains(1));
1125
1126 let removed = cache.remove(1);
1127 assert!(removed.is_some());
1128 assert!(!cache.contains(1));
1129 }
1130
1131 #[test]
1132 fn test_cache_stats() {
1133 let cache = PageCacheShard::new(100);
1134
1135 cache.insert(1, make_page(1));
1136
1137 cache.get(1);
1139 cache.get(1);
1140
1141 cache.get(999);
1143
1144 let stats = cache.stats();
1145 assert_eq!(stats.hits, 2);
1146 assert_eq!(stats.misses, 1);
1147 assert!((stats.hit_rate() - 0.666).abs() < 0.01);
1148 }
1149
1150 #[test]
1151 fn test_cache_clear() {
1152 let cache = PageCacheShard::new(100);
1153
1154 for i in 0..50 {
1155 cache.insert(i, make_page(i));
1156 }
1157
1158 assert_eq!(cache.len(), 50);
1159
1160 cache.clear();
1161 assert!(cache.is_empty());
1162 }
1163
1164 #[test]
1165 fn test_cache_update_existing() {
1166 let cache = PageCacheShard::new(100);
1167
1168 let mut page1 = make_page(1);
1169 page1.as_bytes_mut()[100] = 0xAA;
1170 cache.insert(1, page1);
1171
1172 let mut page1_updated = make_page(1);
1173 page1_updated.as_bytes_mut()[100] = 0xBB;
1174 cache.insert(1, page1_updated);
1175
1176 assert_eq!(cache.len(), 1);
1178
1179 let retrieved = cache.get(1).unwrap();
1181 assert_eq!(retrieved.as_bytes()[100], 0xBB);
1182 }
1183
1184 #[test]
1185 fn test_cache_recovers_after_index_lock_poisoning() {
1186 let cache = std::sync::Arc::new(PageCacheShard::new(8));
1187 let poison_target = std::sync::Arc::clone(&cache);
1188 let _ = std::thread::spawn(move || {
1189 let _guard = poison_target
1190 .index
1191 .write()
1192 .expect("index lock should be acquired");
1193 panic!("poison index lock");
1194 })
1195 .join();
1196
1197 cache.insert(1, make_page(1));
1198 assert!(cache.contains(1));
1199 assert!(cache.get(1).is_some());
1200 }
1201
1202 #[test]
1203 fn test_cache_recovers_after_stats_lock_poisoning() {
1204 let cache = std::sync::Arc::new(PageCacheShard::new(8));
1205 let poison_target = std::sync::Arc::clone(&cache);
1206 let _ = std::thread::spawn(move || {
1207 let _guard = poison_target
1208 .stats
1209 .lock()
1210 .expect("stats lock should be acquired");
1211 panic!("poison stats lock");
1212 })
1213 .join();
1214
1215 assert!(cache.get(999).is_none());
1216 assert_eq!(cache.stats().misses, 1);
1217 cache.reset_stats();
1218 assert_eq!(cache.stats().misses, 0);
1219 }
1220
1221 #[test]
1228 fn test_metadata_layout_is_cache_line_packed() {
1229 assert_eq!(std::mem::align_of::<VisitedLine>(), CACHE_LINE);
1230 assert_eq!(std::mem::size_of::<VisitedLine>(), CACHE_LINE);
1231 assert_eq!(std::mem::align_of::<TagLine>(), CACHE_LINE);
1232 assert_eq!(std::mem::size_of::<TagLine>(), CACHE_LINE);
1233
1234 let meta = ShardMeta::new(200);
1236 assert_eq!(meta.visited.len(), 200usize.div_ceil(64));
1238 assert_eq!(meta.tag.len(), 200usize.div_ceil(16));
1239
1240 meta.occupy(5, 42);
1242 assert_eq!(meta.tag(5), 42);
1243 assert!(!meta.is_visited(5));
1244 meta.mark_visited(5);
1245 assert!(meta.is_visited(5));
1246 meta.vacate(5);
1247 assert_eq!(meta.tag(5), TAG_EMPTY);
1248 assert!(!meta.is_visited(5));
1249 }
1250
1251 #[test]
1256 fn test_hit_sets_visited_and_preserves_sieve_policy() {
1257 let cache = PageCacheShard::new(4);
1258 for i in 0..4 {
1259 cache.insert(i, make_page(i));
1260 }
1261
1262 assert!(cache.get(0).is_some());
1264 assert!(cache.get(1).is_some());
1265
1266 cache.insert(10, make_page(10));
1268 cache.insert(11, make_page(11));
1269
1270 assert!(cache.contains(0), "hit page 0 must survive");
1271 assert!(cache.contains(1), "hit page 1 must survive");
1272 assert!(cache.contains(10));
1273 assert!(cache.contains(11));
1274 assert!(!cache.contains(2));
1275 assert!(!cache.contains(3));
1276 }
1277
1278 #[test]
1284 fn test_concurrent_hits_never_evict_pinned_or_lose_dirty() {
1285 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1286 use std::sync::Arc;
1287
1288 const CAP: u32 = 64;
1289 const PINNED: u32 = 8; const DIRTY: u32 = 8; const HITTERS: usize = 6;
1292 const INSERTERS: usize = 4;
1293 const ROUNDS: usize = 4_000;
1294
1295 let cache = Arc::new(PageCacheShard::new(CAP as usize));
1296 for i in 0..CAP {
1297 cache.insert(i, make_page(i));
1298 }
1299 for p in 0..PINNED {
1300 assert!(cache.pin(p), "pin {p}");
1301 }
1302 for p in PINNED..PINNED + DIRTY {
1303 cache.mark_dirty(p);
1304 }
1305
1306 let dirty_written_back = Arc::new(AtomicUsize::new(0));
1309 let stop = Arc::new(AtomicBool::new(false));
1310
1311 let mut hitters = Vec::with_capacity(HITTERS);
1313 for h in 0..HITTERS {
1314 let cache = Arc::clone(&cache);
1315 let stop = Arc::clone(&stop);
1316 hitters.push(std::thread::spawn(move || {
1317 let mut id = (h as u32) % CAP;
1318 while !stop.load(Ordering::Relaxed) {
1319 let _ = cache.get(id);
1320 id = (id + 1) % CAP;
1321 }
1322 }));
1323 }
1324
1325 let mut inserters = Vec::with_capacity(INSERTERS);
1327 for w in 0..INSERTERS {
1328 let cache = Arc::clone(&cache);
1329 let dirty_written_back = Arc::clone(&dirty_written_back);
1330 inserters.push(std::thread::spawn(move || {
1331 let base = 1_000 + (w as u32) * 1_000_000;
1332 for i in 0..ROUNDS {
1333 let id = base + i as u32;
1334 if cache.insert(id, make_page(id)).is_some() {
1335 dirty_written_back.fetch_add(1, Ordering::Relaxed);
1337 }
1338 for p in 0..PINNED {
1341 assert!(
1342 cache.contains(p),
1343 "pinned page {p} was evicted under concurrency"
1344 );
1345 }
1346 }
1347 }));
1348 }
1349
1350 for h in inserters {
1352 h.join()
1353 .expect("inserter thread panicked (invariant broke)");
1354 }
1355 stop.store(true, Ordering::Relaxed);
1356 for h in hitters {
1357 h.join().expect("hitter thread panicked (invariant broke)");
1358 }
1359
1360 for p in 0..PINNED {
1362 assert!(cache.contains(p), "pinned page {p} missing after storm");
1363 }
1364
1365 let dirty_resident = (PINNED..PINNED + DIRTY)
1369 .filter(|&p| cache.contains(p))
1370 .count();
1371 assert!(
1372 dirty_resident + dirty_written_back.load(Ordering::Relaxed) >= DIRTY as usize,
1373 "dirty pages lost: resident={dirty_resident} written_back={}",
1374 dirty_written_back.load(Ordering::Relaxed)
1375 );
1376
1377 }
1385
1386 mod legacy_baseline {
1394 use super::Page;
1395 use std::collections::HashMap;
1396 use std::sync::RwLock;
1397
1398 pub struct LegacyPageCache {
1399 entries: RwLock<HashMap<u32, Page>>,
1400 }
1401
1402 impl LegacyPageCache {
1403 pub fn new(_capacity: usize) -> Self {
1404 Self {
1405 entries: RwLock::new(HashMap::new()),
1406 }
1407 }
1408
1409 pub fn insert(&self, page_id: u32, page: Page) {
1410 let mut entries = self.entries.write().unwrap();
1411 entries.insert(page_id, page);
1412 }
1413
1414 pub fn get(&self, page_id: u32) -> Option<Page> {
1415 let entries = self.entries.read().unwrap();
1416 entries.get(&page_id).cloned()
1417 }
1418 }
1419 }
1420
1421 fn run_workload<F>(workers: usize, ops_per_worker: usize, run: F) -> std::time::Duration
1426 where
1427 F: Fn(u32, &Page) + Send + Sync + 'static + Clone,
1428 {
1429 use std::sync::Arc;
1430 use std::time::Instant;
1431
1432 let run = Arc::new(run);
1433 let start = Instant::now();
1434 let mut handles = Vec::with_capacity(workers);
1435 for w in 0..workers {
1436 let run = Arc::clone(&run);
1437 handles.push(std::thread::spawn(move || {
1438 let base = (w as u32) * 1_000_000;
1439 let page = make_page(0);
1440 for i in 0..ops_per_worker {
1441 let id = base + (i as u32);
1442 run(id, &page);
1443 }
1444 }));
1445 }
1446 for h in handles {
1447 h.join().unwrap();
1448 }
1449 start.elapsed()
1450 }
1451
1452 #[test]
1453 fn test_sharded_cache_scales_concurrently() {
1454 use std::sync::Arc;
1465
1466 const WORKERS: usize = 10;
1467 const OPS: usize = 5_000;
1468 const CAPACITY: usize = 200_000;
1469
1470 let sharded = Arc::new(PageCache::new(CAPACITY));
1472 let s1 = Arc::clone(&sharded);
1473 let sharded_serial = run_workload(1, OPS * WORKERS, move |id, page| {
1474 s1.insert(id, page.clone());
1475 let _ = s1.get(id);
1476 });
1477
1478 let sharded = Arc::new(PageCache::new(CAPACITY));
1480 let s2 = Arc::clone(&sharded);
1481 let sharded_parallel = run_workload(WORKERS, OPS, move |id, page| {
1482 s2.insert(id, page.clone());
1483 let _ = s2.get(id);
1484 });
1485
1486 let legacy = Arc::new(legacy_baseline::LegacyPageCache::new(CAPACITY));
1488 let l2 = Arc::clone(&legacy);
1489 let legacy_parallel = run_workload(WORKERS, OPS, move |id, page| {
1490 l2.insert(id, page.clone());
1491 let _ = l2.get(id);
1492 });
1493
1494 eprintln!(
1495 "page_cache concurrency: sharded 1w={:?} sharded {}w={:?} legacy {}w={:?}",
1496 sharded_serial, WORKERS, sharded_parallel, WORKERS, legacy_parallel
1497 );
1498
1499 assert!(
1502 sharded_parallel.as_nanos() < sharded_serial.as_nanos() * 7,
1503 "sharded cache did not scale: 1w={:?} {}w={:?}",
1504 sharded_serial,
1505 WORKERS,
1506 sharded_parallel
1507 );
1508
1509 assert!(
1515 sharded_parallel.as_nanos() * 12 < legacy_parallel.as_nanos() * 10,
1516 "sharded cache did not beat legacy baseline: sharded {}w={:?} legacy {}w={:?}",
1517 WORKERS,
1518 sharded_parallel,
1519 WORKERS,
1520 legacy_parallel
1521 );
1522 }
1523}