Skip to main content

superbit/
metrics.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Instant;
3
4/// Collects runtime statistics about index operations using lock-free atomic counters.
5#[derive(Debug, Default)]
6pub struct MetricsCollector {
7    query_count: AtomicU64,
8    insert_count: AtomicU64,
9    total_candidates_examined: AtomicU64,
10    total_query_time_ns: AtomicU64,
11    bucket_hits: AtomicU64,
12    bucket_misses: AtomicU64,
13}
14
15impl MetricsCollector {
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    pub fn record_query(&self, candidates: u64, duration_ns: u64) {
21        self.query_count.fetch_add(1, Ordering::Relaxed);
22        self.total_candidates_examined
23            .fetch_add(candidates, Ordering::Relaxed);
24        self.total_query_time_ns
25            .fetch_add(duration_ns, Ordering::Relaxed);
26    }
27
28    pub fn record_insert(&self) {
29        self.insert_count.fetch_add(1, Ordering::Relaxed);
30    }
31
32    pub fn record_bucket_hit(&self) {
33        self.bucket_hits.fetch_add(1, Ordering::Relaxed);
34    }
35
36    pub fn record_bucket_miss(&self) {
37        self.bucket_misses.fetch_add(1, Ordering::Relaxed);
38    }
39
40    /// Take a point-in-time snapshot of all metrics.
41    pub fn snapshot(&self) -> MetricsSnapshot {
42        let query_count = self.query_count.load(Ordering::Relaxed);
43        let total_query_time_ns = self.total_query_time_ns.load(Ordering::Relaxed);
44        let total_candidates = self.total_candidates_examined.load(Ordering::Relaxed);
45        let hits = self.bucket_hits.load(Ordering::Relaxed);
46        let misses = self.bucket_misses.load(Ordering::Relaxed);
47
48        MetricsSnapshot {
49            query_count,
50            insert_count: self.insert_count.load(Ordering::Relaxed),
51            avg_query_time_us: if query_count > 0 {
52                total_query_time_ns as f64 / query_count as f64 / 1000.0
53            } else {
54                0.0
55            },
56            avg_candidates_per_query: if query_count > 0 {
57                total_candidates as f64 / query_count as f64
58            } else {
59                0.0
60            },
61            hit_rate: if hits + misses > 0 {
62                hits as f64 / (hits + misses) as f64
63            } else {
64                0.0
65            },
66        }
67    }
68
69    /// Reset all counters to zero.
70    pub fn reset(&self) {
71        self.query_count.store(0, Ordering::Relaxed);
72        self.insert_count.store(0, Ordering::Relaxed);
73        self.total_candidates_examined.store(0, Ordering::Relaxed);
74        self.total_query_time_ns.store(0, Ordering::Relaxed);
75        self.bucket_hits.store(0, Ordering::Relaxed);
76        self.bucket_misses.store(0, Ordering::Relaxed);
77    }
78}
79
80/// A point-in-time snapshot of index metrics.
81#[derive(Debug, Clone)]
82pub struct MetricsSnapshot {
83    pub query_count: u64,
84    pub insert_count: u64,
85    pub avg_query_time_us: f64,
86    pub avg_candidates_per_query: f64,
87    /// Fraction of bucket probes that found at least one candidate.
88    pub hit_rate: f64,
89}
90
91impl std::fmt::Display for MetricsSnapshot {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(
94            f,
95            "Queries: {}, Inserts: {}, Avg query: {:.2}us, Avg candidates: {:.1}, Hit rate: {:.1}%",
96            self.query_count,
97            self.insert_count,
98            self.avg_query_time_us,
99            self.avg_candidates_per_query,
100            self.hit_rate * 100.0,
101        )
102    }
103}
104
105/// RAII timer for measuring operation durations.
106pub(crate) struct QueryTimer {
107    start: Instant,
108}
109
110impl QueryTimer {
111    pub fn new() -> Self {
112        Self {
113            start: Instant::now(),
114        }
115    }
116
117    pub fn elapsed_ns(&self) -> u64 {
118        self.start.elapsed().as_nanos() as u64
119    }
120}