Skip to main content

forgekit_runtime/
metrics.rs

1//! Runtime metrics for operations, timing, and cache statistics.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::Arc;
5use std::time::Duration;
6
7/// Kind of metric being tracked.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum MetricKind {
10    /// Graph query operation
11    GraphQuery,
12    /// Semantic search operation
13    Search,
14    /// CFG analysis operation
15    CfgAnalysis,
16    /// Cache hit
17    CacheHit,
18    /// Cache miss
19    CacheMiss,
20    /// Re-index operation
21    Reindex,
22}
23
24/// Runtime metrics collector.
25///
26/// Tracks operation counts, timing data, and cache statistics.
27#[derive(Clone, Debug)]
28pub struct RuntimeMetrics {
29    inner: Arc<MetricsInner>,
30}
31
32#[derive(Debug)]
33struct MetricsInner {
34    /// Operation counts by kind
35    counts: [AtomicU64; 6],
36    /// Total operation time (nanoseconds)
37    total_time_ns: AtomicU64,
38    /// Cache hits
39    cache_hits: AtomicU64,
40    /// Cache misses
41    cache_misses: AtomicU64,
42}
43
44impl RuntimeMetrics {
45    /// Creates a new metrics collector.
46    pub fn new() -> Self {
47        Self {
48            inner: Arc::new(MetricsInner {
49                counts: [
50                    AtomicU64::new(0), // GraphQuery
51                    AtomicU64::new(0), // Search
52                    AtomicU64::new(0), // CfgAnalysis
53                    AtomicU64::new(0), // CacheHit
54                    AtomicU64::new(0), // CacheMiss
55                    AtomicU64::new(0), // Reindex
56                ],
57                total_time_ns: AtomicU64::new(0),
58                cache_hits: AtomicU64::new(0),
59                cache_misses: AtomicU64::new(0),
60            }),
61        }
62    }
63
64    /// Records a metric occurrence.
65    pub fn record(&self, kind: MetricKind) {
66        let index = kind as usize;
67        self.inner.counts[index].fetch_add(1, Ordering::Relaxed);
68    }
69
70    /// Records a timed operation.
71    pub fn record_timing(&self, kind: MetricKind, duration: Duration) {
72        self.record(kind);
73        self.inner
74            .total_time_ns
75            .fetch_add(duration.as_nanos() as u64, Ordering::Relaxed);
76    }
77
78    /// Records a cache access.
79    pub fn record_cache_access(&self, hit: bool) {
80        if hit {
81            self.inner.cache_hits.fetch_add(1, Ordering::Relaxed);
82            self.record(MetricKind::CacheHit);
83        } else {
84            self.inner.cache_misses.fetch_add(1, Ordering::Relaxed);
85            self.record(MetricKind::CacheMiss);
86        }
87    }
88
89    /// Gets the count for a specific metric.
90    pub fn count(&self, kind: MetricKind) -> u64 {
91        self.inner.counts[kind as usize].load(Ordering::Relaxed)
92    }
93
94    /// Gets the total operation time.
95    pub fn total_time(&self) -> Duration {
96        Duration::from_nanos(self.inner.total_time_ns.load(Ordering::Relaxed))
97    }
98
99    /// Gets the cache hit rate (0.0 to 1.0).
100    pub fn cache_hit_rate(&self) -> f64 {
101        let hits = self.inner.cache_hits.load(Ordering::Relaxed);
102        let misses = self.inner.cache_misses.load(Ordering::Relaxed);
103        let total = hits + misses;
104
105        if total == 0 {
106            return 0.0;
107        }
108
109        hits as f64 / total as f64
110    }
111
112    /// Gets all metrics as a summary.
113    pub fn summary(&self) -> MetricsSummary {
114        MetricsSummary {
115            graph_queries: self.count(MetricKind::GraphQuery),
116            searches: self.count(MetricKind::Search),
117            cfg_analyses: self.count(MetricKind::CfgAnalysis),
118            reindex_ops: self.count(MetricKind::Reindex),
119            total_time: self.total_time(),
120            cache_hit_rate: self.cache_hit_rate(),
121        }
122    }
123
124    /// Resets all metrics to zero.
125    pub fn reset(&self) {
126        for count in &self.inner.counts {
127            count.store(0, Ordering::Relaxed);
128        }
129        self.inner.total_time_ns.store(0, Ordering::Relaxed);
130        self.inner.cache_hits.store(0, Ordering::Relaxed);
131        self.inner.cache_misses.store(0, Ordering::Relaxed);
132    }
133}
134
135impl Default for RuntimeMetrics {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141/// Summary of runtime metrics.
142#[derive(Debug, Clone)]
143pub struct MetricsSummary {
144    /// Number of graph queries performed
145    pub graph_queries: u64,
146    /// Number of search operations performed
147    pub searches: u64,
148    /// Number of CFG analyses performed
149    pub cfg_analyses: u64,
150    /// Number of re-index operations performed
151    pub reindex_ops: u64,
152    /// Total time spent on operations
153    pub total_time: Duration,
154    /// Cache hit rate (0.0 to 1.0)
155    pub cache_hit_rate: f64,
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_metrics_record() {
164        let metrics = RuntimeMetrics::new();
165
166        metrics.record(MetricKind::GraphQuery);
167        metrics.record(MetricKind::GraphQuery);
168
169        assert_eq!(metrics.count(MetricKind::GraphQuery), 2);
170        assert_eq!(metrics.count(MetricKind::Search), 0);
171    }
172
173    #[test]
174    fn test_metrics_timing() {
175        let metrics = RuntimeMetrics::new();
176
177        metrics.record_timing(MetricKind::Search, Duration::from_millis(100));
178
179        assert_eq!(metrics.count(MetricKind::Search), 1);
180        assert_eq!(metrics.total_time(), Duration::from_millis(100));
181    }
182
183    #[test]
184    fn test_cache_hit_rate() {
185        let metrics = RuntimeMetrics::new();
186
187        metrics.record_cache_access(true);
188        metrics.record_cache_access(true);
189        metrics.record_cache_access(false);
190
191        // 2 hits out of 3 = 0.666...
192        assert!((metrics.cache_hit_rate() - 0.666).abs() < 0.01);
193    }
194
195    #[test]
196    fn test_metrics_reset() {
197        let metrics = RuntimeMetrics::new();
198
199        metrics.record(MetricKind::GraphQuery);
200        metrics.record_cache_access(true);
201
202        metrics.reset();
203
204        assert_eq!(metrics.count(MetricKind::GraphQuery), 0);
205        assert_eq!(metrics.cache_hit_rate(), 0.0);
206    }
207
208    #[test]
209    fn test_metrics_summary() {
210        let metrics = RuntimeMetrics::new();
211
212        metrics.record(MetricKind::GraphQuery);
213        metrics.record(MetricKind::Search);
214        metrics.record(MetricKind::CfgAnalysis);
215        metrics.record_cache_access(true);
216
217        let summary = metrics.summary();
218
219        assert_eq!(summary.graph_queries, 1);
220        assert_eq!(summary.searches, 1);
221        assert_eq!(summary.cfg_analyses, 1);
222        assert_eq!(summary.cache_hit_rate, 1.0);
223    }
224}