Skip to main content

featherdb_query/
metrics.rs

1//! Query metrics and performance tracking
2//!
3//! Provides infrastructure for tracking query execution metrics using atomic counters.
4//! Metrics are thread-safe and can be shared across multiple query executions.
5
6use std::sync::atomic::{AtomicU64, Ordering};
7
8/// Thread-safe query metrics using atomic counters
9#[derive(Debug, Default)]
10pub struct QueryMetrics {
11    /// Total number of queries executed
12    queries_executed: AtomicU64,
13    /// Total parse time in microseconds
14    total_parse_time_us: AtomicU64,
15    /// Total planning time in microseconds
16    total_plan_time_us: AtomicU64,
17    /// Total execution time in microseconds
18    total_exec_time_us: AtomicU64,
19    /// Total rows scanned during execution
20    rows_scanned: AtomicU64,
21    /// Total rows returned to client
22    rows_returned: AtomicU64,
23}
24
25/// Snapshot of query metrics at a point in time
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct QueryMetricsSnapshot {
28    /// Total number of queries executed
29    pub queries_executed: u64,
30    /// Total parse time in microseconds
31    pub total_parse_time_us: u64,
32    /// Total planning time in microseconds
33    pub total_plan_time_us: u64,
34    /// Total execution time in microseconds
35    pub total_exec_time_us: u64,
36    /// Total rows scanned during execution
37    pub rows_scanned: u64,
38    /// Total rows returned to client
39    pub rows_returned: u64,
40}
41
42impl QueryMetrics {
43    /// Create a new QueryMetrics instance
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Record parse time in microseconds
49    pub fn record_parse_time(&self, micros: u64) {
50        self.total_parse_time_us
51            .fetch_add(micros, Ordering::Relaxed);
52    }
53
54    /// Record planning time in microseconds
55    pub fn record_plan_time(&self, micros: u64) {
56        self.total_plan_time_us.fetch_add(micros, Ordering::Relaxed);
57    }
58
59    /// Record execution time in microseconds
60    pub fn record_exec_time(&self, micros: u64) {
61        self.total_exec_time_us.fetch_add(micros, Ordering::Relaxed);
62    }
63
64    /// Record rows scanned
65    pub fn record_rows_scanned(&self, count: u64) {
66        self.rows_scanned.fetch_add(count, Ordering::Relaxed);
67    }
68
69    /// Record rows returned
70    pub fn record_rows_returned(&self, count: u64) {
71        self.rows_returned.fetch_add(count, Ordering::Relaxed);
72    }
73
74    /// Increment the query execution counter
75    pub fn increment_queries(&self) {
76        self.queries_executed.fetch_add(1, Ordering::Relaxed);
77    }
78
79    /// Get a snapshot of current metrics
80    pub fn snapshot(&self) -> QueryMetricsSnapshot {
81        QueryMetricsSnapshot {
82            queries_executed: self.queries_executed.load(Ordering::Relaxed),
83            total_parse_time_us: self.total_parse_time_us.load(Ordering::Relaxed),
84            total_plan_time_us: self.total_plan_time_us.load(Ordering::Relaxed),
85            total_exec_time_us: self.total_exec_time_us.load(Ordering::Relaxed),
86            rows_scanned: self.rows_scanned.load(Ordering::Relaxed),
87            rows_returned: self.rows_returned.load(Ordering::Relaxed),
88        }
89    }
90
91    /// Reset all metrics to zero
92    pub fn reset(&self) {
93        self.queries_executed.store(0, Ordering::Relaxed);
94        self.total_parse_time_us.store(0, Ordering::Relaxed);
95        self.total_plan_time_us.store(0, Ordering::Relaxed);
96        self.total_exec_time_us.store(0, Ordering::Relaxed);
97        self.rows_scanned.store(0, Ordering::Relaxed);
98        self.rows_returned.store(0, Ordering::Relaxed);
99    }
100}
101
102impl QueryMetricsSnapshot {
103    /// Get average parse time in microseconds
104    pub fn avg_parse_time_us(&self) -> f64 {
105        if self.queries_executed == 0 {
106            0.0
107        } else {
108            self.total_parse_time_us as f64 / self.queries_executed as f64
109        }
110    }
111
112    /// Get average planning time in microseconds
113    pub fn avg_plan_time_us(&self) -> f64 {
114        if self.queries_executed == 0 {
115            0.0
116        } else {
117            self.total_plan_time_us as f64 / self.queries_executed as f64
118        }
119    }
120
121    /// Get average execution time in microseconds
122    pub fn avg_exec_time_us(&self) -> f64 {
123        if self.queries_executed == 0 {
124            0.0
125        } else {
126            self.total_exec_time_us as f64 / self.queries_executed as f64
127        }
128    }
129
130    /// Get average total query time (parse + plan + exec) in microseconds
131    pub fn avg_total_time_us(&self) -> f64 {
132        if self.queries_executed == 0 {
133            0.0
134        } else {
135            let total =
136                self.total_parse_time_us + self.total_plan_time_us + self.total_exec_time_us;
137            total as f64 / self.queries_executed as f64
138        }
139    }
140
141    /// Get average rows scanned per query
142    pub fn avg_rows_scanned(&self) -> f64 {
143        if self.queries_executed == 0 {
144            0.0
145        } else {
146            self.rows_scanned as f64 / self.queries_executed as f64
147        }
148    }
149
150    /// Get average rows returned per query
151    pub fn avg_rows_returned(&self) -> f64 {
152        if self.queries_executed == 0 {
153            0.0
154        } else {
155            self.rows_returned as f64 / self.queries_executed as f64
156        }
157    }
158
159    /// Get selectivity ratio (rows returned / rows scanned)
160    pub fn selectivity_ratio(&self) -> f64 {
161        if self.rows_scanned == 0 {
162            0.0
163        } else {
164            self.rows_returned as f64 / self.rows_scanned as f64
165        }
166    }
167
168    /// Get total query time (parse + plan + exec) in microseconds
169    pub fn total_query_time_us(&self) -> u64 {
170        self.total_parse_time_us + self.total_plan_time_us + self.total_exec_time_us
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_metrics_basic() {
180        let metrics = QueryMetrics::new();
181
182        // Record some metrics
183        metrics.increment_queries();
184        metrics.record_parse_time(100);
185        metrics.record_plan_time(200);
186        metrics.record_exec_time(500);
187        metrics.record_rows_scanned(1000);
188        metrics.record_rows_returned(50);
189
190        let snapshot = metrics.snapshot();
191        assert_eq!(snapshot.queries_executed, 1);
192        assert_eq!(snapshot.total_parse_time_us, 100);
193        assert_eq!(snapshot.total_plan_time_us, 200);
194        assert_eq!(snapshot.total_exec_time_us, 500);
195        assert_eq!(snapshot.rows_scanned, 1000);
196        assert_eq!(snapshot.rows_returned, 50);
197    }
198
199    #[test]
200    fn test_metrics_accumulation() {
201        let metrics = QueryMetrics::new();
202
203        // Record two queries
204        metrics.increment_queries();
205        metrics.record_parse_time(100);
206        metrics.record_plan_time(200);
207        metrics.record_exec_time(500);
208        metrics.record_rows_scanned(1000);
209        metrics.record_rows_returned(50);
210
211        metrics.increment_queries();
212        metrics.record_parse_time(150);
213        metrics.record_plan_time(250);
214        metrics.record_exec_time(600);
215        metrics.record_rows_scanned(2000);
216        metrics.record_rows_returned(100);
217
218        let snapshot = metrics.snapshot();
219        assert_eq!(snapshot.queries_executed, 2);
220        assert_eq!(snapshot.total_parse_time_us, 250);
221        assert_eq!(snapshot.total_plan_time_us, 450);
222        assert_eq!(snapshot.total_exec_time_us, 1100);
223        assert_eq!(snapshot.rows_scanned, 3000);
224        assert_eq!(snapshot.rows_returned, 150);
225    }
226
227    #[test]
228    fn test_metrics_averages() {
229        let metrics = QueryMetrics::new();
230
231        // Record two queries
232        metrics.increment_queries();
233        metrics.record_parse_time(100);
234        metrics.record_plan_time(200);
235        metrics.record_exec_time(500);
236        metrics.record_rows_scanned(1000);
237        metrics.record_rows_returned(50);
238
239        metrics.increment_queries();
240        metrics.record_parse_time(200);
241        metrics.record_plan_time(400);
242        metrics.record_exec_time(700);
243        metrics.record_rows_scanned(2000);
244        metrics.record_rows_returned(150);
245
246        let snapshot = metrics.snapshot();
247        assert_eq!(snapshot.avg_parse_time_us(), 150.0);
248        assert_eq!(snapshot.avg_plan_time_us(), 300.0);
249        assert_eq!(snapshot.avg_exec_time_us(), 600.0);
250        assert_eq!(snapshot.avg_total_time_us(), 1050.0);
251        assert_eq!(snapshot.avg_rows_scanned(), 1500.0);
252        assert_eq!(snapshot.avg_rows_returned(), 100.0);
253    }
254
255    #[test]
256    fn test_selectivity_ratio() {
257        let metrics = QueryMetrics::new();
258
259        metrics.increment_queries();
260        metrics.record_rows_scanned(1000);
261        metrics.record_rows_returned(100);
262
263        let snapshot = metrics.snapshot();
264        assert_eq!(snapshot.selectivity_ratio(), 0.1);
265    }
266
267    #[test]
268    fn test_metrics_reset() {
269        let metrics = QueryMetrics::new();
270
271        metrics.increment_queries();
272        metrics.record_parse_time(100);
273        metrics.record_plan_time(200);
274        metrics.record_exec_time(500);
275        metrics.record_rows_scanned(1000);
276        metrics.record_rows_returned(50);
277
278        let snapshot_before = metrics.snapshot();
279        assert_eq!(snapshot_before.queries_executed, 1);
280
281        metrics.reset();
282
283        let snapshot_after = metrics.snapshot();
284        assert_eq!(snapshot_after.queries_executed, 0);
285        assert_eq!(snapshot_after.total_parse_time_us, 0);
286        assert_eq!(snapshot_after.total_plan_time_us, 0);
287        assert_eq!(snapshot_after.total_exec_time_us, 0);
288        assert_eq!(snapshot_after.rows_scanned, 0);
289        assert_eq!(snapshot_after.rows_returned, 0);
290    }
291
292    #[test]
293    fn test_zero_queries_averages() {
294        let metrics = QueryMetrics::new();
295        let snapshot = metrics.snapshot();
296
297        // All averages should be 0.0 when no queries executed
298        assert_eq!(snapshot.avg_parse_time_us(), 0.0);
299        assert_eq!(snapshot.avg_plan_time_us(), 0.0);
300        assert_eq!(snapshot.avg_exec_time_us(), 0.0);
301        assert_eq!(snapshot.avg_total_time_us(), 0.0);
302        assert_eq!(snapshot.avg_rows_scanned(), 0.0);
303        assert_eq!(snapshot.avg_rows_returned(), 0.0);
304        assert_eq!(snapshot.selectivity_ratio(), 0.0);
305    }
306
307    #[test]
308    fn test_total_query_time() {
309        let metrics = QueryMetrics::new();
310
311        metrics.increment_queries();
312        metrics.record_parse_time(100);
313        metrics.record_plan_time(200);
314        metrics.record_exec_time(500);
315
316        let snapshot = metrics.snapshot();
317        assert_eq!(snapshot.total_query_time_us(), 800);
318    }
319}