diff --git a/src/ds/clock_ring.rs b/src/ds/clock_ring.rs index a3224ce..c886397 100644 --- a/src/ds/clock_ring.rs +++ b/src/ds/clock_ring.rs @@ -262,6 +262,10 @@ pub struct ClockRing { index: FxHashMap, hand: usize, len: usize, + #[cfg(feature = "metrics")] + sweep_hand_advances: u64, + #[cfg(feature = "metrics")] + sweep_ref_bit_resets: u64, } /// Thread-safe wrapper around [`ClockRing`] using `parking_lot::RwLock`. @@ -1102,6 +1106,10 @@ where index: FxHashMap::with_capacity_and_hasher(capacity, Default::default()), hand: 0, len: 0, + #[cfg(feature = "metrics")] + sweep_hand_advances: 0, + #[cfg(feature = "metrics")] + sweep_ref_bit_resets: 0, } } @@ -1173,6 +1181,11 @@ where self.referenced.fill(false); self.len = 0; self.hand = 0; + #[cfg(feature = "metrics")] + { + self.sweep_hand_advances = 0; + self.sweep_ref_bit_resets = 0; + } } /// Clears all entries and shrinks internal storage. @@ -1195,6 +1208,20 @@ where self.referenced.shrink_to_fit(); } + /// Cumulative hand advances during sweep operations. + #[cfg(feature = "metrics")] + #[inline] + pub fn sweep_hand_advances(&self) -> u64 { + self.sweep_hand_advances + } + + /// Cumulative reference-bit resets during sweep operations. + #[cfg(feature = "metrics")] + #[inline] + pub fn sweep_ref_bit_resets(&self) -> u64 { + self.sweep_ref_bit_resets + } + /// Returns an approximate memory footprint in bytes. /// /// # Example @@ -1491,7 +1518,15 @@ where let idx = self.hand; if self.referenced[idx] { self.referenced[idx] = false; + #[cfg(feature = "metrics")] + { + self.sweep_ref_bit_resets += 1; + } self.advance_hand(); + #[cfg(feature = "metrics")] + { + self.sweep_hand_advances += 1; + } continue; } @@ -1506,6 +1541,10 @@ where self.referenced[idx] = false; self.index.insert(key, idx); self.advance_hand(); + #[cfg(feature = "metrics")] + { + self.sweep_hand_advances += 1; + } return Some((evicted.key, evicted.value)); } debug_assert!( @@ -1587,7 +1626,15 @@ where if self.slots[idx].is_some() { if self.referenced[idx] { self.referenced[idx] = false; + #[cfg(feature = "metrics")] + { + self.sweep_ref_bit_resets += 1; + } self.advance_hand(); + #[cfg(feature = "metrics")] + { + self.sweep_hand_advances += 1; + } continue; } @@ -1596,9 +1643,17 @@ where self.referenced[idx] = false; self.len -= 1; self.advance_hand(); + #[cfg(feature = "metrics")] + { + self.sweep_hand_advances += 1; + } return Some((evicted.key, evicted.value)); } self.advance_hand(); + #[cfg(feature = "metrics")] + { + self.sweep_hand_advances += 1; + } } None } diff --git a/src/metrics/exporter.rs b/src/metrics/exporter.rs index 94d738b..9e39bba 100644 --- a/src/metrics/exporter.rs +++ b/src/metrics/exporter.rs @@ -2,7 +2,10 @@ use std::io::Write; use std::sync::Mutex; use crate::metrics::snapshot::{ - CacheMetricsSnapshot, LfuMetricsSnapshot, LruKMetricsSnapshot, LruMetricsSnapshot, + ArcMetricsSnapshot, CacheMetricsSnapshot, CarMetricsSnapshot, ClockMetricsSnapshot, + ClockProMetricsSnapshot, CoreOnlyMetricsSnapshot, LfuMetricsSnapshot, LruKMetricsSnapshot, + LruMetricsSnapshot, MfuMetricsSnapshot, NruMetricsSnapshot, S3FifoMetricsSnapshot, + SlruMetricsSnapshot, TwoQMetricsSnapshot, }; use crate::metrics::traits::MetricsExporter; @@ -323,3 +326,370 @@ impl MetricsExporter for Prometheus self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); } } + +impl MetricsExporter + for PrometheusTextExporter +{ + fn export(&self, snapshot: &CoreOnlyMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter for PrometheusTextExporter { + fn export(&self, snapshot: &ArcMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter( + &self.metric_name("t1_to_t2_promotions_total"), + snapshot.t1_to_t2_promotions, + ); + self.write_counter( + &self.metric_name("b1_ghost_hits_total"), + snapshot.b1_ghost_hits, + ); + self.write_counter( + &self.metric_name("b2_ghost_hits_total"), + snapshot.b2_ghost_hits, + ); + self.write_counter(&self.metric_name("p_increases_total"), snapshot.p_increases); + self.write_counter(&self.metric_name("p_decreases_total"), snapshot.p_decreases); + self.write_counter( + &self.metric_name("t1_evictions_total"), + snapshot.t1_evictions, + ); + self.write_counter( + &self.metric_name("t2_evictions_total"), + snapshot.t2_evictions, + ); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter for PrometheusTextExporter { + fn export(&self, snapshot: &CarMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter( + &self.metric_name("recent_to_frequent_promotions_total"), + snapshot.recent_to_frequent_promotions, + ); + self.write_counter( + &self.metric_name("ghost_recent_hits_total"), + snapshot.ghost_recent_hits, + ); + self.write_counter( + &self.metric_name("ghost_frequent_hits_total"), + snapshot.ghost_frequent_hits, + ); + self.write_counter( + &self.metric_name("target_increases_total"), + snapshot.target_increases, + ); + self.write_counter( + &self.metric_name("target_decreases_total"), + snapshot.target_decreases, + ); + self.write_counter(&self.metric_name("hand_sweeps_total"), snapshot.hand_sweeps); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter for PrometheusTextExporter { + fn export(&self, snapshot: &ClockMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter( + &self.metric_name("hand_advances_total"), + snapshot.hand_advances, + ); + self.write_counter( + &self.metric_name("ref_bit_resets_total"), + snapshot.ref_bit_resets, + ); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter + for PrometheusTextExporter +{ + fn export(&self, snapshot: &ClockProMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter( + &self.metric_name("cold_to_hot_promotions_total"), + snapshot.cold_to_hot_promotions, + ); + self.write_counter( + &self.metric_name("hot_to_cold_demotions_total"), + snapshot.hot_to_cold_demotions, + ); + self.write_counter( + &self.metric_name("test_insertions_total"), + snapshot.test_insertions, + ); + self.write_counter(&self.metric_name("test_hits_total"), snapshot.test_hits); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter for PrometheusTextExporter { + fn export(&self, snapshot: &MfuMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter( + &self.metric_name("pop_mfu_calls_total"), + snapshot.pop_mfu_calls, + ); + self.write_counter( + &self.metric_name("pop_mfu_found_total"), + snapshot.pop_mfu_found, + ); + self.write_counter( + &self.metric_name("peek_mfu_calls_total"), + snapshot.peek_mfu_calls, + ); + self.write_counter( + &self.metric_name("peek_mfu_found_total"), + snapshot.peek_mfu_found, + ); + self.write_counter( + &self.metric_name("frequency_calls_total"), + snapshot.frequency_calls, + ); + self.write_counter( + &self.metric_name("frequency_found_total"), + snapshot.frequency_found, + ); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter for PrometheusTextExporter { + fn export(&self, snapshot: &NruMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter(&self.metric_name("sweep_steps_total"), snapshot.sweep_steps); + self.write_counter( + &self.metric_name("ref_bit_resets_total"), + snapshot.ref_bit_resets, + ); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter for PrometheusTextExporter { + fn export(&self, snapshot: &SlruMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter( + &self.metric_name("probationary_to_protected_total"), + snapshot.probationary_to_protected, + ); + self.write_counter( + &self.metric_name("protected_evictions_total"), + snapshot.protected_evictions, + ); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter for PrometheusTextExporter { + fn export(&self, snapshot: &TwoQMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter( + &self.metric_name("a1in_to_am_promotions_total"), + snapshot.a1in_to_am_promotions, + ); + self.write_counter( + &self.metric_name("a1out_ghost_hits_total"), + snapshot.a1out_ghost_hits, + ); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} + +impl MetricsExporter for PrometheusTextExporter { + fn export(&self, snapshot: &S3FifoMetricsSnapshot) { + self.write_counter(&self.metric_name("get_calls_total"), snapshot.get_calls); + self.write_counter(&self.metric_name("get_hits_total"), snapshot.get_hits); + self.write_counter(&self.metric_name("get_misses_total"), snapshot.get_misses); + self.write_counter( + &self.metric_name("insert_calls_total"), + snapshot.insert_calls, + ); + self.write_counter( + &self.metric_name("insert_updates_total"), + snapshot.insert_updates, + ); + self.write_counter(&self.metric_name("insert_new_total"), snapshot.insert_new); + self.write_counter(&self.metric_name("evict_calls_total"), snapshot.evict_calls); + self.write_counter( + &self.metric_name("evicted_entries_total"), + snapshot.evicted_entries, + ); + self.write_counter(&self.metric_name("promotions_total"), snapshot.promotions); + self.write_counter( + &self.metric_name("main_reinserts_total"), + snapshot.main_reinserts, + ); + self.write_counter( + &self.metric_name("small_evictions_total"), + snapshot.small_evictions, + ); + self.write_counter( + &self.metric_name("main_evictions_total"), + snapshot.main_evictions, + ); + self.write_counter(&self.metric_name("ghost_hits_total"), snapshot.ghost_hits); + self.write_gauge(&self.metric_name("cache_len"), snapshot.cache_len as u64); + self.write_gauge(&self.metric_name("capacity"), snapshot.capacity as u64); + } +} diff --git a/src/metrics/metrics_impl.rs b/src/metrics/metrics_impl.rs index df42459..ecc7de2 100644 --- a/src/metrics/metrics_impl.rs +++ b/src/metrics/metrics_impl.rs @@ -1,8 +1,10 @@ use crate::metrics::cell::MetricsCell; use crate::metrics::traits::{ + ArcMetricsRecorder, CarMetricsRecorder, ClockMetricsRecorder, ClockProMetricsRecorder, CoreMetricsRecorder, FifoMetricsReadRecorder, FifoMetricsRecorder, LfuMetricsReadRecorder, LfuMetricsRecorder, LruKMetricsReadRecorder, LruKMetricsRecorder, LruMetricsReadRecorder, - LruMetricsRecorder, + LruMetricsRecorder, MfuMetricsReadRecorder, MfuMetricsRecorder, NruMetricsRecorder, + S3FifoMetricsRecorder, SlruMetricsRecorder, TwoQMetricsRecorder, }; #[derive(Debug, Default)] @@ -532,3 +534,619 @@ impl LruKMetricsReadRecorder for &LruKMetrics { self.k_distance_rank_scan_steps.incr(); } } + +// --------------------------------------------------------------------------- +// CoreOnlyMetrics (LIFO, Random) +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct CoreOnlyMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, +} + +impl CoreMetricsRecorder for CoreOnlyMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +// --------------------------------------------------------------------------- +// ArcMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct ArcMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub t1_to_t2_promotions: u64, + pub b1_ghost_hits: u64, + pub b2_ghost_hits: u64, + pub p_increases: u64, + pub p_decreases: u64, + pub t1_evictions: u64, + pub t2_evictions: u64, +} + +impl CoreMetricsRecorder for ArcMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl ArcMetricsRecorder for ArcMetrics { + fn record_t1_to_t2_promotion(&mut self) { + self.t1_to_t2_promotions += 1; + } + fn record_b1_ghost_hit(&mut self) { + self.b1_ghost_hits += 1; + } + fn record_b2_ghost_hit(&mut self) { + self.b2_ghost_hits += 1; + } + fn record_p_increase(&mut self) { + self.p_increases += 1; + } + fn record_p_decrease(&mut self) { + self.p_decreases += 1; + } + fn record_t1_eviction(&mut self) { + self.t1_evictions += 1; + } + fn record_t2_eviction(&mut self) { + self.t2_evictions += 1; + } +} + +// --------------------------------------------------------------------------- +// CarMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct CarMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub recent_to_frequent_promotions: u64, + pub ghost_recent_hits: u64, + pub ghost_frequent_hits: u64, + pub target_increases: u64, + pub target_decreases: u64, + pub hand_sweeps: u64, +} + +impl CoreMetricsRecorder for CarMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl CarMetricsRecorder for CarMetrics { + fn record_recent_to_frequent_promotion(&mut self) { + self.recent_to_frequent_promotions += 1; + } + fn record_ghost_recent_hit(&mut self) { + self.ghost_recent_hits += 1; + } + fn record_ghost_frequent_hit(&mut self) { + self.ghost_frequent_hits += 1; + } + fn record_target_increase(&mut self) { + self.target_increases += 1; + } + fn record_target_decrease(&mut self) { + self.target_decreases += 1; + } + fn record_hand_sweep(&mut self) { + self.hand_sweeps += 1; + } +} + +// --------------------------------------------------------------------------- +// ClockMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct ClockMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub hand_advances: u64, + pub ref_bit_resets: u64, +} + +impl CoreMetricsRecorder for ClockMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl ClockMetricsRecorder for ClockMetrics { + fn record_hand_advance(&mut self) { + self.hand_advances += 1; + } + fn record_ref_bit_reset(&mut self) { + self.ref_bit_resets += 1; + } +} + +// --------------------------------------------------------------------------- +// ClockProMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct ClockProMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub cold_to_hot_promotions: u64, + pub hot_to_cold_demotions: u64, + pub test_insertions: u64, + pub test_hits: u64, +} + +impl CoreMetricsRecorder for ClockProMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl ClockProMetricsRecorder for ClockProMetrics { + fn record_cold_to_hot_promotion(&mut self) { + self.cold_to_hot_promotions += 1; + } + fn record_hot_to_cold_demotion(&mut self) { + self.hot_to_cold_demotions += 1; + } + fn record_test_insertion(&mut self) { + self.test_insertions += 1; + } + fn record_test_hit(&mut self) { + self.test_hits += 1; + } +} + +// --------------------------------------------------------------------------- +// MfuMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct MfuMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub pop_mfu_calls: u64, + pub pop_mfu_found: u64, + pub peek_mfu_calls: MetricsCell, + pub peek_mfu_found: MetricsCell, + pub frequency_calls: MetricsCell, + pub frequency_found: MetricsCell, +} + +impl CoreMetricsRecorder for MfuMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl MfuMetricsRecorder for MfuMetrics { + fn record_pop_mfu_call(&mut self) { + self.pop_mfu_calls += 1; + } + fn record_pop_mfu_found(&mut self) { + self.pop_mfu_found += 1; + } + fn record_peek_mfu_call(&mut self) { + self.peek_mfu_calls.incr(); + } + fn record_peek_mfu_found(&mut self) { + self.peek_mfu_found.incr(); + } + fn record_frequency_call(&mut self) { + self.frequency_calls.incr(); + } + fn record_frequency_found(&mut self) { + self.frequency_found.incr(); + } +} + +impl MfuMetricsReadRecorder for &MfuMetrics { + fn record_peek_mfu_call(&self) { + self.peek_mfu_calls.incr(); + } + fn record_peek_mfu_found(&self) { + self.peek_mfu_found.incr(); + } + fn record_frequency_call(&self) { + self.frequency_calls.incr(); + } + fn record_frequency_found(&self) { + self.frequency_found.incr(); + } +} + +// --------------------------------------------------------------------------- +// NruMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct NruMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub sweep_steps: u64, + pub ref_bit_resets: u64, +} + +impl CoreMetricsRecorder for NruMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl NruMetricsRecorder for NruMetrics { + fn record_sweep_step(&mut self) { + self.sweep_steps += 1; + } + fn record_ref_bit_reset(&mut self) { + self.ref_bit_resets += 1; + } +} + +// --------------------------------------------------------------------------- +// SlruMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct SlruMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub probationary_to_protected: u64, + pub protected_evictions: u64, +} + +impl CoreMetricsRecorder for SlruMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl SlruMetricsRecorder for SlruMetrics { + fn record_probationary_to_protected(&mut self) { + self.probationary_to_protected += 1; + } + fn record_protected_eviction(&mut self) { + self.protected_evictions += 1; + } +} + +// --------------------------------------------------------------------------- +// TwoQMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default)] +pub struct TwoQMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub a1in_to_am_promotions: u64, + pub a1out_ghost_hits: u64, +} + +impl CoreMetricsRecorder for TwoQMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl TwoQMetricsRecorder for TwoQMetrics { + fn record_a1in_to_am_promotion(&mut self) { + self.a1in_to_am_promotions += 1; + } + fn record_a1out_ghost_hit(&mut self) { + self.a1out_ghost_hits += 1; + } +} + +// --------------------------------------------------------------------------- +// S3FifoMetrics +// --------------------------------------------------------------------------- + +#[derive(Debug, Default, Clone)] +pub struct S3FifoMetrics { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + pub evict_calls: u64, + pub evicted_entries: u64, + pub promotions: u64, + pub main_reinserts: u64, + pub small_evictions: u64, + pub main_evictions: u64, + pub ghost_hits: u64, +} + +impl CoreMetricsRecorder for S3FifoMetrics { + fn record_get_hit(&mut self) { + self.get_calls += 1; + self.get_hits += 1; + } + fn record_get_miss(&mut self) { + self.get_calls += 1; + self.get_misses += 1; + } + fn record_insert_call(&mut self) { + self.insert_calls += 1; + } + fn record_insert_new(&mut self) { + self.insert_new += 1; + } + fn record_insert_update(&mut self) { + self.insert_updates += 1; + } + fn record_evict_call(&mut self) { + self.evict_calls += 1; + } + fn record_evicted_entry(&mut self) { + self.evicted_entries += 1; + } + fn record_clear(&mut self) {} +} + +impl S3FifoMetricsRecorder for S3FifoMetrics { + fn record_promotion(&mut self) { + self.promotions += 1; + } + fn record_main_reinsert(&mut self) { + self.main_reinserts += 1; + } + fn record_small_eviction(&mut self) { + self.small_evictions += 1; + } + fn record_main_eviction(&mut self) { + self.main_evictions += 1; + } + fn record_ghost_hit(&mut self) { + self.ghost_hits += 1; + } +} diff --git a/src/metrics/snapshot.rs b/src/metrics/snapshot.rs index 228cb24..e928465 100644 --- a/src/metrics/snapshot.rs +++ b/src/metrics/snapshot.rs @@ -121,3 +121,218 @@ pub struct LruKMetricsSnapshot { pub cache_len: usize, pub capacity: usize, } + +#[derive(Debug, Default, Clone, Copy)] +pub struct CoreOnlyMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct ArcMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub t1_to_t2_promotions: u64, + pub b1_ghost_hits: u64, + pub b2_ghost_hits: u64, + pub p_increases: u64, + pub p_decreases: u64, + pub t1_evictions: u64, + pub t2_evictions: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct CarMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub recent_to_frequent_promotions: u64, + pub ghost_recent_hits: u64, + pub ghost_frequent_hits: u64, + pub target_increases: u64, + pub target_decreases: u64, + pub hand_sweeps: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct ClockMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub hand_advances: u64, + pub ref_bit_resets: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct ClockProMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub cold_to_hot_promotions: u64, + pub hot_to_cold_demotions: u64, + pub test_insertions: u64, + pub test_hits: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct MfuMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub pop_mfu_calls: u64, + pub pop_mfu_found: u64, + pub peek_mfu_calls: u64, + pub peek_mfu_found: u64, + pub frequency_calls: u64, + pub frequency_found: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct NruMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub sweep_steps: u64, + pub ref_bit_resets: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct SlruMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub probationary_to_protected: u64, + pub protected_evictions: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct TwoQMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub a1in_to_am_promotions: u64, + pub a1out_ghost_hits: u64, + + pub cache_len: usize, + pub capacity: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct S3FifoMetricsSnapshot { + pub get_calls: u64, + pub get_hits: u64, + pub get_misses: u64, + + pub insert_calls: u64, + pub insert_updates: u64, + pub insert_new: u64, + + pub evict_calls: u64, + pub evicted_entries: u64, + + pub promotions: u64, + pub main_reinserts: u64, + pub small_evictions: u64, + pub main_evictions: u64, + pub ghost_hits: u64, + + pub cache_len: usize, + pub capacity: usize, +} diff --git a/src/metrics/traits.rs b/src/metrics/traits.rs index 4a5bd15..91db925 100644 --- a/src/metrics/traits.rs +++ b/src/metrics/traits.rs @@ -8,25 +8,25 @@ //! ## Architecture //! //! ```text -//! ┌─────────────────────────────┐ -//! │ CoreMetricsRecorder │ -//! │ get_hit/get_miss/insert │ -//! │ evict/clear │ -//! └──────────────┬──────────────┘ -//! │ -//! ┌─────────────────────────┼─────────────────────────┐ -//! │ │ │ -//! ▼ ▼ ▼ -//! ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐ -//! │ FifoMetricsRecorder │ │ LruMetricsRecorder │ │ LfuMetricsRecorder │ -//! │ pop_oldest/peek/age │ │ pop_lru/peek/touch │ │ pop_lfu/peek/frequency │ -//! └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘ -//! │ -//! ▼ -//! ┌─────────────────────────┐ -//! │ LruKMetricsRecorder │ -//! │ pop_lru_k/k_distance │ -//! └─────────────────────────┘ +//! ┌─────────────────────────────┐ +//! │ CoreMetricsRecorder │ +//! │ get_hit/get_miss/insert │ +//! │ evict/clear │ +//! └──────────────┬──────────────┘ +//! │ +//! ┌──────────────┬───────────────┬───────────────┼───────────────┬───────────────┐ +//! │ │ │ │ │ │ +//! ▼ ▼ ▼ ▼ ▼ ▼ +//! ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ +//! │ Fifo │ │ Lru │ │ Lfu │ │ Arc │ │ Clock │ │ S3Fifo/ │ +//! │Recorder│ │ Recorder │ │ Recorder │ │ Recorder │ │ Recorder │ │ Car/Slru/ │ +//! └────────┘ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ │ TwoQ/Mfu/ │ +//! │ │ NRU/ClkPro │ +//! ▼ └──────────────┘ +//! ┌──────────┐ +//! │ LruK │ +//! │ Recorder │ +//! └──────────┘ //! //! Consumption (decoupled from recording): //! ┌──────────────────────────────┐ ┌──────────────────────────────┐ @@ -145,6 +145,86 @@ pub trait LruKMetricsReadRecorder { fn record_k_distance_rank_scan_step(&self); } +/// Metrics for ARC behavior (adaptive replacement with ghost lists). +pub trait ArcMetricsRecorder: CoreMetricsRecorder { + fn record_t1_to_t2_promotion(&mut self); + fn record_b1_ghost_hit(&mut self); + fn record_b2_ghost_hit(&mut self); + fn record_p_increase(&mut self); + fn record_p_decrease(&mut self); + fn record_t1_eviction(&mut self); + fn record_t2_eviction(&mut self); +} + +/// Metrics for CAR behavior (clock with adaptive replacement). +pub trait CarMetricsRecorder: CoreMetricsRecorder { + fn record_recent_to_frequent_promotion(&mut self); + fn record_ghost_recent_hit(&mut self); + fn record_ghost_frequent_hit(&mut self); + fn record_target_increase(&mut self); + fn record_target_decrease(&mut self); + fn record_hand_sweep(&mut self); +} + +/// Metrics for Clock behavior (clock hand sweep). +pub trait ClockMetricsRecorder: CoreMetricsRecorder { + fn record_hand_advance(&mut self); + fn record_ref_bit_reset(&mut self); +} + +/// Metrics for Clock-PRO behavior (hot/cold/test states). +pub trait ClockProMetricsRecorder: CoreMetricsRecorder { + fn record_cold_to_hot_promotion(&mut self); + fn record_hot_to_cold_demotion(&mut self); + fn record_test_insertion(&mut self); + fn record_test_hit(&mut self); +} + +/// Metrics for MFU behavior (most frequently used eviction). +pub trait MfuMetricsRecorder: CoreMetricsRecorder { + fn record_pop_mfu_call(&mut self); + fn record_pop_mfu_found(&mut self); + fn record_peek_mfu_call(&mut self); + fn record_peek_mfu_found(&mut self); + fn record_frequency_call(&mut self); + fn record_frequency_found(&mut self); +} + +/// Read-only MFU metrics for &self methods (uses interior mutability). +pub trait MfuMetricsReadRecorder { + fn record_peek_mfu_call(&self); + fn record_peek_mfu_found(&self); + fn record_frequency_call(&self); + fn record_frequency_found(&self); +} + +/// Metrics for NRU behavior (not recently used, clock sweep). +pub trait NruMetricsRecorder: CoreMetricsRecorder { + fn record_sweep_step(&mut self); + fn record_ref_bit_reset(&mut self); +} + +/// Metrics for SLRU behavior (segmented LRU). +pub trait SlruMetricsRecorder: CoreMetricsRecorder { + fn record_probationary_to_protected(&mut self); + fn record_protected_eviction(&mut self); +} + +/// Metrics for Two-Q behavior (A1in/A1out/Am queues). +pub trait TwoQMetricsRecorder: CoreMetricsRecorder { + fn record_a1in_to_am_promotion(&mut self); + fn record_a1out_ghost_hit(&mut self); +} + +/// Metrics for S3-FIFO behavior (small/main/ghost queues). +pub trait S3FifoMetricsRecorder: CoreMetricsRecorder { + fn record_promotion(&mut self); + fn record_main_reinsert(&mut self); + fn record_small_eviction(&mut self); + fn record_main_eviction(&mut self); + fn record_ghost_hit(&mut self); +} + /// Snapshot provider for bench/testing. pub trait MetricsSnapshotProvider { fn snapshot(&self) -> S; diff --git a/src/policy/arc.rs b/src/policy/arc.rs index 8ec2bcb..7eaf3eb 100644 --- a/src/policy/arc.rs +++ b/src/policy/arc.rs @@ -167,6 +167,12 @@ //! - **vs 2Q/SLRU**: More adaptive, no manual tuning needed use crate::ds::GhostList; +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::ArcMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::ArcMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{ArcMetricsRecorder, CoreMetricsRecorder, MetricsSnapshotProvider}; use crate::prelude::ReadOnlyCache; use crate::traits::{CoreCache, MutableCache}; use rustc_hash::FxHashMap; @@ -270,6 +276,9 @@ where /// Maximum total cache capacity capacity: usize, + + #[cfg(feature = "metrics")] + metrics: ArcMetrics, } // SAFETY: ARCCore can be sent between threads if K and V are Send. @@ -326,6 +335,8 @@ where b2: GhostList::new(capacity), p: capacity / 2, capacity, + #[cfg(feature = "metrics")] + metrics: ArcMetrics::default(), } } @@ -399,6 +410,9 @@ where /// /// This is the core ARC replacement algorithm. fn replace(&mut self, in_b2: bool) { + #[cfg(feature = "metrics")] + self.metrics.record_evict_call(); + // Decide whether to evict from T1 or T2 let evict_from_t1 = if self.t1_len > 0 && (self.t1_len > self.p || (self.t1_len == self.p && in_b2)) { @@ -406,12 +420,10 @@ where } else if self.t2_len > 0 { false } else { - // Both conditions failed, default to T1 if it has entries self.t1_len > 0 }; if evict_from_t1 { - // Evict from T1 LRU if let Some(victim_ptr) = self.t1_tail { unsafe { let victim = victim_ptr.as_ref(); @@ -419,29 +431,30 @@ where self.detach(victim_ptr); self.map.remove(&key); - - // Move key to B1 ghost list self.b1.record(key.clone()); - - // Deallocate the node let _ = Box::from_raw(victim_ptr.as_ptr()); + + #[cfg(feature = "metrics")] + { + self.metrics.record_evicted_entry(); + self.metrics.record_t1_eviction(); + } } } - } else { - // Evict from T2 LRU - if let Some(victim_ptr) = self.t2_tail { - unsafe { - let victim = victim_ptr.as_ref(); - let key = victim.key.clone(); - - self.detach(victim_ptr); - self.map.remove(&key); - - // Move key to B2 ghost list - self.b2.record(key.clone()); - - // Deallocate the node - let _ = Box::from_raw(victim_ptr.as_ptr()); + } else if let Some(victim_ptr) = self.t2_tail { + unsafe { + let victim = victim_ptr.as_ref(); + let key = victim.key.clone(); + + self.detach(victim_ptr); + self.map.remove(&key); + self.b2.record(key.clone()); + let _ = Box::from_raw(victim_ptr.as_ptr()); + + #[cfg(feature = "metrics")] + { + self.metrics.record_evicted_entry(); + self.metrics.record_t2_eviction(); } } } @@ -450,7 +463,6 @@ where /// Adapt parameter p based on ghost hit location. fn adapt(&mut self, in_b1: bool, in_b2: bool) { if in_b1 { - // Hit in B1: increase p (favor recency/T1) let delta = if self.b2.len() >= self.b1.len() { 1 } else if !self.b1.is_empty() { @@ -459,8 +471,10 @@ where 1 }; self.p = (self.p + delta).min(self.capacity); + + #[cfg(feature = "metrics")] + self.metrics.record_p_increase(); } else if in_b2 { - // Hit in B2: decrease p (favor frequency/T2) let delta = if self.b1.len() >= self.b2.len() { 1 } else if !self.b2.is_empty() { @@ -469,6 +483,9 @@ where 1 }; self.p = self.p.saturating_sub(delta); + + #[cfg(feature = "metrics")] + self.metrics.record_p_decrease(); } } @@ -708,19 +725,30 @@ where K: Clone + Eq + Hash, { fn get(&mut self, key: &K) -> Option<&V> { - let node_ptr = *self.map.get(key)?; + let node_ptr = match self.map.get(key) { + Some(&ptr) => ptr, + None => { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); + return None; + }, + }; + + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); unsafe { let node = node_ptr.as_ref(); match node.list { ListKind::T1 => { - // Promote from T1 to T2 + #[cfg(feature = "metrics")] + self.metrics.record_t1_to_t2_promotion(); + self.detach(node_ptr); self.attach_t2_head(node_ptr); }, ListKind::T2 => { - // Move to T2 MRU self.detach(node_ptr); self.attach_t2_head(node_ptr); }, @@ -731,27 +759,29 @@ where } fn insert(&mut self, key: K, value: V) -> Option { - // Handle zero capacity edge case + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.capacity == 0 { return None; } // Case 1: Key already in cache (T1 or T2) if let Some(&node_ptr) = self.map.get(&key) { + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); + unsafe { - let mut node_ptr = node_ptr; // Make mutable copy + let mut node_ptr = node_ptr; let node = node_ptr.as_mut(); let old_value = std::mem::replace(&mut node.value, value); - // Update position based on current list match node.list { ListKind::T1 => { - // Promote to T2 self.detach(node_ptr); self.attach_t2_head(node_ptr); }, ListKind::T2 => { - // Move to T2 MRU self.detach(node_ptr); self.attach_t2_head(node_ptr); }, @@ -761,21 +791,24 @@ where } } - // Check for ghost hits let in_b1 = self.b1.contains(&key); let in_b2 = self.b2.contains(&key); // Case 2: Ghost hit in B1 if in_b1 { + #[cfg(feature = "metrics")] + { + self.metrics.record_b1_ghost_hit(); + self.metrics.record_insert_new(); + } + self.adapt(true, false); self.b1.remove(&key); - // Make space if needed if self.t1_len + self.t2_len >= self.capacity { self.replace(false); } - // Insert into T2 (proven reuse) let node = Box::new(Node { prev: None, next: None, @@ -792,15 +825,19 @@ where // Case 3: Ghost hit in B2 if in_b2 { + #[cfg(feature = "metrics")] + { + self.metrics.record_b2_ghost_hit(); + self.metrics.record_insert_new(); + } + self.adapt(false, true); self.b2.remove(&key); - // Make space if needed if self.t1_len + self.t2_len >= self.capacity { self.replace(true); } - // Insert into T2 (proven reuse) let node = Box::new(Node { prev: None, next: None, @@ -815,7 +852,10 @@ where return None; } - // Case 4: Complete miss -- prune directory per ARC paper + // Case 4: Complete miss + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + let l1_len = self.t1_len + self.b1.len(); if l1_len >= self.capacity { if !self.b1.is_empty() { @@ -834,7 +874,6 @@ where } } - // Insert into T1 let node = Box::new(Node { prev: None, next: None, @@ -850,7 +889,9 @@ where } fn clear(&mut self) { - // Deallocate all nodes in T1 + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + let mut current = self.t1_head; while let Some(node_ptr) = current { unsafe { @@ -860,7 +901,6 @@ where } } - // Deallocate all nodes in T2 let mut current = self.t2_head; while let Some(node_ptr) = current { unsafe { @@ -908,6 +948,45 @@ where } } +#[cfg(feature = "metrics")] +impl ARCCore +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> ArcMetricsSnapshot { + ArcMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + t1_to_t2_promotions: self.metrics.t1_to_t2_promotions, + b1_ghost_hits: self.metrics.b1_ghost_hits, + b2_ghost_hits: self.metrics.b2_ghost_hits, + p_increases: self.metrics.p_increases, + p_decreases: self.metrics.p_decreases, + t1_evictions: self.metrics.t1_evictions, + t2_evictions: self.metrics.t2_evictions, + cache_len: self.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for ARCCore +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> ArcMetricsSnapshot { + self.metrics_snapshot() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/policy/car.rs b/src/policy/car.rs index b2a7a14..18be56f 100644 --- a/src/policy/car.rs +++ b/src/policy/car.rs @@ -96,6 +96,12 @@ //! - Wikipedia: Cache replacement policies use crate::ds::GhostList; +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::CarMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::CarMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{CarMetricsRecorder, CoreMetricsRecorder, MetricsSnapshotProvider}; use crate::prelude::ReadOnlyCache; use crate::traits::{CoreCache, MutableCache}; use rustc_hash::FxHashMap; @@ -202,6 +208,9 @@ where /// Maximum total cache capacity. capacity: usize, + + #[cfg(feature = "metrics")] + metrics: CarMetrics, } impl CARCore @@ -249,6 +258,8 @@ where recent_len: 0, frequent_len: 0, capacity, + #[cfg(feature = "metrics")] + metrics: CarMetrics::default(), } } @@ -333,7 +344,12 @@ where /// Sweep budget: `2 * capacity` steps — enough to clear all ref bits /// in the first pass and find a victim in the second. fn replace(&mut self, ghost_frequent_hit: bool) -> bool { + #[cfg(feature = "metrics")] + self.metrics.record_evict_call(); + for _ in 0..(2 * self.capacity + 1) { + #[cfg(feature = "metrics")] + self.metrics.record_hand_sweep(); let evict_from_recent = if self.recent_len > 0 && (self.recent_len > self.target_recent_size || (self.recent_len == self.target_recent_size && ghost_frequent_hit)) @@ -357,6 +373,8 @@ where self.recent_len -= 1; self.link_before_hand(h, Ring::Frequent); self.frequent_len += 1; + #[cfg(feature = "metrics")] + self.metrics.record_recent_to_frequent_promotion(); } else { // Evict from recent ring to ghost_recent. let key = self.slots[h].as_ref().unwrap().key.clone(); @@ -365,6 +383,8 @@ where self.recent_len -= 1; self.free_slot(h); self.ghost_recent.record(key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); return true; } } else { @@ -384,6 +404,8 @@ where self.frequent_len -= 1; self.free_slot(h); self.ghost_frequent.record(key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); return true; } } @@ -406,6 +428,8 @@ where 1 }; self.target_recent_size = (self.target_recent_size + delta).min(self.capacity); + #[cfg(feature = "metrics")] + self.metrics.record_target_increase(); } else if ghost_frequent_hit { let delta = if !self.ghost_frequent.is_empty() { (self.ghost_recent.len() / self.ghost_frequent.len()).max(1) @@ -413,6 +437,8 @@ where 1 }; self.target_recent_size = self.target_recent_size.saturating_sub(delta); + #[cfg(feature = "metrics")] + self.metrics.record_target_decrease(); } } @@ -693,12 +719,26 @@ where K: Clone + Eq + Hash, { fn get(&mut self, key: &K) -> Option<&V> { - let &idx = self.index.get(key)?; + let &idx = match self.index.get(key) { + Some(idx) => { + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); + idx + }, + None => { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); + return None; + }, + }; self.referenced[idx] = true; self.slots[idx].as_ref().map(|s| &s.value) } fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.capacity == 0 { return None; } @@ -708,6 +748,8 @@ where if let Some(slot) = self.slots[idx].as_mut() { let old = std::mem::replace(&mut slot.value, value); self.referenced[idx] = true; + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); return Some(old); } } @@ -717,23 +759,31 @@ where // Case 2: Ghost hit in ghost_recent (key was recently evicted from recent ring). if ghost_recent_hit { + #[cfg(feature = "metrics")] + self.metrics.record_ghost_recent_hit(); self.adapt(true, false); self.ghost_recent.remove(&key); if self.recent_len + self.frequent_len >= self.capacity { self.replace(false); } self.insert_into_ring(key, value, Ring::Frequent); + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); return None; } // Case 3: Ghost hit in ghost_frequent (key was evicted from frequent ring). if ghost_frequent_hit { + #[cfg(feature = "metrics")] + self.metrics.record_ghost_frequent_hit(); self.adapt(false, true); self.ghost_frequent.remove(&key); if self.recent_len + self.frequent_len >= self.capacity { self.replace(true); } self.insert_into_ring(key, value, Ring::Frequent); + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); return None; } @@ -742,10 +792,15 @@ where return None; } self.insert_into_ring(key, value, Ring::Recent); + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); None } fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + self.index.clear(); for slot in &mut self.slots { *slot = None; @@ -784,6 +839,44 @@ where } } +#[cfg(feature = "metrics")] +impl CARCore +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> CarMetricsSnapshot { + CarMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + recent_to_frequent_promotions: self.metrics.recent_to_frequent_promotions, + ghost_recent_hits: self.metrics.ghost_recent_hits, + ghost_frequent_hits: self.metrics.ghost_frequent_hits, + target_increases: self.metrics.target_increases, + target_decreases: self.metrics.target_decreases, + hand_sweeps: self.metrics.hand_sweeps, + cache_len: self.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for CARCore +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> CarMetricsSnapshot { + self.metrics_snapshot() + } +} + // ============================================================================= // Tests // ============================================================================= diff --git a/src/policy/clock.rs b/src/policy/clock.rs index ee43b76..55cfc76 100644 --- a/src/policy/clock.rs +++ b/src/policy/clock.rs @@ -112,6 +112,13 @@ use std::hash::Hash; use crate::ds::ClockRing; use crate::traits::{CoreCache, MutableCache, ReadOnlyCache}; +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::ClockMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::ClockMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::MetricsSnapshotProvider; + /// High-performance Clock cache with O(1) access operations. /// /// Uses the [`ClockRing`] data structure with a sweeping clock hand for eviction. @@ -141,6 +148,8 @@ where K: Clone + Eq + Hash, { ring: ClockRing, + #[cfg(feature = "metrics")] + metrics: ClockMetrics, } impl ClockCache @@ -163,6 +172,8 @@ where pub fn new(capacity: usize) -> Self { Self { ring: ClockRing::new(capacity), + #[cfg(feature = "metrics")] + metrics: ClockMetrics::default(), } } @@ -230,13 +241,48 @@ where /// ``` #[inline] fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + { + self.metrics.insert_calls += 1; + } + if self.ring.capacity() == 0 { return None; } if self.ring.contains(&key) { + #[cfg(feature = "metrics")] + { + self.metrics.insert_updates += 1; + } return self.ring.update(&key, value); } - let _ = self.ring.insert(key, value); + + #[cfg(feature = "metrics")] + { + self.metrics.insert_new += 1; + } + + #[cfg(feature = "metrics")] + let need_evict = self.ring.len() >= self.ring.capacity(); + #[cfg(feature = "metrics")] + let ha_before = self.ring.sweep_hand_advances(); + #[cfg(feature = "metrics")] + let rb_before = self.ring.sweep_ref_bit_resets(); + + let evicted = self.ring.insert(key, value); + + #[cfg(feature = "metrics")] + if need_evict { + self.metrics.evict_calls += 1; + if evicted.is_some() { + self.metrics.evicted_entries += 1; + } + self.metrics.hand_advances += self.ring.sweep_hand_advances() - ha_before; + self.metrics.ref_bit_resets += self.ring.sweep_ref_bit_resets() - rb_before; + } + + #[allow(unused_variables)] + let _ = evicted; None } @@ -258,12 +304,26 @@ where /// ``` #[inline] fn get(&mut self, key: &K) -> Option<&V> { - self.ring.get(key) + let result = self.ring.get(key); + #[cfg(feature = "metrics")] + if result.is_some() { + self.metrics.get_calls += 1; + self.metrics.get_hits += 1; + } else { + self.metrics.get_calls += 1; + self.metrics.get_misses += 1; + } + result } /// Clears all entries from the cache. fn clear(&mut self) { self.ring.clear(); + #[cfg(feature = "metrics")] + { + use crate::metrics::traits::CoreMetricsRecorder; + self.metrics.record_clear(); + } } } @@ -292,6 +352,40 @@ where } } +#[cfg(feature = "metrics")] +impl ClockCache +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> ClockMetricsSnapshot { + ClockMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + hand_advances: self.metrics.hand_advances, + ref_bit_resets: self.metrics.ref_bit_resets, + cache_len: self.ring.len(), + capacity: self.ring.capacity(), + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for ClockCache +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> ClockMetricsSnapshot { + self.metrics_snapshot() + } +} + impl std::fmt::Debug for ClockCache where K: Clone + Eq + Hash + std::fmt::Debug, diff --git a/src/policy/clock_pro.rs b/src/policy/clock_pro.rs index d42068e..a27cbd3 100644 --- a/src/policy/clock_pro.rs +++ b/src/policy/clock_pro.rs @@ -117,6 +117,14 @@ //! let _ = cache.contains(&"page1".to_string()); //! ``` +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::ClockProMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::ClockProMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{ + ClockProMetricsRecorder, CoreMetricsRecorder, MetricsSnapshotProvider, +}; use crate::prelude::ReadOnlyCache; use crate::traits::{CoreCache, MutableCache}; use rustc_hash::FxHashMap; @@ -181,6 +189,8 @@ where ghost_capacity: usize, /// Target ratio of hot pages (adaptive). target_hot_ratio: f64, + #[cfg(feature = "metrics")] + metrics: ClockProMetrics, } // Safety: ClockProCache uses no interior mutability or non-Send types @@ -234,6 +244,8 @@ where capacity, ghost_capacity, target_hot_ratio: 0.5, // Start with 50% hot target + #[cfg(feature = "metrics")] + metrics: ClockProMetrics::default(), } } @@ -321,6 +333,9 @@ where /// Returns the index of the evicted slot. #[inline] fn evict(&mut self) -> usize { + #[cfg(feature = "metrics")] + self.metrics.record_evict_call(); + let max_iterations = self.capacity * 2; let max_hot = ((self.capacity as f64) * self.target_hot_ratio).ceil() as usize; let max_hot = max_hot.max(1).min(self.capacity.saturating_sub(1)); @@ -334,6 +349,8 @@ where entry.status = PageStatus::Hot; entry.referenced = false; self.hot_count += 1; + #[cfg(feature = "metrics")] + self.metrics.record_cold_to_hot_promotion(); } else { // Cold and unreferenced → evict immediately let slot = self.hand_cold; @@ -342,6 +359,11 @@ where self.entries[slot] = None; self.len -= 1; self.add_ghost(key); + #[cfg(feature = "metrics")] + { + self.metrics.record_evicted_entry(); + self.metrics.record_test_insertion(); + } self.hand_cold = (self.hand_cold + 1) % self.capacity; return slot; } @@ -354,6 +376,8 @@ where } else { entry.status = PageStatus::Cold; self.hot_count -= 1; + #[cfg(feature = "metrics")] + self.metrics.record_hot_to_cold_demotion(); } } else if entry.referenced { // Just clear the reference bit @@ -375,6 +399,11 @@ where self.hot_count -= 1; } self.add_ghost(key); + #[cfg(feature = "metrics")] + { + self.metrics.record_evicted_entry(); + self.metrics.record_test_insertion(); + } } self.entries[slot] = None; self.len -= 1; @@ -433,6 +462,9 @@ where /// ``` #[inline] fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.capacity == 0 { return None; } @@ -442,6 +474,8 @@ where let entry = self.entries[slot].as_mut().unwrap(); let old = std::mem::replace(&mut entry.value, value); entry.referenced = true; + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); return Some(old); } @@ -474,7 +508,9 @@ where self.index.insert(key, slot); self.len += 1; - // Hot balancing happens during eviction sweeps, not here + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + None } @@ -497,14 +533,29 @@ where /// ``` #[inline] fn get(&mut self, key: &K) -> Option<&V> { - let slot = *self.index.get(key)?; - let entry = self.entries[slot].as_mut()?; - entry.referenced = true; - Some(&entry.value) + if let Some(&slot) = self.index.get(key) { + let entry = self.entries[slot].as_mut()?; + entry.referenced = true; + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); + Some(&entry.value) + } else { + #[cfg(feature = "metrics")] + { + self.metrics.record_get_miss(); + if self.ghost_index.contains_key(key) { + self.metrics.record_test_hit(); + } + } + None + } } /// Clears all entries from the cache. fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + self.index.clear(); self.ghost_index.clear(); for entry in &mut self.entries { @@ -570,6 +621,42 @@ where } } +#[cfg(feature = "metrics")] +impl ClockProCache +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> ClockProMetricsSnapshot { + ClockProMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + cold_to_hot_promotions: self.metrics.cold_to_hot_promotions, + hot_to_cold_demotions: self.metrics.hot_to_cold_demotions, + test_insertions: self.metrics.test_insertions, + test_hits: self.metrics.test_hits, + cache_len: self.len, + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for ClockProCache +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> ClockProMetricsSnapshot { + self.metrics_snapshot() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/policy/fast_lru.rs b/src/policy/fast_lru.rs index d436196..ad0d927 100644 --- a/src/policy/fast_lru.rs +++ b/src/policy/fast_lru.rs @@ -28,6 +28,15 @@ use std::hash::Hash; use std::mem; use std::ptr::NonNull; +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::LruMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::LruMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{ + CoreMetricsRecorder, LruMetricsReadRecorder, LruMetricsRecorder, MetricsSnapshotProvider, +}; + /// A fast, single-threaded LRU cache. /// /// Values are stored directly without `Arc` wrapping for maximum performance. @@ -55,6 +64,8 @@ pub struct FastLru { head: Option>>, tail: Option>>, capacity: usize, + #[cfg(feature = "metrics")] + metrics: LruMetrics, } /// Node in the LRU linked list. @@ -96,6 +107,8 @@ where head: None, tail: None, capacity, + #[cfg(feature = "metrics")] + metrics: LruMetrics::default(), } } @@ -142,7 +155,17 @@ where /// ``` #[inline(always)] pub fn get(&mut self, key: &K) -> Option<&V> { - let node_ptr = *self.map.get(key)?; + let node_ptr = match self.map.get(key) { + Some(&ptr) => ptr, + None => { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); + return None; + }, + }; + + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); // Move to front (MRU position) self.detach(node_ptr); @@ -192,8 +215,14 @@ where /// ``` #[inline(always)] pub fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + // Check for existing key if let Some(&node_ptr) = self.map.get(&key) { + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); + // Update existing value let old_value = unsafe { let node = node_ptr.as_ptr(); @@ -209,7 +238,13 @@ where // Evict if at capacity if self.capacity > 0 && self.map.len() >= self.capacity { - self.pop_lru(); + #[cfg(feature = "metrics")] + self.metrics.record_evict_call(); + + if self.pop_lru().is_some() { + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); + } } // Don't insert if capacity is 0 @@ -217,6 +252,9 @@ where return None; } + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + // Create new node with optimized field order let node = Box::new(Node { prev: None, @@ -274,6 +312,9 @@ where /// assert_eq!(cache.pop_lru(), Some((2, "two"))); /// ``` pub fn pop_lru(&mut self) -> Option<(K, V)> { + #[cfg(feature = "metrics")] + self.metrics.record_pop_lru_call(); + let tail_ptr = self.tail?; // SAFETY: tail is valid if Some @@ -283,19 +324,34 @@ where self.detach(tail_ptr); let node = unsafe { Box::from_raw(tail_ptr.as_ptr()) }; + + #[cfg(feature = "metrics")] + self.metrics.record_pop_lru_found(); + Some((node.key, node.value)) } /// Peeks at the least recently used item without removing it. pub fn peek_lru(&self) -> Option<(&K, &V)> { - self.tail.map(|node_ptr| unsafe { - let node = node_ptr.as_ptr(); - (&(*node).key, &(*node).value) + #[cfg(feature = "metrics")] + (&self.metrics).record_peek_lru_call(); + + self.tail.map(|node_ptr| { + #[cfg(feature = "metrics")] + (&self.metrics).record_peek_lru_found(); + + unsafe { + let node = node_ptr.as_ptr(); + (&(*node).key, &(*node).value) + } }) } /// Clears all entries from the cache. pub fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + while self.pop_lru().is_some() {} } @@ -304,9 +360,16 @@ where /// Returns `true` if the key existed and was touched. #[inline(always)] pub fn touch(&mut self, key: &K) -> bool { + #[cfg(feature = "metrics")] + self.metrics.record_touch_call(); + if let Some(&node_ptr) = self.map.get(key) { self.detach(node_ptr); self.attach_front(node_ptr); + + #[cfg(feature = "metrics")] + self.metrics.record_touch_found(); + true } else { false @@ -358,6 +421,46 @@ where } } +#[cfg(feature = "metrics")] +impl FastLru +where + K: Eq + Hash + Clone, +{ + pub fn metrics_snapshot(&self) -> LruMetricsSnapshot { + LruMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + pop_lru_calls: self.metrics.pop_lru_calls, + pop_lru_found: self.metrics.pop_lru_found, + peek_lru_calls: self.metrics.peek_lru_calls.get(), + peek_lru_found: self.metrics.peek_lru_found.get(), + touch_calls: self.metrics.touch_calls, + touch_found: self.metrics.touch_found, + recency_rank_calls: self.metrics.recency_rank_calls.get(), + recency_rank_found: self.metrics.recency_rank_found.get(), + recency_rank_scan_steps: self.metrics.recency_rank_scan_steps.get(), + cache_len: self.map.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for FastLru +where + K: Eq + Hash + Clone, +{ + fn snapshot(&self) -> LruMetricsSnapshot { + self.metrics_snapshot() + } +} + impl Drop for FastLru { fn drop(&mut self) { // Free all nodes diff --git a/src/policy/fifo.rs b/src/policy/fifo.rs index fe7067b..e5a7408 100644 --- a/src/policy/fifo.rs +++ b/src/policy/fifo.rs @@ -286,6 +286,15 @@ use crate::traits::{CoreCache, FifoCacheTrait}; #[cfg(feature = "concurrency")] use parking_lot::RwLock; +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::CacheMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::CacheMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{ + CoreMetricsRecorder, FifoMetricsReadRecorder, FifoMetricsRecorder, MetricsSnapshotProvider, +}; + /// FIFO (First In, First Out) Cache. /// /// Evicts the oldest (first inserted) item when capacity is reached. @@ -304,7 +313,9 @@ where K: Eq + Hash + Clone, { store: HashMapStore, - insertion_order: VecDeque, // Tracks the order of insertion + insertion_order: VecDeque, + #[cfg(feature = "metrics")] + metrics: CacheMetrics, } impl FifoCacheInner @@ -315,6 +326,8 @@ where Self { store: HashMapStore::new(capacity), insertion_order: VecDeque::with_capacity(capacity), + #[cfg(feature = "metrics")] + metrics: CacheMetrics::default(), } } } @@ -378,15 +391,20 @@ where /// Evicts the oldest valid entry from the cache. /// Skips over any stale entries (keys that were lazily deleted). fn evict_oldest(&mut self) { - // Keep popping from the front until we find a valid key or the queue is empty while let Some(oldest_key) = self.inner.insertion_order.pop_front() { + #[cfg(feature = "metrics")] + self.inner.metrics.record_evict_scan_step(); + if self.inner.store.contains(&oldest_key) { - // Found a valid key, remove it and stop self.inner.store.remove(&oldest_key); self.inner.store.record_eviction(); + #[cfg(feature = "metrics")] + self.inner.metrics.record_evicted_entry(); break; } - // Skip stale entries (keys that were already removed from the cache) + + #[cfg(feature = "metrics")] + self.inner.metrics.record_stale_skip(); } } } @@ -508,23 +526,33 @@ where V: Debug, { fn insert(&mut self, key: K, value: V) -> Option { - // If capacity is 0, cannot store anything + #[cfg(feature = "metrics")] + self.inner.metrics.record_insert_call(); + if self.inner.store.capacity() == 0 { return None; } if self.inner.store.contains(&key) { + #[cfg(feature = "metrics")] + self.inner.metrics.record_insert_update(); + if let Ok(previous) = self.inner.store.try_insert(key, value) { return previous; } return None; } - // If the cache is at capacity, remove the oldest valid item (FIFO) + + #[cfg(feature = "metrics")] + self.inner.metrics.record_insert_new(); + if self.inner.store.len() >= self.inner.store.capacity() { + #[cfg(feature = "metrics")] + self.inner.metrics.record_evict_call(); + self.evict_oldest(); } - // Add the new key to the insertion order and cache let key_for_queue = key.clone(); if self.inner.store.try_insert(key, value).is_ok() { self.inner.insertion_order.push_back(key_for_queue); @@ -533,11 +561,24 @@ where } fn get(&mut self, key: &K) -> Option<&V> { - // In FIFO, getting an item doesn't change its position - self.inner.store.get(key) + match self.inner.store.get(key) { + Some(value) => { + #[cfg(feature = "metrics")] + self.inner.metrics.record_get_hit(); + Some(value) + }, + None => { + #[cfg(feature = "metrics")] + self.inner.metrics.record_get_miss(); + None + }, + } } fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.inner.metrics.record_clear(); + self.inner.store.clear(); self.inner.insertion_order.clear(); } @@ -549,21 +590,31 @@ where V: Debug, { fn pop_oldest(&mut self) -> Option<(K, V)> { - // Use the existing evict_oldest logic but return the key-value pair + #[cfg(feature = "metrics")] + self.inner.metrics.record_pop_oldest_call(); + while let Some(oldest_key) = self.inner.insertion_order.pop_front() { if let Some(value) = self.inner.store.remove(&oldest_key) { self.inner.store.record_eviction(); + #[cfg(feature = "metrics")] + self.inner.metrics.record_pop_oldest_found(); return Some((oldest_key, value)); } - // Skip stale entries (keys that were already removed from the cache) } + + #[cfg(feature = "metrics")] + self.inner.metrics.record_pop_oldest_empty_or_stale(); None } fn peek_oldest(&self) -> Option<(&K, &V)> { - // Find the first valid entry in the insertion order + #[cfg(feature = "metrics")] + (&self.inner.metrics).record_peek_oldest_call(); + for key in &self.inner.insertion_order { if let Some(value) = self.inner.store.peek(key) { + #[cfg(feature = "metrics")] + (&self.inner.metrics).record_peek_oldest_found(); return Some((key, value)); } } @@ -583,11 +634,18 @@ where } fn age_rank(&self, key: &K) -> Option { - // Find position in insertion order, accounting for stale entries + #[cfg(feature = "metrics")] + (&self.inner.metrics).record_age_rank_call(); + let mut rank = 0; for insertion_key in &self.inner.insertion_order { + #[cfg(feature = "metrics")] + (&self.inner.metrics).record_age_rank_scan_step(); + if self.inner.store.contains(insertion_key) { if insertion_key == key { + #[cfg(feature = "metrics")] + (&self.inner.metrics).record_age_rank_found(); return Some(rank); } rank += 1; @@ -597,6 +655,73 @@ where } } +#[cfg(feature = "metrics")] +impl FifoCache +where + K: Eq + Hash + Clone, + V: Debug, +{ + pub fn metrics_snapshot(&self) -> CacheMetricsSnapshot { + CacheMetricsSnapshot { + get_calls: self.inner.metrics.get_calls, + get_hits: self.inner.metrics.get_hits, + get_misses: self.inner.metrics.get_misses, + insert_calls: self.inner.metrics.insert_calls, + insert_updates: self.inner.metrics.insert_updates, + insert_new: self.inner.metrics.insert_new, + evict_calls: self.inner.metrics.evict_calls, + evicted_entries: self.inner.metrics.evicted_entries, + stale_skips: self.inner.metrics.stale_skips, + evict_scan_steps: self.inner.metrics.evict_scan_steps, + pop_oldest_calls: self.inner.metrics.pop_oldest_calls, + pop_oldest_found: self.inner.metrics.pop_oldest_found, + pop_oldest_empty_or_stale: self.inner.metrics.pop_oldest_empty_or_stale, + peek_oldest_calls: self.inner.metrics.peek_oldest_calls.get(), + peek_oldest_found: self.inner.metrics.peek_oldest_found.get(), + age_rank_calls: self.inner.metrics.age_rank_calls.get(), + age_rank_found: self.inner.metrics.age_rank_found.get(), + age_rank_scan_steps: self.inner.metrics.age_rank_scan_steps.get(), + cache_len: self.inner.store.len(), + insertion_order_len: self.inner.insertion_order.len(), + capacity: self.inner.store.capacity(), + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for FifoCache +where + K: Eq + Hash + Clone, + V: Debug, +{ + fn snapshot(&self) -> CacheMetricsSnapshot { + self.metrics_snapshot() + } +} + +#[cfg(all(feature = "metrics", feature = "concurrency"))] +impl ConcurrentFifoCache +where + K: Eq + Hash + Clone + Debug + Send + Sync, + V: Debug + Send + Sync, +{ + pub fn metrics_snapshot(&self) -> CacheMetricsSnapshot { + let cache = self.inner.read(); + cache.metrics_snapshot() + } +} + +#[cfg(all(feature = "metrics", feature = "concurrency"))] +impl MetricsSnapshotProvider for ConcurrentFifoCache +where + K: Eq + Hash + Clone + Debug + Send + Sync, + V: Debug + Send + Sync, +{ + fn snapshot(&self) -> CacheMetricsSnapshot { + self.metrics_snapshot() + } +} + #[cfg(test)] mod tests { use std::collections::HashSet; diff --git a/src/policy/heap_lfu.rs b/src/policy/heap_lfu.rs index b140b76..cdc524f 100644 --- a/src/policy/heap_lfu.rs +++ b/src/policy/heap_lfu.rs @@ -249,6 +249,15 @@ use std::collections::{BinaryHeap, HashMap}; use std::hash::Hash; use std::sync::Arc; +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::LfuMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::LfuMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{ + CoreMetricsRecorder, LfuMetricsReadRecorder, LfuMetricsRecorder, MetricsSnapshotProvider, +}; + /// Heap-based LFU Cache with O(log n) eviction. /// /// Uses a binary min-heap for efficient LFU item identification. @@ -300,6 +309,8 @@ where // Min-heap: smallest frequency first // Reverse wrapper converts max-heap to min-heap freq_heap: BinaryHeap>, + #[cfg(feature = "metrics")] + metrics: LfuMetrics, } impl HeapLfuCache @@ -326,6 +337,8 @@ where store: HashMapStore::new(capacity), frequencies: HashMap::with_capacity(capacity), freq_heap: BinaryHeap::with_capacity(capacity), + #[cfg(feature = "metrics")] + metrics: LfuMetrics::default(), } } @@ -421,7 +434,17 @@ where /// assert_eq!(cache.frequency(&"missing"), None); /// ``` pub fn frequency(&self, key: &K) -> Option { - self.frequencies.get(key).copied() + #[cfg(feature = "metrics")] + (&self.metrics).record_frequency_call(); + + let result = self.frequencies.get(key).copied(); + + #[cfg(feature = "metrics")] + if result.is_some() { + (&self.metrics).record_frequency_found(); + } + + result } /// Clears all items from the cache. @@ -441,6 +464,9 @@ where /// assert!(cache.is_empty()); /// ``` pub fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + self.store.clear(); self.frequencies.clear(); self.freq_heap.clear(); @@ -571,13 +597,32 @@ where K: Eq + Hash + Clone + Ord, { fn insert(&mut self, key: K, value: Arc) -> Option> { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + // If key already exists, just update the value (don't change frequency) if self.store.contains(&key) { + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); + return self.store.try_insert(key, value).ok().flatten(); } - // Ensure we have capacity (may evict LFU item) - self.ensure_capacity(); + // Evict if at capacity + #[cfg(feature = "metrics")] + if self.store.len() >= self.store.capacity() { + self.metrics.record_evict_call(); + } + + let _evicted = self.ensure_capacity(); + + #[cfg(feature = "metrics")] + if _evicted.is_some() { + self.metrics.record_evicted_entry(); + } + + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); // Insert new item with frequency 1 if self.store.try_insert(key.clone(), value).is_err() { @@ -591,6 +636,9 @@ where fn get(&mut self, key: &K) -> Option<&Arc> { if self.store.contains(key) { + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); + // Increment frequency let new_freq = self.frequencies.get_mut(key).map(|f| { *f += 1; @@ -602,11 +650,17 @@ where self.store.get(key) } else { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); + None } } fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + self.store.clear(); self.frequencies.clear(); self.freq_heap.clear(); @@ -687,6 +741,9 @@ where K: Eq + Hash + Clone + Ord, { fn pop_lfu(&mut self) -> Option<(K, Arc)> { + #[cfg(feature = "metrics")] + self.metrics.record_pop_lfu_call(); + // Find the key with minimum frequency (handling stale entries) let (lfu_key, _freq) = self.pop_lfu_internal()?; @@ -695,26 +752,32 @@ where self.frequencies.remove(&lfu_key); self.store.record_eviction(); + #[cfg(feature = "metrics")] + self.metrics.record_pop_lfu_found(); + Some((lfu_key, value)) } fn peek_lfu(&self) -> Option<(&K, &Arc)> { - // This is more expensive for heap-based approach since we need to - // scan through potential stale entries. For better performance, - // consider avoiding this operation if possible. + #[cfg(feature = "metrics")] + (&self.metrics).record_peek_lfu_call(); - // Find the key with minimum frequency by scanning the frequencies map - // This is O(n) but avoids the borrowing issues with heap cloning if self.frequencies.is_empty() { return None; } let min_freq = *self.frequencies.values().min()?; - // Find a key with the minimum frequency for (key, &freq) in &self.frequencies { if freq == min_freq { - return self.store.peek(key).map(|value| (key, value)); + let result = self.store.peek(key).map(|value| (key, value)); + + #[cfg(feature = "metrics")] + if result.is_some() { + (&self.metrics).record_peek_lfu_found(); + } + + return result; } } @@ -722,7 +785,17 @@ where } fn frequency(&self, key: &K) -> Option { - self.frequencies.get(key).copied() + #[cfg(feature = "metrics")] + (&self.metrics).record_frequency_call(); + + let result = self.frequencies.get(key).copied(); + + #[cfg(feature = "metrics")] + if result.is_some() { + (&self.metrics).record_frequency_found(); + } + + result } fn increment_frequency(&mut self, key: &K) -> Option { @@ -748,6 +821,47 @@ where } } +#[cfg(feature = "metrics")] +impl HeapLfuCache +where + K: Eq + Hash + Clone + Ord, +{ + pub fn metrics_snapshot(&self) -> LfuMetricsSnapshot { + LfuMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + pop_lfu_calls: self.metrics.pop_lfu_calls, + pop_lfu_found: self.metrics.pop_lfu_found, + peek_lfu_calls: self.metrics.peek_lfu_calls.get(), + peek_lfu_found: self.metrics.peek_lfu_found.get(), + frequency_calls: self.metrics.frequency_calls.get(), + frequency_found: self.metrics.frequency_found.get(), + reset_frequency_calls: self.metrics.reset_frequency_calls, + reset_frequency_found: self.metrics.reset_frequency_found, + increment_frequency_calls: self.metrics.increment_frequency_calls, + increment_frequency_found: self.metrics.increment_frequency_found, + cache_len: self.store.len(), + capacity: self.store.capacity(), + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for HeapLfuCache +where + K: Eq + Hash + Clone + Ord, +{ + fn snapshot(&self) -> LfuMetricsSnapshot { + self.metrics_snapshot() + } +} + // ============================================== // HEAP LFU CACHE TESTS // ============================================== diff --git a/src/policy/lifo.rs b/src/policy/lifo.rs index 1050280..f553d3e 100644 --- a/src/policy/lifo.rs +++ b/src/policy/lifo.rs @@ -147,6 +147,12 @@ //! //! - Wikipedia: Cache replacement policies +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::CoreOnlyMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::CoreOnlyMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{CoreMetricsRecorder, MetricsSnapshotProvider}; use crate::prelude::ReadOnlyCache; use crate::traits::CoreCache; use rustc_hash::FxHashMap; @@ -198,6 +204,8 @@ where stack: Vec, /// Maximum cache capacity capacity: usize, + #[cfg(feature = "metrics")] + metrics: CoreOnlyMetrics, } impl LifoCore @@ -225,6 +233,8 @@ where map: FxHashMap::with_capacity_and_hasher(capacity, Default::default()), stack: Vec::with_capacity(capacity), capacity, + #[cfg(feature = "metrics")] + metrics: CoreOnlyMetrics::default(), } } @@ -245,7 +255,13 @@ where /// assert_eq!(cache.get(&"missing"), None); /// ``` #[inline] - pub fn get(&self, key: &K) -> Option<&V> { + pub fn get(&mut self, key: &K) -> Option<&V> { + #[cfg(feature = "metrics")] + if self.map.contains_key(key) { + self.metrics.record_get_hit(); + } else { + self.metrics.record_get_miss(); + } self.map.get(key) } @@ -273,21 +289,25 @@ where /// ``` #[inline] pub fn insert(&mut self, key: K, value: V) { - // Handle zero capacity - reject all insertions + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.capacity == 0 { return; } - // Check for existing key - update in place (no stack change) if let Some(v) = self.map.get_mut(&key) { + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); *v = value; return; } - // Evict from top of stack if at capacity + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + self.evict_if_needed(); - // Push new entry to top of stack self.stack.push(key.clone()); self.map.insert(key, value); } @@ -297,10 +317,16 @@ where /// LIFO evicts from the top (most recently inserted). #[inline] fn evict_if_needed(&mut self) { + #[cfg(feature = "metrics")] + if self.len() >= self.capacity && !self.stack.is_empty() { + self.metrics.record_evict_call(); + } + while self.len() >= self.capacity && !self.stack.is_empty() { - // Pop from top of stack (most recent) if let Some(key) = self.stack.pop() { self.map.remove(&key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); } else { break; } @@ -396,6 +422,9 @@ where /// assert!(!cache.contains(&"a")); /// ``` pub fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + self.map.clear(); self.stack.clear(); @@ -498,12 +527,15 @@ where { #[inline] fn insert(&mut self, key: K, value: V) -> Option { - // Check if key exists - update in place if let Some(v) = self.map.get_mut(&key) { + #[cfg(feature = "metrics")] + { + self.metrics.record_insert_call(); + self.metrics.record_insert_update(); + } return Some(std::mem::replace(v, value)); } - // New insert LifoCore::insert(self, key, value); None } @@ -518,6 +550,38 @@ where } } +#[cfg(feature = "metrics")] +impl LifoCore +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> CoreOnlyMetricsSnapshot { + CoreOnlyMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + cache_len: self.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for LifoCore +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> CoreOnlyMetricsSnapshot { + self.metrics_snapshot() + } +} + #[cfg(test)] mod tests { use super::*; @@ -561,7 +625,7 @@ mod tests { #[test] fn get_missing_key_returns_none() { - let cache: LifoCore<&str, i32> = LifoCore::new(100); + let mut cache: LifoCore<&str, i32> = LifoCore::new(100); assert_eq!(cache.get(&"missing"), None); } @@ -847,7 +911,7 @@ mod tests { #[test] fn empty_cache_operations() { - let cache: LifoCore = LifoCore::new(100); + let mut cache: LifoCore = LifoCore::new(100); assert!(cache.is_empty()); assert_eq!(cache.get(&1), None); diff --git a/src/policy/mfu.rs b/src/policy/mfu.rs index 2ecd874..2ef1462 100644 --- a/src/policy/mfu.rs +++ b/src/policy/mfu.rs @@ -161,6 +161,14 @@ //! **⚠️ Warning**: MFU performs poorly for typical workloads with temporal locality. //! Use LFU, LRU, or S3-FIFO for general-purpose caching. +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::MfuMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::MfuMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{ + CoreMetricsRecorder, MetricsSnapshotProvider, MfuMetricsReadRecorder, MfuMetricsRecorder, +}; use crate::prelude::ReadOnlyCache; use crate::traits::CoreCache; use rustc_hash::FxHashMap; @@ -177,6 +185,8 @@ pub struct MfuCore { frequencies: FxHashMap, freq_heap: BinaryHeap<(u64, K)>, // Max-heap (no Reverse wrapper) capacity: usize, + #[cfg(feature = "metrics")] + metrics: MfuMetrics, } impl MfuCore @@ -190,51 +200,67 @@ where frequencies: FxHashMap::with_capacity_and_hasher(capacity, Default::default()), freq_heap: BinaryHeap::with_capacity(capacity), capacity, + #[cfg(feature = "metrics")] + metrics: MfuMetrics::default(), } } /// Gets a value by key, incrementing its frequency. pub fn get(&mut self, key: &K) -> Option<&V> { if self.map.contains_key(key) { - // Increment frequency + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); + let freq = self.frequencies.entry(key.clone()).or_insert(0); *freq += 1; - // Push new (freq, key) entry to heap (old entries become stale) self.freq_heap.push((*freq, key.clone())); - // Rebuild heap if too many stale entries accumulated if self.freq_heap.len() > self.map.len() * HEAP_REBUILD_FACTOR { self.rebuild_heap(); } self.map.get(key) } else { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); None } } /// Inserts a key-value pair, evicting the most frequently used entry if at capacity. pub fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.capacity == 0 { return None; } - // Update or insert let result = if self.map.contains_key(&key) { - // Update existing entry + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); + let old_value = self.map.insert(key.clone(), value); let freq = self.frequencies.entry(key.clone()).or_insert(0); *freq += 1; self.freq_heap.push((*freq, key)); old_value } else { - // Need to evict if at capacity + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + + #[cfg(feature = "metrics")] + if self.map.len() >= self.capacity { + self.metrics.record_evict_call(); + } + while self.map.len() >= self.capacity { self.evict_mfu(); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); } - // Insert new entry self.map.insert(key.clone(), value); self.frequencies.insert(key.clone(), 1); self.freq_heap.push((1, key)); @@ -309,6 +335,9 @@ where /// Removes all entries from the cache. pub fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + self.map.clear(); self.frequencies.clear(); self.freq_heap.clear(); @@ -319,18 +348,33 @@ where /// Gets the current frequency count for a key. pub fn frequency(&self, key: &K) -> Option { - self.frequencies.get(key).copied() + #[cfg(feature = "metrics")] + (&self.metrics).record_frequency_call(); + + let result = self.frequencies.get(key).copied(); + + #[cfg(feature = "metrics")] + if result.is_some() { + (&self.metrics).record_frequency_found(); + } + + result } /// Removes and returns the entry with the highest frequency. pub fn pop_mfu(&mut self) -> Option<(K, V)> { + #[cfg(feature = "metrics")] + self.metrics.record_pop_mfu_call(); + while let Some((heap_freq, key)) = self.freq_heap.pop() { if let Some(¤t_freq) = self.frequencies.get(&key) { if current_freq == heap_freq { - // Valid entry if let Some(value) = self.map.remove(&key) { self.frequencies.remove(&key); + #[cfg(feature = "metrics")] + self.metrics.record_pop_mfu_found(); + #[cfg(debug_assertions)] self.validate_invariants(); @@ -344,7 +388,9 @@ where /// Peeks at the entry with highest frequency without removing it. pub fn peek_mfu(&self) -> Option<(&K, &V)> { - // Find max frequency + #[cfg(feature = "metrics")] + (&self.metrics).record_peek_mfu_call(); + let mut max_freq = 0u64; let mut max_key: Option<&K> = None; @@ -355,7 +401,14 @@ where } } - max_key.and_then(|k| self.map.get(k).map(|v| (k, v))) + let result = max_key.and_then(|k| self.map.get(k).map(|v| (k, v))); + + #[cfg(feature = "metrics")] + if result.is_some() { + (&self.metrics).record_peek_mfu_found(); + } + + result } /// Validates internal data structure invariants. @@ -430,25 +483,37 @@ where K: Clone + Eq + Hash + Ord, { fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.capacity == 0 { return None; } - // Update or insert if self.map.contains_key(&key) { - // Update existing entry + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); + let old_value = self.map.insert(key.clone(), value); let freq = self.frequencies.entry(key.clone()).or_insert(0); *freq += 1; self.freq_heap.push((*freq, key)); old_value } else { - // Need to evict if at capacity + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + + #[cfg(feature = "metrics")] + if self.map.len() >= self.capacity { + self.metrics.record_evict_call(); + } + while self.map.len() >= self.capacity { self.evict_mfu(); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); } - // Insert new entry self.map.insert(key.clone(), value); self.frequencies.insert(key.clone(), 1); self.freq_heap.push((1, key)); @@ -458,25 +523,30 @@ where fn get(&mut self, key: &K) -> Option<&V> { if self.map.contains_key(key) { - // Increment frequency + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); + let freq = self.frequencies.entry(key.clone()).or_insert(0); *freq += 1; - // Push new (freq, key) entry to heap (old entries become stale) self.freq_heap.push((*freq, key.clone())); - // Rebuild heap if too many stale entries accumulated if self.freq_heap.len() > self.map.len() * HEAP_REBUILD_FACTOR { self.rebuild_heap(); } self.map.get(key) } else { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); None } } fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + self.map.clear(); self.frequencies.clear(); self.freq_heap.clear(); @@ -496,6 +566,44 @@ where } } +#[cfg(feature = "metrics")] +impl MfuCore +where + K: Clone + Eq + Hash + Ord, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> MfuMetricsSnapshot { + MfuMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + pop_mfu_calls: self.metrics.pop_mfu_calls, + pop_mfu_found: self.metrics.pop_mfu_found, + peek_mfu_calls: self.metrics.peek_mfu_calls.get(), + peek_mfu_found: self.metrics.peek_mfu_found.get(), + frequency_calls: self.metrics.frequency_calls.get(), + frequency_found: self.metrics.frequency_found.get(), + cache_len: self.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for MfuCore +where + K: Clone + Eq + Hash + Ord, +{ + fn snapshot(&self) -> MfuMetricsSnapshot { + self.metrics_snapshot() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/policy/mru.rs b/src/policy/mru.rs index 704f263..634104d 100644 --- a/src/policy/mru.rs +++ b/src/policy/mru.rs @@ -148,6 +148,12 @@ //! //! - Wikipedia: Cache replacement policies +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::CoreOnlyMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::CoreOnlyMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{CoreMetricsRecorder, MetricsSnapshotProvider}; use crate::prelude::ReadOnlyCache; use crate::traits::CoreCache; use rustc_hash::FxHashMap; @@ -217,6 +223,9 @@ where /// Maximum cache capacity capacity: usize, + + #[cfg(feature = "metrics")] + metrics: CoreOnlyMetrics, } // SAFETY: MruCore can be sent between threads if K and V are Send. @@ -261,6 +270,8 @@ where head: None, tail: None, capacity, + #[cfg(feature = "metrics")] + metrics: CoreOnlyMetrics::default(), } } @@ -338,9 +349,19 @@ where /// ``` #[inline] pub fn get(&mut self, key: &K) -> Option<&V> { - let node_ptr = *self.map.get(key)?; + let node_ptr = match self.map.get(key) { + Some(&ptr) => { + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); + ptr + }, + None => { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); + return None; + }, + }; - // Move to head (MRU position) self.detach(node_ptr); self.attach_head(node_ptr); @@ -371,20 +392,25 @@ where /// ``` #[inline] pub fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.capacity == 0 { return None; } - // Check for existing key - update in place if let Some(&node_ptr) = self.map.get(&key) { + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); let old = unsafe { std::mem::replace(&mut (*node_ptr.as_ptr()).value, value) }; return Some(old); } - // Evict BEFORE inserting to ensure space is available + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + self.evict_if_needed(); - // Create new node let node = Box::new(Node { prev: None, next: None, @@ -406,10 +432,16 @@ where /// MRU evicts from the head (MRU position) - the most recently used item! #[inline] fn evict_if_needed(&mut self) { + #[cfg(feature = "metrics")] + if self.len() >= self.capacity && self.head.is_some() { + self.metrics.record_evict_call(); + } + while self.len() >= self.capacity { - // Evict from head (MRU - most recently used!) if let Some(node) = self.pop_head() { self.map.remove(&node.key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); } else { break; } @@ -504,7 +536,9 @@ where /// assert!(!cache.contains(&"a")); /// ``` pub fn clear(&mut self) { - // Free all nodes + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + while self.pop_head().is_some() {} self.map.clear(); @@ -632,6 +666,38 @@ where } } +#[cfg(feature = "metrics")] +impl MruCore +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> CoreOnlyMetricsSnapshot { + CoreOnlyMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + cache_len: self.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for MruCore +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> CoreOnlyMetricsSnapshot { + self.metrics_snapshot() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/policy/nru.rs b/src/policy/nru.rs index dfa0c86..01d03fc 100644 --- a/src/policy/nru.rs +++ b/src/policy/nru.rs @@ -166,6 +166,13 @@ use rustc_hash::FxHashMap; use std::fmt::{Debug, Formatter}; use std::hash::Hash; +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::NruMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::NruMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::MetricsSnapshotProvider; + /// Entry in the NRU cache containing value, index, and reference bit. #[derive(Debug, Clone)] struct Entry { @@ -228,6 +235,8 @@ where keys: Vec, /// Maximum capacity capacity: usize, + #[cfg(feature = "metrics")] + metrics: NruMetrics, } impl NruCache @@ -252,6 +261,8 @@ where map: FxHashMap::default(), keys: Vec::with_capacity(capacity), capacity, + #[cfg(feature = "metrics")] + metrics: NruMetrics::default(), } } @@ -274,12 +285,14 @@ where // Phase 1: Try to find an unreferenced entry for (idx, key) in self.keys.iter().enumerate() { + #[cfg(feature = "metrics")] + { + self.metrics.sweep_steps += 1; + } if let Some(entry) = self.map.get(key) { if !entry.referenced { - // Found unreferenced entry - evict it let victim_key = self.keys.swap_remove(idx); - // Update index of swapped key if we didn't remove the last element if idx < self.keys.len() { let swapped_key = &self.keys[idx]; if let Some(swapped_entry) = self.map.get_mut(swapped_key) { @@ -296,15 +309,19 @@ where // Phase 2: All entries are referenced - clear all bits and evict first for key in &self.keys { if let Some(entry) = self.map.get_mut(key) { - entry.referenced = false; + if entry.referenced { + entry.referenced = false; + #[cfg(feature = "metrics")] + { + self.metrics.ref_bit_resets += 1; + } + } } } - // Now evict the first entry (index 0) if !self.keys.is_empty() { let victim_key = self.keys.swap_remove(0); - // Update index of swapped key if we didn't remove the last element if !self.keys.is_empty() { let swapped_key = &self.keys[0]; if let Some(swapped_entry) = self.map.get_mut(swapped_key) { @@ -370,24 +387,42 @@ where /// ``` #[inline] fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + { + self.metrics.insert_calls += 1; + } + if self.capacity == 0 { return None; } - // Check if key already exists if let Some(entry) = self.map.get_mut(&key) { - // Update existing entry + #[cfg(feature = "metrics")] + { + self.metrics.insert_updates += 1; + } let old_value = std::mem::replace(&mut entry.value, value); entry.referenced = true; return Some(old_value); } - // New key - check capacity + #[cfg(feature = "metrics")] + { + self.metrics.insert_new += 1; + } + if self.map.len() >= self.capacity { - // Evict using NRU policy - let _ = self.evict_one(); + #[cfg(feature = "metrics")] + { + self.metrics.evict_calls += 1; + } + if self.evict_one().is_some() { + #[cfg(feature = "metrics")] + { + self.metrics.evicted_entries += 1; + } + } } - // Insert new entry let index = self.keys.len(); self.keys.push(key.clone()); self.map.insert( @@ -395,7 +430,7 @@ where Entry { index, value, - referenced: false, // New inserts start unreferenced (cold start) + referenced: false, }, ); @@ -422,8 +457,18 @@ where fn get(&mut self, key: &K) -> Option<&V> { if let Some(entry) = self.map.get_mut(key) { entry.referenced = true; + #[cfg(feature = "metrics")] + { + self.metrics.get_calls += 1; + self.metrics.get_hits += 1; + } Some(&entry.value) } else { + #[cfg(feature = "metrics")] + { + self.metrics.get_calls += 1; + self.metrics.get_misses += 1; + } None } } @@ -432,6 +477,11 @@ where fn clear(&mut self) { self.map.clear(); self.keys.clear(); + #[cfg(feature = "metrics")] + { + use crate::metrics::traits::CoreMetricsRecorder; + self.metrics.record_clear(); + } } } @@ -474,6 +524,40 @@ where } } +#[cfg(feature = "metrics")] +impl NruCache +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> NruMetricsSnapshot { + NruMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + sweep_steps: self.metrics.sweep_steps, + ref_bit_resets: self.metrics.ref_bit_resets, + cache_len: self.map.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for NruCache +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> NruMetricsSnapshot { + self.metrics_snapshot() + } +} + impl Debug for NruCache where K: Clone + Eq + Hash + std::fmt::Debug, diff --git a/src/policy/random.rs b/src/policy/random.rs index 95e737c..9804cd6 100644 --- a/src/policy/random.rs +++ b/src/policy/random.rs @@ -154,6 +154,12 @@ //! //! - Wikipedia: Cache replacement policies +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::CoreOnlyMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::CoreOnlyMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{CoreMetricsRecorder, MetricsSnapshotProvider}; use crate::prelude::ReadOnlyCache; use crate::traits::CoreCache; use rustc_hash::FxHashMap; @@ -209,6 +215,8 @@ where capacity: usize, /// Internal PRNG state for random eviction (XorShift) rng_state: u64, + #[cfg(feature = "metrics")] + metrics: CoreOnlyMetrics, } impl RandomCore @@ -236,8 +244,9 @@ where map: FxHashMap::with_capacity_and_hasher(capacity, Default::default()), keys: Vec::with_capacity(capacity), capacity, - // Initialize with non-zero seed for XorShift (capacity + 1 ensures non-zero) rng_state: capacity as u64 + 0x9e3779b97f4a7c15, + #[cfg(feature = "metrics")] + metrics: CoreOnlyMetrics::default(), } } @@ -258,7 +267,13 @@ where /// assert_eq!(cache.get(&"missing"), None); /// ``` #[inline] - pub fn get(&self, key: &K) -> Option<&V> { + pub fn get(&mut self, key: &K) -> Option<&V> { + #[cfg(feature = "metrics")] + if self.map.contains_key(key) { + self.metrics.record_get_hit(); + } else { + self.metrics.record_get_miss(); + } self.map.get(key).map(|(_, v)| v) } @@ -286,21 +301,25 @@ where /// ``` #[inline] pub fn insert(&mut self, key: K, value: V) { - // Handle zero capacity - reject all insertions + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.capacity == 0 { return; } - // Check for existing key - update in place if let Some((_, v)) = self.map.get_mut(&key) { + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); *v = value; return; } - // Evict random entry if at capacity + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + self.evict_if_needed(); - // Insert new entry let idx = self.keys.len(); self.keys.push(key.clone()); self.map.insert(key, (idx, value)); @@ -311,8 +330,15 @@ where /// Uses simple random for O(1) selection with swap-remove technique. #[inline] fn evict_if_needed(&mut self) { + #[cfg(feature = "metrics")] + if self.len() >= self.capacity && self.capacity > 0 && !self.keys.is_empty() { + self.metrics.record_evict_call(); + } + while self.len() >= self.capacity && self.capacity > 0 && !self.keys.is_empty() { self.evict_random(); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); } #[cfg(debug_assertions)] @@ -450,6 +476,9 @@ where /// assert!(!cache.contains(&"a")); /// ``` pub fn clear(&mut self) { + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + self.map.clear(); self.keys.clear(); @@ -558,12 +587,15 @@ where { #[inline] fn insert(&mut self, key: K, value: V) -> Option { - // Check if key exists - update in place if let Some((_, v)) = self.map.get_mut(&key) { + #[cfg(feature = "metrics")] + { + self.metrics.record_insert_call(); + self.metrics.record_insert_update(); + } return Some(std::mem::replace(v, value)); } - // New insert RandomCore::insert(self, key, value); None } @@ -578,6 +610,38 @@ where } } +#[cfg(feature = "metrics")] +impl RandomCore +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> CoreOnlyMetricsSnapshot { + CoreOnlyMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + cache_len: self.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for RandomCore +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> CoreOnlyMetricsSnapshot { + self.metrics_snapshot() + } +} + #[cfg(test)] mod tests { use super::*; @@ -621,7 +685,7 @@ mod tests { #[test] fn get_missing_key_returns_none() { - let cache: RandomCore<&str, i32> = RandomCore::new(100); + let mut cache: RandomCore<&str, i32> = RandomCore::new(100); assert_eq!(cache.get(&"missing"), None); } @@ -837,7 +901,7 @@ mod tests { #[test] fn empty_cache_operations() { - let cache: RandomCore = RandomCore::new(100); + let mut cache: RandomCore = RandomCore::new(100); assert!(cache.is_empty()); assert_eq!(cache.get(&1), None); diff --git a/src/policy/s3_fifo.rs b/src/policy/s3_fifo.rs index e91cac4..7b76286 100644 --- a/src/policy/s3_fifo.rs +++ b/src/policy/s3_fifo.rs @@ -80,58 +80,14 @@ use crate::ds::{GhostList, SlotArena, SlotId}; use crate::error::ConfigError; #[cfg(feature = "concurrency")] use crate::traits::ConcurrentCache; -/// Performance metrics for S3-FIFO cache operations. -#[cfg(feature = "metrics")] -#[derive(Debug, Clone, Default)] -#[non_exhaustive] -pub struct S3FifoMetrics { - /// Number of cache hits. - pub hits: u64, - /// Number of cache misses. - pub misses: u64, - /// Number of insertions. - pub inserts: u64, - /// Number of updates (key already existed). - pub updates: u64, - /// Number of promotions from Small to Main. - pub promotions: u64, - /// Number of Main reinsertions (freq > 0). - pub main_reinserts: u64, - /// Number of evictions from Small. - pub small_evictions: u64, - /// Number of evictions from Main. - pub main_evictions: u64, - /// Number of ghost hits (ghost-guided admission). - pub ghost_hits: u64, -} #[cfg(feature = "metrics")] -impl std::fmt::Display for S3FifoMetrics { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let total_accesses = self.hits + self.misses; - let hit_rate = if total_accesses > 0 { - (self.hits as f64 / total_accesses as f64) * 100.0 - } else { - 0.0 - }; +use crate::metrics::metrics_impl::S3FifoMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::S3FifoMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{CoreMetricsRecorder, MetricsSnapshotProvider, S3FifoMetricsRecorder}; - write!( - f, - "S3FifoMetrics {{ hits: {}, misses: {}, hit_rate: {:.2}%, inserts: {}, updates: {}, \ - promotions: {}, main_reinserts: {}, small_evictions: {}, main_evictions: {}, ghost_hits: {} }}", - self.hits, - self.misses, - hit_rate, - self.inserts, - self.updates, - self.promotions, - self.main_reinserts, - self.small_evictions, - self.main_evictions, - self.ghost_hits - ) - } -} use crate::traits::CoreCache; use crate::traits::MutableCache; use crate::traits::ReadOnlyCache; @@ -572,7 +528,7 @@ where None => { #[cfg(feature = "metrics")] { - self.metrics.misses += 1; + self.metrics.record_get_miss(); } return None; }, @@ -580,7 +536,7 @@ where #[cfg(feature = "metrics")] { - self.metrics.hits += 1; + self.metrics.record_get_hit(); } let node = self.arena.get(id).expect("map/arena out of sync"); @@ -599,7 +555,7 @@ where None => { #[cfg(feature = "metrics")] { - self.metrics.misses += 1; + self.metrics.record_get_miss(); } return None; }, @@ -607,7 +563,7 @@ where #[cfg(feature = "metrics")] { - self.metrics.hits += 1; + self.metrics.record_get_hit(); } let node = self.arena.get_mut(id).expect("map/arena out of sync"); @@ -649,7 +605,8 @@ where if let Some(&id) = self.map.get(&key) { #[cfg(feature = "metrics")] { - self.metrics.updates += 1; + self.metrics.record_insert_call(); + self.metrics.record_insert_update(); } let node = self.arena.get_mut(id).expect("map/arena out of sync"); let old = std::mem::replace(&mut node.value, value); @@ -662,7 +619,8 @@ where #[cfg(feature = "metrics")] { - self.metrics.inserts += 1; + self.metrics.record_insert_call(); + self.metrics.record_insert_new(); } // Ghost-guided admission @@ -670,7 +628,7 @@ where #[cfg(feature = "metrics")] if insert_to_main { - self.metrics.ghost_hits += 1; + self.metrics.record_ghost_hit(); } // Evict before inserting @@ -886,7 +844,7 @@ where QueueKind::Small => { #[cfg(feature = "metrics")] { - self.metrics.promotions += 1; + self.metrics.record_promotion(); } *self.arena.get_mut(id).unwrap().freq.get_mut() = 0; self.attach_main_head(id); @@ -894,7 +852,7 @@ where QueueKind::Main => { #[cfg(feature = "metrics")] { - self.metrics.main_reinserts += 1; + self.metrics.record_main_reinsert(); } *self.arena.get_mut(id).unwrap().freq.get_mut() = freq - 1; self.attach_main_head(id); @@ -906,7 +864,8 @@ where QueueKind::Small => { #[cfg(feature = "metrics")] { - self.metrics.small_evictions += 1; + self.metrics.record_small_eviction(); + self.metrics.record_evicted_entry(); } let node = self.arena.remove(id).unwrap(); self.map.remove(&node.key); @@ -915,7 +874,8 @@ where QueueKind::Main => { #[cfg(feature = "metrics")] { - self.metrics.main_evictions += 1; + self.metrics.record_main_eviction(); + self.metrics.record_evicted_entry(); } let node = self.arena.remove(id).unwrap(); self.map.remove(&node.key); @@ -929,6 +889,11 @@ where /// Evicts entries until there is room for a new entry. fn evict_if_needed(&mut self) { + #[cfg(feature = "metrics")] + if self.len() >= self.capacity { + self.metrics.record_evict_call(); + } + while self.len() >= self.capacity { let acted = if self.small_len > self.small_cap { self.try_evict_from_queue(QueueKind::Small) @@ -1038,6 +1003,47 @@ where } } +// --------------------------------------------------------------------------- +// Metrics snapshot +// --------------------------------------------------------------------------- + +#[cfg(feature = "metrics")] +impl S3FifoCache +where + K: Clone + Eq + Hash, +{ + /// Captures a point-in-time snapshot of all metrics counters plus gauges. + pub fn metrics_snapshot(&self) -> S3FifoMetricsSnapshot { + S3FifoMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + promotions: self.metrics.promotions, + main_reinserts: self.metrics.main_reinserts, + small_evictions: self.metrics.small_evictions, + main_evictions: self.metrics.main_evictions, + ghost_hits: self.metrics.ghost_hits, + cache_len: self.map.len(), + capacity: self.capacity, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for S3FifoCache +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> S3FifoMetricsSnapshot { + self.metrics_snapshot() + } +} + // --------------------------------------------------------------------------- // Std trait implementations // --------------------------------------------------------------------------- @@ -1523,8 +1529,11 @@ where #[cfg(feature = "metrics")] pub fn metrics(&self) -> S3FifoMetrics { let mut m = self.inner.read().metrics().clone(); - m.hits += self.read_hits.load(Ordering::Relaxed); - m.misses += self.read_misses.load(Ordering::Relaxed); + let rh = self.read_hits.load(Ordering::Relaxed); + let rm = self.read_misses.load(Ordering::Relaxed); + m.get_hits += rh; + m.get_misses += rm; + m.get_calls += rh + rm; m } @@ -1545,6 +1554,34 @@ where { } +#[cfg(all(feature = "metrics", feature = "concurrency"))] +impl ConcurrentS3FifoCache +where + K: Clone + Eq + Hash, +{ + /// Captures a point-in-time snapshot including concurrent read-path counters. + pub fn metrics_snapshot(&self) -> S3FifoMetricsSnapshot { + let inner = self.inner.read(); + let mut snap = inner.metrics_snapshot(); + let rh = self.read_hits.load(Ordering::Relaxed); + let rm = self.read_misses.load(Ordering::Relaxed); + snap.get_hits += rh; + snap.get_misses += rm; + snap.get_calls += rh + rm; + snap + } +} + +#[cfg(all(feature = "metrics", feature = "concurrency"))] +impl MetricsSnapshotProvider for ConcurrentS3FifoCache +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> S3FifoMetricsSnapshot { + self.metrics_snapshot() + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/src/policy/slru.rs b/src/policy/slru.rs index 18cee25..940b644 100644 --- a/src/policy/slru.rs +++ b/src/policy/slru.rs @@ -136,6 +136,12 @@ //! - Karedla et al., "Caching Strategies to Improve Disk System Performance", 1994 //! - Wikipedia: Cache replacement policies +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::SlruMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::SlruMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{CoreMetricsRecorder, MetricsSnapshotProvider, SlruMetricsRecorder}; use crate::prelude::ReadOnlyCache; use crate::traits::CoreCache; use rustc_hash::FxHashMap; @@ -228,6 +234,9 @@ where probationary_cap: usize, /// Maximum total cache capacity. protected_cap: usize, + + #[cfg(feature = "metrics")] + metrics: SlruMetrics, } // SAFETY: SlruCore can be sent between threads if K and V are Send. @@ -284,6 +293,8 @@ where protected_len: 0, probationary_cap, protected_cap, + #[cfg(feature = "metrics")] + metrics: SlruMetrics::default(), } } @@ -420,18 +431,29 @@ where /// ``` #[inline] pub fn get(&mut self, key: &K) -> Option<&V> { - let node_ptr = *self.map.get(key)?; + let node_ptr = match self.map.get(key) { + Some(&ptr) => ptr, + None => { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); + return None; + }, + }; + + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); let segment = unsafe { node_ptr.as_ref().segment }; match segment { Segment::Probationary => { - // Promote from probationary to protected self.detach(node_ptr); self.attach_protected_head(node_ptr); + + #[cfg(feature = "metrics")] + self.metrics.record_probationary_to_protected(); }, Segment::Protected => { - // Move to MRU position self.detach(node_ptr); self.attach_protected_head(node_ptr); }, @@ -464,20 +486,26 @@ where /// ``` #[inline] pub fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.protected_cap == 0 { return None; } - // Check for existing key - update in place if let Some(&node_ptr) = self.map.get(&key) { + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); + let old = unsafe { std::mem::replace(&mut (*node_ptr.as_ptr()).value, value) }; return Some(old); } - // Evict BEFORE inserting to ensure space is available + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + self.evict_if_needed(); - // Create new node in probationary let node = Box::new(Node { prev: None, next: None, @@ -499,24 +527,32 @@ where #[inline] fn evict_if_needed(&mut self) { while self.len() >= self.protected_cap { + #[cfg(feature = "metrics")] + self.metrics.record_evict_call(); + if self.probationary_len > self.probationary_cap { - // Evict from probationary tail (LRU) if let Some(node) = self.pop_probationary_tail() { self.map.remove(&node.key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); continue; } } - // Evict from protected tail (LRU) if let Some(node) = self.pop_protected_tail() { self.map.remove(&node.key); + #[cfg(feature = "metrics")] + { + self.metrics.record_evicted_entry(); + self.metrics.record_protected_eviction(); + } continue; } - // Fallback: evict from probationary even if under cap if let Some(node) = self.pop_probationary_tail() { self.map.remove(&node.key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); continue; } - // No entries to evict break; } } @@ -609,7 +645,9 @@ where /// assert!(!cache.contains(&"a")); /// ``` pub fn clear(&mut self) { - // Free all nodes + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + while self.pop_probationary_tail().is_some() {} while self.pop_protected_tail().is_some() {} self.map.clear(); @@ -707,6 +745,39 @@ where } } +#[cfg(feature = "metrics")] +impl SlruCore +where + K: Clone + Eq + Hash, +{ + pub fn metrics_snapshot(&self) -> SlruMetricsSnapshot { + SlruMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + probationary_to_protected: self.metrics.probationary_to_protected, + protected_evictions: self.metrics.protected_evictions, + cache_len: self.len(), + capacity: self.capacity(), + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for SlruCore +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> SlruMetricsSnapshot { + self.metrics_snapshot() + } +} + // Proper cleanup when cache is dropped impl Drop for SlruCore where diff --git a/src/policy/two_q.rs b/src/policy/two_q.rs index a7fefaa..7db6f90 100644 --- a/src/policy/two_q.rs +++ b/src/policy/two_q.rs @@ -139,6 +139,12 @@ //! Replacement Algorithm", VLDB 1994 use crate::ds::{IntrusiveList, SlotId}; +#[cfg(feature = "metrics")] +use crate::metrics::metrics_impl::TwoQMetrics; +#[cfg(feature = "metrics")] +use crate::metrics::snapshot::TwoQMetricsSnapshot; +#[cfg(feature = "metrics")] +use crate::metrics::traits::{CoreMetricsRecorder, MetricsSnapshotProvider, TwoQMetricsRecorder}; use crate::prelude::ReadOnlyCache; use crate::traits::CoreCache; use rustc_hash::FxHashMap; @@ -299,6 +305,9 @@ where probation_cap: usize, /// Maximum total cache capacity. protected_cap: usize, + + #[cfg(feature = "metrics")] + metrics: TwoQMetrics, } // SAFETY: TwoQCore can be sent between threads if K and V are Send. @@ -533,6 +542,8 @@ where protected_len: 0, probation_cap, protected_cap, + #[cfg(feature = "metrics")] + metrics: TwoQMetrics::default(), } } @@ -669,18 +680,29 @@ where /// ``` #[inline] pub fn get(&mut self, key: &K) -> Option<&V> { - let node_ptr = *self.map.get(key)?; + let node_ptr = match self.map.get(key) { + Some(&ptr) => ptr, + None => { + #[cfg(feature = "metrics")] + self.metrics.record_get_miss(); + return None; + }, + }; + + #[cfg(feature = "metrics")] + self.metrics.record_get_hit(); let queue = unsafe { node_ptr.as_ref().queue }; match queue { QueueKind::Probation => { - // Promote from probation to protected + #[cfg(feature = "metrics")] + self.metrics.record_a1in_to_am_promotion(); + self.detach(node_ptr); self.attach_protected_head(node_ptr); }, QueueKind::Protected => { - // Move to MRU position self.detach(node_ptr); self.attach_protected_head(node_ptr); }, @@ -713,20 +735,26 @@ where /// ``` #[inline] pub fn insert(&mut self, key: K, value: V) -> Option { + #[cfg(feature = "metrics")] + self.metrics.record_insert_call(); + if self.protected_cap == 0 { return None; } - // Check for existing key - update in place if let Some(&node_ptr) = self.map.get(&key) { + #[cfg(feature = "metrics")] + self.metrics.record_insert_update(); + let old = unsafe { std::mem::replace(&mut (*node_ptr.as_ptr()).value, value) }; return Some(old); } - // Evict BEFORE inserting to ensure space is available + #[cfg(feature = "metrics")] + self.metrics.record_insert_new(); + self.evict_if_needed(); - // Create new node in probation let node = Box::new(Node { prev: None, next: None, @@ -744,25 +772,32 @@ where /// Evicts entries until there is room for a new entry. #[inline] fn evict_if_needed(&mut self) { + if self.len() >= self.protected_cap { + #[cfg(feature = "metrics")] + self.metrics.record_evict_call(); + } + while self.len() >= self.protected_cap { if self.probation_len > self.probation_cap { - // Evict from probation tail (oldest) if let Some(node) = self.pop_probation_tail() { self.map.remove(&node.key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); continue; } } - // Evict from protected tail (LRU) if let Some(node) = self.pop_protected_tail() { self.map.remove(&node.key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); continue; } - // Fallback: evict from probation even if under cap if let Some(node) = self.pop_probation_tail() { self.map.remove(&node.key); + #[cfg(feature = "metrics")] + self.metrics.record_evicted_entry(); continue; } - // No entries to evict break; } } @@ -855,7 +890,9 @@ where /// assert!(!cache.contains(&"a")); /// ``` pub fn clear(&mut self) { - // Free all nodes + #[cfg(feature = "metrics")] + self.metrics.record_clear(); + while self.pop_probation_tail().is_some() {} while self.pop_protected_tail().is_some() {} self.map.clear(); @@ -945,6 +982,40 @@ where } } +#[cfg(feature = "metrics")] +impl TwoQCore +where + K: Clone + Eq + Hash, +{ + /// Returns a snapshot of cache metrics. + pub fn metrics_snapshot(&self) -> TwoQMetricsSnapshot { + TwoQMetricsSnapshot { + get_calls: self.metrics.get_calls, + get_hits: self.metrics.get_hits, + get_misses: self.metrics.get_misses, + insert_calls: self.metrics.insert_calls, + insert_updates: self.metrics.insert_updates, + insert_new: self.metrics.insert_new, + evict_calls: self.metrics.evict_calls, + evicted_entries: self.metrics.evicted_entries, + a1in_to_am_promotions: self.metrics.a1in_to_am_promotions, + a1out_ghost_hits: self.metrics.a1out_ghost_hits, + cache_len: self.len(), + capacity: self.protected_cap, + } + } +} + +#[cfg(feature = "metrics")] +impl MetricsSnapshotProvider for TwoQCore +where + K: Clone + Eq + Hash, +{ + fn snapshot(&self) -> TwoQMetricsSnapshot { + self.metrics_snapshot() + } +} + #[cfg(test)] mod tests { use super::*;