1use crate::epoch::{Epoch, Snapshot};
26use crate::page::CachedPage;
27use std::collections::HashMap;
28use std::path::PathBuf;
29use std::sync::atomic::{AtomicU64, Ordering};
30
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct CacheStats {
37 pub hits: u64,
38 pub misses: u64,
39 pub try_lock_misses: u64,
43}
44
45impl CacheStats {
46 pub fn hit_rate(&self) -> f64 {
48 let total = self.hits + self.misses;
49 if total == 0 {
50 0.0
51 } else {
52 self.hits as f64 / total as f64
53 }
54 }
55}
56
57pub struct PageCache {
68 map: HashMap<[u8; 32], Entry>,
69 ring: Vec<[u8; 32]>,
71 hand: usize,
72 capacity_bytes: u64,
73 used_bytes: u64,
74 dir: Option<PathBuf>,
77 persistent: bool,
78 grant: Option<crate::memory::Reservation>,
81 hits: AtomicU64,
83 misses: AtomicU64,
85}
86
87struct Entry {
88 bytes: bytes::Bytes,
89 epoch: Epoch,
90 freq: u8,
91}
92
93const FREQ_MAX: u8 = 3;
94
95impl PageCache {
96 pub fn new(capacity_bytes: u64) -> Self {
97 Self {
98 map: HashMap::new(),
99 ring: Vec::new(),
100 hand: 0,
101 capacity_bytes,
102 used_bytes: 0,
103 dir: None,
104 persistent: false,
105 grant: None,
106 hits: AtomicU64::new(0),
107 misses: AtomicU64::new(0),
108 }
109 }
110
111 pub fn with_governor(
118 mut self,
119 governor: crate::memory::MemoryGovernor,
120 class: crate::memory::MemoryClass,
121 ) -> Self {
122 self.grant = Some(
123 governor
124 .try_reserve(0, class)
125 .expect("zero-byte reservation always succeeds"),
126 );
127 self.sync_governor();
128 self
129 }
130
131 pub fn stats(&self) -> CacheStats {
134 CacheStats {
135 hits: self.hits.load(Ordering::Relaxed),
136 misses: self.misses.load(Ordering::Relaxed),
137 try_lock_misses: 0,
138 }
139 }
140
141 pub fn reset_stats(&self) {
143 self.hits.store(0, Ordering::Relaxed);
144 self.misses.store(0, Ordering::Relaxed);
145 }
146
147 pub fn with_persistence(mut self, dir: PathBuf) -> Self {
151 self.persistent = true;
152 self.dir = Some(dir.clone());
153 self.load_from_disk();
154 self.sync_governor();
157 self
158 }
159
160 pub fn insert(&mut self, page: CachedPage) {
163 let key = page.content_hash;
164 let size = page.bytes.len() as u64;
165 let epoch = page.committed_epoch;
166 let was_new = !self.map.contains_key(&key);
167 if let Some(old) = self.map.insert(
168 key,
169 Entry {
170 bytes: page.bytes.clone(),
171 epoch,
172 freq: 1,
173 },
174 ) {
175 self.used_bytes = self.used_bytes.saturating_sub(old.bytes.len() as u64);
176 } else if was_new {
177 self.ring.push(key);
178 }
179 self.used_bytes += size;
180 self.evict_if_needed();
181 self.sync_governor();
182 }
183
184 pub fn get(&mut self, content_hash: &[u8; 32], snapshot: Snapshot) -> Option<bytes::Bytes> {
187 let hit = self
188 .map
189 .get_mut(content_hash)
190 .filter(|e| e.epoch <= snapshot.epoch)
191 .map(|entry| {
192 if entry.freq < FREQ_MAX {
193 entry.freq += 1;
194 }
195 entry.bytes.clone()
196 });
197 let counter = if hit.is_some() {
198 &self.hits
199 } else {
200 &self.misses
201 };
202 counter.fetch_add(1, Ordering::Relaxed);
203 hit
204 }
205
206 pub fn try_get(&self, content_hash: &[u8; 32], snapshot: Snapshot) -> Option<bytes::Bytes> {
211 let hit = self.map.get(content_hash).and_then(|e| {
212 if e.epoch <= snapshot.epoch {
213 Some(e.bytes.clone())
214 } else {
215 None
216 }
217 });
218 let counter = if hit.is_some() {
219 &self.hits
220 } else {
221 &self.misses
222 };
223 counter.fetch_add(1, Ordering::Relaxed);
224 hit
225 }
226
227 pub fn used_bytes(&self) -> u64 {
228 self.used_bytes
229 }
230
231 pub fn len(&self) -> usize {
232 self.map.len()
233 }
234
235 pub fn is_empty(&self) -> bool {
236 self.map.is_empty()
237 }
238
239 pub fn flush_to_disk(&self) {
241 if !self.persistent {
242 return;
243 }
244 for (key, entry) in &self.map {
245 self.spill(key, entry.epoch, &entry.bytes);
246 }
247 }
248
249 fn spill(&self, key: &[u8; 32], epoch: Epoch, bytes: &[u8]) {
250 if !self.persistent {
251 return;
252 }
253 let Some(dir) = &self.dir else { return };
254 let _ = std::fs::create_dir_all(dir);
255 let hex = hex_key(key);
256 let path = dir.join(format!("{hex}.{}", epoch.0));
262 let tmp = dir.join(format!("{hex}.{}.tmp", epoch.0));
263 if std::fs::write(&tmp, bytes).is_ok() {
264 let _ = std::fs::rename(&tmp, &path);
265 }
266 }
267
268 fn load_from_disk(&mut self) {
269 let Some(dir) = &self.dir else { return };
270 let entries = match std::fs::read_dir(dir) {
271 Ok(e) => e,
272 Err(_) => return,
273 };
274 for entry in entries.flatten() {
275 let name = entry.file_name();
276 let Some(s) = name.to_str() else { continue };
277 if s.ends_with(".tmp") {
279 continue;
280 }
281 let (key, epoch) = match s.rsplit_once('.') {
284 Some((hex, suffix)) => {
285 let key = match decode_hex_key(hex) {
286 Some(k) => k,
287 None => continue,
288 };
289 let epoch = match suffix.parse::<u64>() {
290 Ok(e) => Epoch(e),
291 Err(_) => continue,
292 };
293 (key, epoch)
294 }
295 None => {
296 let key = match decode_hex_key(s) {
297 Some(k) => k,
298 None => continue,
299 };
300 (key, Epoch(u64::MAX))
301 }
302 };
303 if self.map.contains_key(&key) {
304 continue;
305 }
306 let Ok(bytes) = std::fs::read(entry.path()) else {
307 continue;
308 };
309 let size = bytes.len() as u64;
310 if self.used_bytes.saturating_add(size) > self.capacity_bytes {
311 break; }
313 self.map.insert(
314 key,
315 Entry {
316 bytes: bytes::Bytes::from(bytes),
317 epoch,
318 freq: 0,
319 },
320 );
321 self.ring.push(key);
322 self.used_bytes += size;
323 }
324 }
325
326 fn evict_if_needed(&mut self) {
329 while self.used_bytes > self.capacity_bytes && self.clock_step() {}
330 }
331
332 fn clock_step(&mut self) -> bool {
336 if self.ring.is_empty() {
337 return false;
338 }
339 let mut scanned = 0;
341 let len = self.ring.len();
342 loop {
343 let key = self.ring[self.hand];
344 let Some(entry) = self.map.get_mut(&key) else {
345 self.ring.swap_remove(self.hand);
347 if self.hand >= self.ring.len() && !self.ring.is_empty() {
348 self.hand %= self.ring.len();
349 }
350 break;
351 };
352 if entry.freq == 0 {
353 let removed = self.map.remove(&key).expect("entry present");
355 self.spill(&key, removed.epoch, &removed.bytes);
356 self.used_bytes = self.used_bytes.saturating_sub(removed.bytes.len() as u64);
357 self.ring.swap_remove(self.hand);
358 if !self.ring.is_empty() {
359 self.hand %= self.ring.len();
360 }
361 break;
362 } else {
363 entry.freq -= 1;
364 scanned += 1;
365 if scanned > len * 2 {
366 let removed = self.map.remove(&key).expect("entry present");
370 self.spill(&key, removed.epoch, &removed.bytes);
371 self.used_bytes = self.used_bytes.saturating_sub(removed.bytes.len() as u64);
372 self.ring.swap_remove(self.hand);
373 if !self.ring.is_empty() {
374 self.hand %= self.ring.len();
375 }
376 break;
377 }
378 self.hand = (self.hand + 1) % len;
379 }
380 }
381 true
382 }
383
384 fn sync_governor(&mut self) {
391 if self.grant.is_none() {
392 return;
393 }
394 loop {
395 let denied = {
396 let used = self.used_bytes;
397 let grant = self.grant.as_mut().expect("checked above");
398 grant.resize(used).is_err()
399 };
400 if !denied || !self.clock_step() {
401 break;
402 }
403 }
404 }
405
406 pub fn evict_reclaimable(&mut self, budget: u64) -> u64 {
411 let before = self.used_bytes;
412 while before - self.used_bytes < budget && self.clock_step() {}
413 self.sync_governor();
414 before - self.used_bytes
415 }
416}
417
418fn hex_key(key: &[u8; 32]) -> String {
419 let mut s = String::with_capacity(64);
420 for b in key {
421 s.push_str(&format!("{b:02x}"));
422 }
423 s
424}
425
426fn decode_hex_key(hex: &str) -> Option<[u8; 32]> {
427 if hex.len() != 64 {
428 return None;
429 }
430 let mut out = [0u8; 32];
431 for i in 0..32 {
432 out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?;
433 }
434 Some(out)
435}
436
437pub struct DecodedPageCache {
450 map: HashMap<[u8; 32], std::sync::Arc<crate::columnar::NativeColumn>>,
451 ring: Vec<[u8; 32]>,
452 hand: usize,
453 capacity_bytes: u64,
454 used_bytes: u64,
455 grant: Option<crate::memory::Reservation>,
458 hits: AtomicU64,
460 misses: AtomicU64,
462}
463
464impl DecodedPageCache {
465 pub fn new(capacity_bytes: u64) -> Self {
466 Self {
467 map: HashMap::new(),
468 ring: Vec::new(),
469 hand: 0,
470 capacity_bytes,
471 used_bytes: 0,
472 grant: None,
473 hits: AtomicU64::new(0),
474 misses: AtomicU64::new(0),
475 }
476 }
477
478 pub fn with_governor(
482 mut self,
483 governor: crate::memory::MemoryGovernor,
484 class: crate::memory::MemoryClass,
485 ) -> Self {
486 self.grant = Some(
487 governor
488 .try_reserve(0, class)
489 .expect("zero-byte reservation always succeeds"),
490 );
491 self.sync_governor();
492 self
493 }
494
495 pub fn stats(&self) -> CacheStats {
498 CacheStats {
499 hits: self.hits.load(Ordering::Relaxed),
500 misses: self.misses.load(Ordering::Relaxed),
501 try_lock_misses: 0,
502 }
503 }
504
505 pub fn reset_stats(&self) {
507 self.hits.store(0, Ordering::Relaxed);
508 self.misses.store(0, Ordering::Relaxed);
509 }
510
511 pub fn try_get(&self, key: &[u8; 32]) -> Option<std::sync::Arc<crate::columnar::NativeColumn>> {
514 match self.map.get(key).cloned() {
515 Some(v) => {
516 self.hits.fetch_add(1, Ordering::Relaxed);
517 Some(v)
518 }
519 None => {
520 self.misses.fetch_add(1, Ordering::Relaxed);
521 None
522 }
523 }
524 }
525
526 pub fn insert(&mut self, key: [u8; 32], col: std::sync::Arc<crate::columnar::NativeColumn>) {
529 let size = col.approx_bytes();
530 let was_new = !self.map.contains_key(&key);
531 if let Some(old) = self.map.insert(key, col) {
532 self.used_bytes = self.used_bytes.saturating_sub(old.approx_bytes());
533 } else if was_new {
534 self.ring.push(key);
535 }
536 self.used_bytes += size;
537 self.evict_if_needed();
538 self.sync_governor();
539 }
540
541 fn evict_if_needed(&mut self) {
542 while self.used_bytes > self.capacity_bytes && self.clock_step() {}
547 }
548
549 fn clock_step(&mut self) -> bool {
552 if self.ring.is_empty() {
553 return false;
554 }
555 let idx = self.hand.min(self.ring.len() - 1);
556 let key = self.ring.swap_remove(idx);
557 if let Some(col) = self.map.remove(&key) {
558 self.used_bytes = self.used_bytes.saturating_sub(col.approx_bytes());
559 }
560 if self.ring.is_empty() {
561 self.hand = 0;
562 } else {
563 self.hand %= self.ring.len();
564 }
565 true
566 }
567
568 fn sync_governor(&mut self) {
571 if self.grant.is_none() {
572 return;
573 }
574 loop {
575 let denied = {
576 let used = self.used_bytes;
577 let grant = self.grant.as_mut().expect("checked above");
578 grant.resize(used).is_err()
579 };
580 if !denied || !self.clock_step() {
581 break;
582 }
583 }
584 }
585
586 pub fn evict_reclaimable(&mut self, budget: u64) -> u64 {
589 let before = self.used_bytes;
590 while before - self.used_bytes < budget && self.clock_step() {}
591 self.sync_governor();
592 before - self.used_bytes
593 }
594
595 pub fn len(&self) -> usize {
596 self.map.len()
597 }
598
599 pub fn is_empty(&self) -> bool {
600 self.map.is_empty()
601 }
602
603 pub fn used_bytes(&self) -> u64 {
604 self.used_bytes
605 }
606}
607
608pub struct Sharded<T> {
613 shards: Vec<parking_lot::Mutex<T>>,
614 try_lock_misses: AtomicU64,
615}
616
617pub const CACHE_SHARDS: usize = 16;
620
621impl<T> Sharded<T> {
622 pub fn new(n_shards: usize, make: impl FnMut() -> T) -> Self {
623 let mut make = make;
624 let shards = (0..n_shards)
625 .map(|_| parking_lot::Mutex::new(make()))
626 .collect();
627 Self {
628 shards,
629 try_lock_misses: AtomicU64::new(0),
630 }
631 }
632
633 fn idx(&self, key: &[u8; 32]) -> usize {
634 let h = u32::from_le_bytes([key[0], key[1], key[2], key[3]]);
635 (h as usize) % self.shards.len()
636 }
637
638 pub fn try_lock(&self, key: &[u8; 32]) -> Option<parking_lot::MutexGuard<'_, T>> {
639 match self.shards[self.idx(key)].try_lock() {
640 Some(g) => Some(g),
641 None => {
642 self.try_lock_misses.fetch_add(1, Ordering::Relaxed);
643 None
644 }
645 }
646 }
647
648 pub fn lock(&self, key: &[u8; 32]) -> parking_lot::MutexGuard<'_, T> {
649 self.shards[self.idx(key)].lock()
650 }
651
652 pub fn try_lock_misses(&self) -> u64 {
653 self.try_lock_misses.load(Ordering::Relaxed)
654 }
655}
656
657impl Sharded<PageCache> {
658 pub fn stats(&self) -> CacheStats {
659 let mut total = CacheStats::default();
660 for s in &self.shards {
661 let st = s.lock().stats();
662 total.hits += st.hits;
663 total.misses += st.misses;
664 }
665 total.try_lock_misses = self.try_lock_misses();
666 total
667 }
668
669 pub fn reset_stats(&self) {
670 for s in &self.shards {
671 s.lock().reset_stats();
672 }
673 self.try_lock_misses.store(0, Ordering::Relaxed);
674 }
675
676 pub fn flush_to_disk(&self) {
677 for s in &self.shards {
678 s.lock().flush_to_disk();
679 }
680 }
681
682 pub fn len(&self) -> usize {
683 self.shards.iter().map(|s| s.lock().len()).sum()
684 }
685
686 #[allow(clippy::len_without_is_empty)]
687 pub fn is_empty(&self) -> bool {
688 self.len() == 0
689 }
690
691 pub fn used_bytes(&self) -> u64 {
693 self.shards.iter().map(|s| s.lock().used_bytes()).sum()
694 }
695
696 pub fn evict_reclaimable(&self, budget: u64) -> u64 {
700 let n = self.shards.len() as u64;
701 let mut freed = 0u64;
702 for (i, shard) in self.shards.iter().enumerate() {
703 if freed >= budget {
704 break;
705 }
706 let left = n - i as u64;
708 let target = (budget - freed).div_ceil(left);
709 freed += shard.lock().evict_reclaimable(target);
710 }
711 freed
712 }
713}
714
715impl crate::memory::Reclaimable for Sharded<PageCache> {
716 fn evict_reclaimable(&self, budget: u64) -> u64 {
717 <Sharded<PageCache>>::evict_reclaimable(self, budget)
718 }
719
720 fn reclaimable_bytes(&self) -> u64 {
721 self.used_bytes()
722 }
723}
724
725impl Sharded<DecodedPageCache> {
726 pub fn stats(&self) -> CacheStats {
727 let mut total = CacheStats::default();
728 for s in &self.shards {
729 let st = s.lock().stats();
730 total.hits += st.hits;
731 total.misses += st.misses;
732 }
733 total.try_lock_misses = self.try_lock_misses();
734 total
735 }
736
737 pub fn reset_stats(&self) {
738 for s in &self.shards {
739 s.lock().reset_stats();
740 }
741 self.try_lock_misses.store(0, Ordering::Relaxed);
742 }
743
744 pub fn len(&self) -> usize {
745 self.shards.iter().map(|s| s.lock().len()).sum()
746 }
747
748 pub fn used_bytes(&self) -> u64 {
750 self.shards.iter().map(|s| s.lock().used_bytes()).sum()
751 }
752
753 pub fn evict_reclaimable(&self, budget: u64) -> u64 {
756 let n = self.shards.len() as u64;
757 let mut freed = 0u64;
758 for (i, shard) in self.shards.iter().enumerate() {
759 if freed >= budget {
760 break;
761 }
762 let left = n - i as u64;
763 let target = (budget - freed).div_ceil(left);
764 freed += shard.lock().evict_reclaimable(target);
765 }
766 freed
767 }
768}
769
770impl crate::memory::Reclaimable for Sharded<DecodedPageCache> {
771 fn evict_reclaimable(&self, budget: u64) -> u64 {
772 <Sharded<DecodedPageCache>>::evict_reclaimable(self, budget)
773 }
774
775 fn reclaimable_bytes(&self) -> u64 {
776 self.used_bytes()
777 }
778}
779
780#[cfg(test)]
781mod tests {
782 use super::*;
783 use tempfile::tempdir;
784
785 fn page(hash: [u8; 32], epoch: u64, data: &[u8]) -> CachedPage {
786 CachedPage {
787 committed_epoch: Epoch(epoch),
788 content_hash: hash,
789 bytes: bytes::Bytes::copy_from_slice(data),
790 }
791 }
792
793 #[test]
794 fn mvcc_visibility() {
795 let mut cache = PageCache::new(1 << 20);
796 let hash = [1u8; 32];
797 cache.insert(page(hash, 3, b"v3"));
798 assert!(cache.get(&hash, Snapshot::at(Epoch(2))).is_none());
800 assert_eq!(
802 cache.get(&hash, Snapshot::at(Epoch(3))),
803 Some(bytes::Bytes::copy_from_slice(b"v3"))
804 );
805 }
806
807 #[test]
808 fn hit_miss_counters_track_lookups() {
809 let mut cache = PageCache::new(1 << 20);
810 let hash = [7u8; 32];
811 cache.insert(page(hash, 1, b"v"));
812 assert_eq!(
813 cache.stats(),
814 CacheStats {
815 hits: 0,
816 misses: 0,
817 try_lock_misses: 0
818 }
819 );
820
821 assert!(cache.get(&hash, Snapshot::at(Epoch(1))).is_some());
823 assert!(cache.try_get(&hash, Snapshot::at(Epoch(1))).is_some());
824 assert!(cache.get(&[9u8; 32], Snapshot::at(Epoch(1))).is_none());
826 assert!(cache.get(&hash, Snapshot::at(Epoch(0))).is_none());
827
828 let s = cache.stats();
829 assert_eq!(
830 s,
831 CacheStats {
832 hits: 2,
833 misses: 2,
834 try_lock_misses: 0
835 }
836 );
837 assert!((s.hit_rate() - 0.5).abs() < 1e-9);
838
839 cache.reset_stats();
840 assert_eq!(
841 cache.stats(),
842 CacheStats {
843 hits: 0,
844 misses: 0,
845 try_lock_misses: 0
846 }
847 );
848 assert_eq!(cache.stats().hit_rate(), 0.0);
849 }
850
851 #[test]
852 fn content_addressed_replacement_ages_out_old() {
853 let mut cache = PageCache::new(1 << 20);
854 let hash_a = [1u8; 32];
855 let hash_b = [2u8; 32];
856 cache.insert(page(hash_a, 1, b"old"));
857 cache.insert(page(hash_b, 1, b"new"));
858 assert_eq!(cache.len(), 2);
859 assert!(cache.get(&hash_a, Snapshot::at(Epoch(1))).is_some());
860 }
861
862 #[test]
863 fn capacity_eviction_removes_cold_first() {
864 let mut cache = PageCache::new(10);
865 cache.insert(page([1u8; 32], 1, b"0123456789")); assert_eq!(cache.used_bytes(), 10);
867 cache.insert(page([2u8; 32], 1, b"x")); assert_eq!(cache.used_bytes(), 1);
869 assert!(cache.get(&[1u8; 32], Snapshot::at(Epoch(1))).is_none());
870 assert!(cache.get(&[2u8; 32], Snapshot::at(Epoch(1))).is_some());
871 }
872
873 #[test]
874 fn frequency_keeps_hot_pages_alive() {
875 let mut cache = PageCache::new(2);
878 let a = [10u8; 32];
879 cache.insert(page(a, 1, b"A"));
880 for _ in 0..5 {
882 let _ = cache.get(&a, Snapshot::at(Epoch(1)));
883 }
884 cache.insert(page([20u8; 32], 1, b"B"));
885 cache.insert(page([30u8; 32], 1, b"C"));
886 assert!(
888 cache.get(&a, Snapshot::at(Epoch(1))).is_some(),
889 "hot page should survive eviction"
890 );
891 }
892
893 #[test]
894 fn persistent_cache_survives_restart() {
895 let dir = tempdir().unwrap();
896 let data = b"page-payload-1234";
897 let hash = [42u8; 32];
898
899 {
901 let mut cache = PageCache::new(1 << 20).with_persistence(dir.path().to_path_buf());
902 cache.insert(page(hash, 5, data));
903 cache.flush_to_disk();
904 }
905 let backing = dir.path().join(format!("{}.{}", hex_key(&hash), 5));
907 assert!(backing.exists(), "cache file should be spilled");
908
909 let mut cache = PageCache::new(1 << 20).with_persistence(dir.path().to_path_buf());
911 let got = cache.get(&hash, Snapshot::at(Epoch(u64::MAX)));
912 assert_eq!(got, Some(bytes::Bytes::copy_from_slice(data)));
913 }
914
915 #[test]
916 fn try_get_does_not_block() {
917 let mut cache = PageCache::new(1 << 20);
918 let hash = [7u8; 32];
919 cache.insert(page(hash, 1, b"hi"));
920 let got = cache.try_get(&hash, Snapshot::at(Epoch(1)));
921 assert!(got.is_some());
922 let miss = cache.try_get(&[8u8; 32], Snapshot::at(Epoch(1)));
923 assert!(miss.is_none());
924 }
925
926 #[test]
927 fn hot_eviction_storm_does_not_orphan_entries() {
928 let mut cache = PageCache::new(6); let mut next = 1u8;
933 for _ in 0..200 {
934 let key = [next; 32];
935 cache.insert(page(key, 1, b"aa")); for _ in 0..4 {
939 let _ = cache.get(&key, Snapshot::at(Epoch(1)));
940 }
941 next = next.wrapping_add(1);
942 }
943 assert!(
946 cache.used_bytes() <= cache.capacity_bytes + 2, "used_bytes {} leaked past capacity {}",
948 cache.used_bytes(),
949 cache.capacity_bytes
950 );
951 let mut ok = true;
953 for k in &cache.ring {
954 if !cache.map.contains_key(k) {
955 ok = false;
956 }
957 }
958 assert!(
959 ok,
960 "ring references a key absent from the map (orphan slot)"
961 );
962 assert_eq!(
963 cache.ring.len(),
964 cache.map.len(),
965 "ring/map size mismatch (orphaned entries or slots)"
966 );
967 }
968
969 #[test]
970 fn decoded_cache_hit_miss_counters() {
971 use crate::columnar::NativeColumn;
974 let mut cache = DecodedPageCache::new(1 << 20);
975 let key = [9u8; 32];
976 cache.insert(
977 key,
978 std::sync::Arc::new(NativeColumn::Int64 {
979 data: vec![1, 2, 3],
980 validity: vec![],
981 }),
982 );
983 assert!(cache.try_get(&key).is_some());
985 assert!(cache.try_get(&key).is_some());
986 assert!(cache.try_get(&[0u8; 32]).is_none());
987 let s = cache.stats();
988 assert_eq!(s.hits, 2);
989 assert_eq!(s.misses, 1);
990 assert!(s.hit_rate() > 0.66 && s.hit_rate() < 0.67);
991 cache.reset_stats();
992 assert_eq!(cache.stats(), CacheStats::default());
993 }
994
995 #[test]
996 fn governor_attached_cache_reports_live_bytes() {
997 use crate::memory::{GovernorConfig, MemoryClass, MemoryGovernor};
998 let governor =
999 MemoryGovernor::new(GovernorConfig::new(1 << 20).with_reserved_floor(0)).unwrap();
1000 let mut cache =
1001 PageCache::new(1 << 20).with_governor(governor.clone(), MemoryClass::PageCache);
1002 cache.insert(page([1u8; 32], 1, b"0123456789"));
1003 cache.insert(page([2u8; 32], 1, b"abcde"));
1004 assert_eq!(governor.usage(MemoryClass::PageCache), 15);
1006 assert_eq!(cache.used_bytes(), 15);
1007 let mut cache = PageCache::new(10).with_governor(governor.clone(), MemoryClass::PageCache);
1009 cache.insert(page([3u8; 32], 1, b"0123456789"));
1010 cache.insert(page([4u8; 32], 1, b"x"));
1011 assert_eq!(cache.used_bytes(), 1);
1012 assert_eq!(governor.usage(MemoryClass::PageCache), 16);
1013 drop(cache);
1015 assert_eq!(governor.usage(MemoryClass::PageCache), 15);
1016 }
1017
1018 #[test]
1019 fn governor_denial_sheds_coldest_entries() {
1020 use crate::memory::{GovernorConfig, MemoryClass, MemoryGovernor};
1021 let governor = MemoryGovernor::new(GovernorConfig::new(24).with_reserved_floor(0)).unwrap();
1024 let mut cache =
1025 PageCache::new(1 << 20).with_governor(governor.clone(), MemoryClass::PageCache);
1026 for i in 0..8u8 {
1027 cache.insert(page([i; 32], 1, b"01234567")); }
1029 assert!(cache.used_bytes() <= 24);
1032 assert_eq!(governor.usage(MemoryClass::PageCache), cache.used_bytes());
1033 assert!(cache.get(&[7u8; 32], Snapshot::at(Epoch(1))).is_some());
1035 }
1036
1037 #[test]
1038 fn evict_reclaimable_frees_budget_and_keeps_accounting() {
1039 use crate::memory::{GovernorConfig, MemoryClass, MemoryGovernor};
1040 let governor =
1041 MemoryGovernor::new(GovernorConfig::new(1 << 20).with_reserved_floor(0)).unwrap();
1042 let mut cache =
1043 PageCache::new(1 << 20).with_governor(governor.clone(), MemoryClass::PageCache);
1044 for i in 0..4u8 {
1045 cache.insert(page([i; 32], 1, b"0123456789")); }
1047 assert_eq!(governor.usage(MemoryClass::PageCache), 40);
1048 let freed = cache.evict_reclaimable(25);
1049 assert!(freed >= 25, "freed {freed}");
1050 assert_eq!(cache.used_bytes(), 40 - freed);
1051 assert_eq!(governor.usage(MemoryClass::PageCache), cache.used_bytes());
1052 let remaining = cache.used_bytes();
1054 let freed = cache.evict_reclaimable(1 << 20);
1055 assert_eq!(freed, remaining);
1056 assert_eq!(cache.used_bytes(), 0);
1057 assert_eq!(governor.usage(MemoryClass::PageCache), 0);
1058 }
1059
1060 #[test]
1061 fn decoded_cache_governor_reporting() {
1062 use crate::columnar::NativeColumn;
1063 use crate::memory::{GovernorConfig, MemoryClass, MemoryGovernor};
1064 let governor =
1065 MemoryGovernor::new(GovernorConfig::new(1 << 20).with_reserved_floor(0)).unwrap();
1066 let mut cache = DecodedPageCache::new(1 << 20)
1067 .with_governor(governor.clone(), MemoryClass::DecodedCache);
1068 cache.insert(
1069 [9u8; 32],
1070 std::sync::Arc::new(NativeColumn::Int64 {
1071 data: vec![1, 2, 3],
1072 validity: vec![],
1073 }),
1074 );
1075 assert_eq!(
1076 governor.usage(MemoryClass::DecodedCache),
1077 cache.used_bytes()
1078 );
1079 assert!(cache.used_bytes() > 0);
1080 let held = cache.used_bytes();
1081 let freed = cache.evict_reclaimable(u64::MAX);
1082 assert_eq!(freed, held);
1083 assert_eq!(cache.used_bytes(), 0);
1084 assert_eq!(governor.usage(MemoryClass::DecodedCache), 0);
1085 }
1086}