codemem_engine/
metrics.rs1use codemem_core::{LatencyStats, Metrics, MetricsSnapshot};
4use std::collections::{HashMap, VecDeque};
5use std::sync::Mutex;
6
7const MAX_SAMPLES: usize = 10_000;
9
10pub struct InMemoryMetrics {
15 inner: Mutex<Inner>,
16}
17
18struct Inner {
19 latency_samples: HashMap<String, VecDeque<f64>>,
21 counters: HashMap<String, u64>,
23 gauges: HashMap<String, f64>,
25}
26
27impl InMemoryMetrics {
28 pub fn new() -> Self {
29 Self {
30 inner: Mutex::new(Inner {
31 latency_samples: HashMap::new(),
32 counters: HashMap::new(),
33 gauges: HashMap::new(),
34 }),
35 }
36 }
37
38 pub fn snapshot(&self) -> MetricsSnapshot {
40 let inner = match self.inner.lock() {
41 Ok(guard) => guard,
42 Err(e) => {
43 tracing::warn!("Metrics lock poisoned: {e}");
44 return MetricsSnapshot::default();
45 }
46 };
47
48 let latencies: HashMap<String, LatencyStats> = inner
49 .latency_samples
50 .iter()
51 .map(|(name, samples)| {
52 let stats = compute_latency_stats(samples);
53 (name.clone(), stats)
54 })
55 .collect();
56
57 MetricsSnapshot {
58 latencies,
59 counters: inner.counters.clone(),
60 gauges: inner.gauges.clone(),
61 }
62 }
63}
64
65impl Default for InMemoryMetrics {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Metrics for InMemoryMetrics {
72 fn record_latency(&self, operation: &str, duration_ms: f64) {
73 if let Ok(mut inner) = self.inner.lock() {
74 let samples = inner
75 .latency_samples
76 .entry(operation.to_string())
77 .or_default();
78 if samples.len() >= MAX_SAMPLES {
79 samples.pop_front();
80 }
81 samples.push_back(duration_ms);
82 }
83 }
84
85 fn increment_counter(&self, name: &str, delta: u64) {
86 if let Ok(mut inner) = self.inner.lock() {
87 *inner.counters.entry(name.to_string()).or_insert(0) += delta;
88 }
89 }
90
91 fn record_gauge(&self, name: &str, value: f64) {
92 if let Ok(mut inner) = self.inner.lock() {
93 inner.gauges.insert(name.to_string(), value);
94 }
95 }
96}
97
98fn compute_latency_stats(samples: &VecDeque<f64>) -> LatencyStats {
101 if samples.is_empty() {
102 return LatencyStats::default();
103 }
104
105 let mut sorted: Vec<f64> = samples.iter().copied().collect();
106 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
107
108 let count = sorted.len() as u64;
109 let total: f64 = sorted.iter().sum();
110 let min = sorted[0];
111 let max = sorted[sorted.len() - 1];
112
113 let p50 = percentile(&sorted, 50.0);
114 let p95 = percentile(&sorted, 95.0);
115 let p99 = percentile(&sorted, 99.0);
116
117 LatencyStats {
118 count,
119 total_ms: total,
120 min_ms: min,
121 max_ms: max,
122 p50_ms: p50,
123 p95_ms: p95,
124 p99_ms: p99,
125 }
126}
127
128fn percentile(sorted: &[f64], pct: f64) -> f64 {
129 if sorted.is_empty() {
130 return 0.0;
131 }
132 let idx = (pct / 100.0 * (sorted.len() as f64 - 1.0)).round() as usize;
133 sorted[idx.min(sorted.len() - 1)]
134}
135
136#[cfg(test)]
137#[path = "tests/metrics_tests.rs"]
138mod tests;