Skip to main content

fraiseql_server/
performance.rs

1//! Performance monitoring and tracking for query execution.
2//!
3//! Tracks query performance metrics, builds performance profiles, and enables
4//! analysis of query execution patterns for optimization.
5
6#[allow(unused_imports)]
7use std::sync::Arc;
8use std::{
9    sync::atomic::{AtomicU64, Ordering},
10    time::Instant,
11};
12
13/// Query performance data.
14#[derive(Debug, Clone)]
15pub struct QueryPerformance {
16    /// Query execution time in microseconds
17    pub duration_us: u64,
18
19    /// Number of database queries executed
20    pub db_queries: u32,
21
22    /// Estimated query complexity (field count, depth, etc.)
23    pub complexity: u32,
24
25    /// Whether result was cached
26    pub cached: bool,
27
28    /// Database query time in microseconds
29    pub db_duration_us: u64,
30
31    /// Parse time in microseconds (for compilation-free operation)
32    pub parse_duration_us: u64,
33
34    /// Validation time in microseconds
35    pub validation_duration_us: u64,
36}
37
38impl QueryPerformance {
39    /// Create new query performance tracker.
40    #[must_use]
41    pub fn new(
42        duration_us: u64,
43        db_queries: u32,
44        complexity: u32,
45        cached: bool,
46        db_duration_us: u64,
47    ) -> Self {
48        Self {
49            duration_us,
50            db_queries,
51            complexity,
52            cached,
53            db_duration_us,
54            parse_duration_us: 0,
55            validation_duration_us: 0,
56        }
57    }
58
59    /// Set parse duration.
60    #[must_use]
61    pub fn with_parse_duration(mut self, duration_us: u64) -> Self {
62        self.parse_duration_us = duration_us;
63        self
64    }
65
66    /// Set validation duration.
67    #[must_use]
68    pub fn with_validation_duration(mut self, duration_us: u64) -> Self {
69        self.validation_duration_us = duration_us;
70        self
71    }
72
73    /// Calculate total non-database time in microseconds.
74    #[must_use]
75    pub fn non_db_duration_us(&self) -> u64 {
76        self.duration_us.saturating_sub(self.db_duration_us)
77    }
78
79    /// Calculate database time percentage.
80    #[must_use]
81    pub fn db_percentage(&self) -> f64 {
82        if self.duration_us == 0 {
83            0.0
84        } else {
85            (self.db_duration_us as f64 / self.duration_us as f64) * 100.0
86        }
87    }
88
89    /// Check if query is slow (over threshold).
90    #[must_use]
91    pub fn is_slow(&self, threshold_ms: f64) -> bool {
92        (self.duration_us as f64 / 1000.0) > threshold_ms
93    }
94}
95
96/// Performance profile for a specific operation type.
97#[derive(Debug, Clone)]
98pub struct OperationProfile {
99    /// Operation name (e.g., "`GetUser`", "`ListProducts`")
100    pub operation: String,
101
102    /// Total execution count
103    pub count: u64,
104
105    /// Total execution time (microseconds)
106    pub total_duration_us: u64,
107
108    /// Minimum execution time
109    pub min_duration_us: u64,
110
111    /// Maximum execution time
112    pub max_duration_us: u64,
113
114    /// Total database query count
115    pub total_db_queries: u64,
116
117    /// Average complexity
118    pub avg_complexity: f64,
119
120    /// Cache hit rate (0.0-1.0)
121    pub cache_hit_rate: f64,
122}
123
124impl OperationProfile {
125    /// Calculate average duration in milliseconds.
126    #[must_use]
127    pub fn avg_duration_ms(&self) -> f64 {
128        if self.count == 0 {
129            0.0
130        } else {
131            (self.total_duration_us as f64 / self.count as f64) / 1000.0
132        }
133    }
134
135    /// Calculate average database queries per operation.
136    #[must_use]
137    pub fn avg_db_queries(&self) -> f64 {
138        if self.count == 0 {
139            0.0
140        } else {
141            self.total_db_queries as f64 / self.count as f64
142        }
143    }
144}
145
146/// Performance monitor for tracking query execution metrics.
147#[derive(Debug, Clone)]
148pub struct PerformanceMonitor {
149    /// Total queries tracked
150    queries_tracked: Arc<AtomicU64>,
151
152    /// Total slow queries (above threshold)
153    slow_queries: Arc<AtomicU64>,
154
155    /// Total cached queries
156    cached_queries: Arc<AtomicU64>,
157
158    /// Total database queries
159    db_queries_total: Arc<AtomicU64>,
160
161    /// Total query execution time (microseconds)
162    total_duration_us: Arc<AtomicU64>,
163
164    /// Minimum query duration (microseconds)
165    min_duration_us: Arc<AtomicU64>,
166
167    /// Maximum query duration (microseconds)
168    max_duration_us: Arc<AtomicU64>,
169
170    /// Slow query threshold in milliseconds
171    slow_query_threshold_ms: f64,
172}
173
174impl PerformanceMonitor {
175    /// Create new performance monitor.
176    #[must_use]
177    pub fn new(slow_query_threshold_ms: f64) -> Self {
178        Self {
179            queries_tracked: Arc::new(AtomicU64::new(0)),
180            slow_queries: Arc::new(AtomicU64::new(0)),
181            cached_queries: Arc::new(AtomicU64::new(0)),
182            db_queries_total: Arc::new(AtomicU64::new(0)),
183            total_duration_us: Arc::new(AtomicU64::new(0)),
184            min_duration_us: Arc::new(AtomicU64::new(u64::MAX)),
185            max_duration_us: Arc::new(AtomicU64::new(0)),
186            slow_query_threshold_ms,
187        }
188    }
189
190    /// Record a query execution.
191    pub fn record_query(&self, performance: QueryPerformance) {
192        // Track query count
193        self.queries_tracked.fetch_add(1, Ordering::Relaxed);
194
195        // Track duration
196        self.total_duration_us.fetch_add(performance.duration_us, Ordering::Relaxed);
197
198        // Track min/max
199        let mut min = self.min_duration_us.load(Ordering::Relaxed);
200        while performance.duration_us < min
201            && self
202                .min_duration_us
203                .compare_exchange(
204                    min,
205                    performance.duration_us,
206                    Ordering::Relaxed,
207                    Ordering::Relaxed,
208                )
209                .is_err()
210        {
211            min = self.min_duration_us.load(Ordering::Relaxed);
212        }
213
214        let mut max = self.max_duration_us.load(Ordering::Relaxed);
215        while performance.duration_us > max
216            && self
217                .max_duration_us
218                .compare_exchange(
219                    max,
220                    performance.duration_us,
221                    Ordering::Relaxed,
222                    Ordering::Relaxed,
223                )
224                .is_err()
225        {
226            max = self.max_duration_us.load(Ordering::Relaxed);
227        }
228
229        // Track slow queries
230        if performance.is_slow(self.slow_query_threshold_ms) {
231            self.slow_queries.fetch_add(1, Ordering::Relaxed);
232        }
233
234        // Track cached queries
235        if performance.cached {
236            self.cached_queries.fetch_add(1, Ordering::Relaxed);
237        }
238
239        // Track database queries
240        self.db_queries_total
241            .fetch_add(u64::from(performance.db_queries), Ordering::Relaxed);
242    }
243
244    /// Get performance statistics.
245    #[must_use]
246    pub fn stats(&self) -> PerformanceStats {
247        let queries_tracked = self.queries_tracked.load(Ordering::Relaxed);
248        let slow_queries = self.slow_queries.load(Ordering::Relaxed);
249        let cached_queries = self.cached_queries.load(Ordering::Relaxed);
250        let db_queries_total = self.db_queries_total.load(Ordering::Relaxed);
251        let total_duration_us = self.total_duration_us.load(Ordering::Relaxed);
252        let min_duration_us = self.min_duration_us.load(Ordering::Relaxed);
253        let max_duration_us = self.max_duration_us.load(Ordering::Relaxed);
254
255        PerformanceStats {
256            queries_tracked,
257            slow_queries,
258            cached_queries,
259            db_queries_total,
260            total_duration_us,
261            min_duration_us,
262            max_duration_us,
263        }
264    }
265
266    /// Get average query duration in milliseconds.
267    #[must_use]
268    pub fn avg_duration_ms(&self) -> f64 {
269        let stats = self.stats();
270        if stats.queries_tracked == 0 {
271            0.0
272        } else {
273            (stats.total_duration_us as f64 / stats.queries_tracked as f64) / 1000.0
274        }
275    }
276
277    /// Get slow query percentage.
278    #[must_use]
279    pub fn slow_query_percentage(&self) -> f64 {
280        let stats = self.stats();
281        if stats.queries_tracked == 0 {
282            0.0
283        } else {
284            (stats.slow_queries as f64 / stats.queries_tracked as f64) * 100.0
285        }
286    }
287
288    /// Get cache hit rate (0.0-1.0).
289    #[must_use]
290    pub fn cache_hit_rate(&self) -> f64 {
291        let stats = self.stats();
292        if stats.queries_tracked == 0 {
293            0.0
294        } else {
295            stats.cached_queries as f64 / stats.queries_tracked as f64
296        }
297    }
298
299    /// Create timing guard for duration tracking.
300    #[must_use]
301    pub fn create_timer(&self) -> PerformanceTimer {
302        PerformanceTimer::new()
303    }
304}
305
306/// Performance statistics snapshot.
307#[derive(Debug, Clone)]
308pub struct PerformanceStats {
309    /// Total queries tracked
310    pub queries_tracked: u64,
311
312    /// Total slow queries
313    pub slow_queries: u64,
314
315    /// Total cached queries
316    pub cached_queries: u64,
317
318    /// Total database queries
319    pub db_queries_total: u64,
320
321    /// Total execution time (microseconds)
322    pub total_duration_us: u64,
323
324    /// Minimum duration (microseconds)
325    pub min_duration_us: u64,
326
327    /// Maximum duration (microseconds)
328    pub max_duration_us: u64,
329}
330
331impl PerformanceStats {
332    /// Average query duration in milliseconds.
333    #[must_use]
334    pub fn avg_duration_ms(&self) -> f64 {
335        if self.queries_tracked == 0 {
336            0.0
337        } else {
338            (self.total_duration_us as f64 / self.queries_tracked as f64) / 1000.0
339        }
340    }
341
342    /// Average database queries per operation.
343    #[must_use]
344    pub fn avg_db_queries(&self) -> f64 {
345        if self.queries_tracked == 0 {
346            0.0
347        } else {
348            self.db_queries_total as f64 / self.queries_tracked as f64
349        }
350    }
351
352    /// Slow query percentage.
353    #[must_use]
354    pub fn slow_query_percentage(&self) -> f64 {
355        if self.queries_tracked == 0 {
356            0.0
357        } else {
358            (self.slow_queries as f64 / self.queries_tracked as f64) * 100.0
359        }
360    }
361}
362
363/// Timer for measuring operation duration.
364#[derive(Debug)]
365pub struct PerformanceTimer {
366    start: Instant,
367}
368
369impl PerformanceTimer {
370    /// Create new performance timer.
371    #[must_use]
372    pub fn new() -> Self {
373        Self {
374            start: Instant::now(),
375        }
376    }
377
378    /// Record duration and consume timer.
379    #[must_use]
380    pub fn record(self) -> u64 {
381        self.start.elapsed().as_micros() as u64
382    }
383
384    /// Record duration and get reference to elapsed time.
385    #[must_use]
386    pub fn elapsed_us(&self) -> u64 {
387        self.start.elapsed().as_micros() as u64
388    }
389}
390
391impl Default for PerformanceTimer {
392    fn default() -> Self {
393        Self::new()
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_query_performance_creation() {
403        let perf = QueryPerformance::new(1000, 2, 5, false, 500);
404        assert_eq!(perf.duration_us, 1000);
405        assert_eq!(perf.db_queries, 2);
406        assert_eq!(perf.complexity, 5);
407        assert!(!perf.cached);
408    }
409
410    #[test]
411    fn test_query_performance_with_durations() {
412        let perf = QueryPerformance::new(1000, 2, 5, false, 500)
413            .with_parse_duration(100)
414            .with_validation_duration(150);
415
416        assert_eq!(perf.parse_duration_us, 100);
417        assert_eq!(perf.validation_duration_us, 150);
418    }
419
420    #[test]
421    fn test_query_performance_calculations() {
422        let perf = QueryPerformance::new(1000, 2, 5, false, 600);
423
424        assert_eq!(perf.non_db_duration_us(), 400);
425        assert!(perf.db_percentage() > 59.0 && perf.db_percentage() < 61.0); // ~60%
426    }
427
428    #[test]
429    fn test_query_performance_is_slow() {
430        let fast = QueryPerformance::new(5000, 1, 3, false, 1000); // 5ms
431        let slow = QueryPerformance::new(50000, 3, 8, false, 40000); // 50ms
432
433        assert!(!fast.is_slow(10.0));
434        assert!(slow.is_slow(10.0));
435    }
436
437    #[test]
438    fn test_performance_monitor_creation() {
439        let monitor = PerformanceMonitor::new(100.0);
440        let stats = monitor.stats();
441
442        assert_eq!(stats.queries_tracked, 0);
443        assert_eq!(stats.slow_queries, 0);
444    }
445
446    #[test]
447    fn test_performance_monitor_record_query() {
448        let monitor = PerformanceMonitor::new(10.0);
449        let perf = QueryPerformance::new(5000, 2, 5, false, 2500);
450
451        monitor.record_query(perf);
452
453        let stats = monitor.stats();
454        assert_eq!(stats.queries_tracked, 1);
455        assert_eq!(stats.total_duration_us, 5000);
456        assert_eq!(stats.db_queries_total, 2);
457    }
458
459    #[test]
460    fn test_performance_monitor_slow_queries() {
461        let monitor = PerformanceMonitor::new(10.0);
462
463        let slow_perf = QueryPerformance::new(20000, 2, 5, false, 10000); // 20ms
464        monitor.record_query(slow_perf);
465
466        let stats = monitor.stats();
467        assert_eq!(stats.slow_queries, 1);
468        assert_eq!(stats.queries_tracked, 1);
469    }
470
471    #[test]
472    fn test_performance_monitor_cached_queries() {
473        let monitor = PerformanceMonitor::new(10.0);
474
475        let cached_perf = QueryPerformance::new(1000, 0, 5, true, 0);
476        monitor.record_query(cached_perf);
477
478        let stats = monitor.stats();
479        assert_eq!(stats.cached_queries, 1);
480    }
481
482    #[test]
483    fn test_performance_monitor_avg_calculations() {
484        let monitor = PerformanceMonitor::new(10.0);
485
486        monitor.record_query(QueryPerformance::new(2000, 1, 5, false, 1000));
487        monitor.record_query(QueryPerformance::new(4000, 3, 5, false, 2000));
488
489        assert!((monitor.avg_duration_ms() - 3.0).abs() < f64::EPSILON); // (2000 + 4000) / 2 / 1000
490    }
491
492    #[test]
493    fn test_performance_monitor_cache_hit_rate() {
494        let monitor = PerformanceMonitor::new(10.0);
495
496        monitor.record_query(QueryPerformance::new(1000, 1, 5, false, 800));
497        monitor.record_query(QueryPerformance::new(1000, 0, 5, true, 0));
498        monitor.record_query(QueryPerformance::new(1000, 1, 5, false, 800));
499
500        let rate = monitor.cache_hit_rate();
501        assert!(rate > 0.32 && rate < 0.34); // ~33%
502    }
503
504    #[test]
505    fn test_performance_timer_creation() {
506        let timer = PerformanceTimer::new();
507        let duration = timer.elapsed_us();
508        assert!(duration < 1000); // Should be very fast
509    }
510
511    #[test]
512    fn test_performance_timer_record() {
513        let timer = PerformanceTimer::new();
514        std::thread::sleep(std::time::Duration::from_millis(10));
515        let duration = timer.record();
516
517        assert!(duration >= 10000); // At least 10ms in microseconds
518    }
519
520    #[test]
521    fn test_performance_stats_calculations() {
522        let stats = PerformanceStats {
523            queries_tracked:   100,
524            slow_queries:      10,
525            cached_queries:    30,
526            db_queries_total:  200,
527            total_duration_us: 500_000,
528            min_duration_us:   1000,
529            max_duration_us:   50_000,
530        };
531
532        assert!((stats.avg_duration_ms() - 5.0).abs() < f64::EPSILON);
533        assert!((stats.avg_db_queries() - 2.0).abs() < f64::EPSILON);
534        assert!((stats.slow_query_percentage() - 10.0).abs() < f64::EPSILON);
535    }
536
537    #[test]
538    fn test_operation_profile_creation() {
539        let profile = OperationProfile {
540            operation:         "GetUser".to_string(),
541            count:             100,
542            total_duration_us: 500_000,
543            min_duration_us:   1000,
544            max_duration_us:   50_000,
545            total_db_queries:  200,
546            avg_complexity:    5.5,
547            cache_hit_rate:    0.75,
548        };
549
550        assert!((profile.avg_duration_ms() - 5.0).abs() < f64::EPSILON);
551        assert!((profile.avg_db_queries() - 2.0).abs() < f64::EPSILON);
552    }
553}