use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Default)]
pub struct RecallMetrics {
graph_path_hits: AtomicU64,
vector_fallback_hits: AtomicU64,
projector_lag_episodes: AtomicU64,
graph_candidates_total: AtomicU64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
#[allow(missing_docs)] pub struct RecallSnapshot {
pub graph_path_hits: u64,
pub vector_fallback_hits: u64,
pub projector_lag_episodes: u64,
pub graph_candidates_total: u64,
}
impl RecallMetrics {
pub fn record_graph_hit(&self) {
self.graph_path_hits.fetch_add(1, Ordering::Relaxed);
}
pub fn record_graph_hit_with_candidates(&self, candidates: u64) {
self.graph_path_hits.fetch_add(1, Ordering::Relaxed);
self.graph_candidates_total
.fetch_add(candidates, Ordering::Relaxed);
}
pub fn record_vector_fallback(&self) {
self.vector_fallback_hits.fetch_add(1, Ordering::Relaxed);
}
pub fn record_projector_lag(&self, n: u64) {
self.projector_lag_episodes.fetch_add(n, Ordering::Relaxed);
}
pub fn snapshot(&self) -> RecallSnapshot {
RecallSnapshot {
graph_path_hits: self.graph_path_hits.load(Ordering::Relaxed),
vector_fallback_hits: self.vector_fallback_hits.load(Ordering::Relaxed),
projector_lag_episodes: self.projector_lag_episodes.load(Ordering::Relaxed),
graph_candidates_total: self.graph_candidates_total.load(Ordering::Relaxed),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_graph_hit_with_candidates_bumps_both_counters() {
let m = RecallMetrics::default();
m.record_graph_hit_with_candidates(5);
m.record_graph_hit_with_candidates(3);
let snap = m.snapshot();
assert_eq!(snap.graph_path_hits, 2);
assert_eq!(snap.graph_candidates_total, 8);
}
#[test]
fn record_graph_hit_does_not_advance_candidate_total() {
let m = RecallMetrics::default();
m.record_graph_hit();
let snap = m.snapshot();
assert_eq!(snap.graph_path_hits, 1);
assert_eq!(
snap.graph_candidates_total, 0,
"record_graph_hit must not touch graph_candidates_total"
);
}
}