Skip to main content

things3_core/
query_performance.rs

1//! Database query performance tracking and optimization
2
3use chrono::{DateTime, Utc};
4use parking_lot::RwLock;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Instant;
9use tracing::debug;
10use uuid::Uuid;
11
12/// Query execution context for tracking performance
13#[derive(Debug, Clone)]
14pub struct QueryContext {
15    pub query_id: Uuid,
16    pub query_type: String,
17    pub query_text: String,
18    pub parameters: Vec<String>,
19    pub start_time: Instant,
20    pub cache_hit: bool,
21    pub result_size: Option<usize>,
22}
23
24/// Query performance metrics
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct QueryPerformanceMetrics {
27    pub query_id: Uuid,
28    pub query_type: String,
29    pub query_text: String,
30    pub execution_time_ms: u64,
31    pub cache_hit: bool,
32    pub result_size: Option<usize>,
33    pub memory_usage_bytes: Option<u64>,
34    pub cpu_usage_percent: Option<f64>,
35    pub timestamp: DateTime<Utc>,
36    pub parameters: Vec<String>,
37    pub optimization_applied: Vec<String>,
38}
39
40/// Aggregated query performance statistics
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct QueryPerformanceStats {
43    pub query_type: String,
44    pub total_executions: u64,
45    pub cache_hits: u64,
46    pub cache_misses: u64,
47    pub average_execution_time_ms: f64,
48    pub min_execution_time_ms: u64,
49    pub max_execution_time_ms: u64,
50    pub p95_execution_time_ms: u64,
51    pub p99_execution_time_ms: u64,
52    pub average_result_size: f64,
53    pub total_memory_usage_bytes: u64,
54    pub average_cpu_usage_percent: f64,
55    pub cache_hit_rate: f64,
56    pub slow_queries_count: u64,
57    pub fast_queries_count: u64,
58    pub last_executed: Option<DateTime<Utc>>,
59}
60
61/// Query optimization suggestions
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct QueryOptimizationSuggestion {
64    pub query_type: String,
65    pub suggestion_type: OptimizationType,
66    pub description: String,
67    pub potential_improvement_percent: f64,
68    pub priority: OptimizationPriority,
69    pub implementation_effort: ImplementationEffort,
70}
71
72/// Types of query optimizations
73#[non_exhaustive]
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub enum OptimizationType {
76    /// Add database index
77    AddIndex,
78    /// Use prepared statement
79    UsePreparedStatement,
80    /// Optimize query structure
81    OptimizeQuery,
82    /// Add caching
83    AddCaching,
84    /// Reduce result set size
85    ReduceResultSet,
86    /// Use connection pooling
87    UseConnectionPooling,
88}
89
90/// Priority levels for optimizations
91#[non_exhaustive]
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
93pub enum OptimizationPriority {
94    Low,
95    Medium,
96    High,
97    Critical,
98}
99
100/// Implementation effort levels
101#[non_exhaustive]
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
103pub enum ImplementationEffort {
104    Low,
105    Medium,
106    High,
107}
108
109/// Query performance tracker
110pub struct QueryPerformanceTracker {
111    /// Individual query metrics
112    metrics: Arc<RwLock<Vec<QueryPerformanceMetrics>>>,
113    /// Aggregated statistics by query type
114    stats: Arc<RwLock<HashMap<String, QueryPerformanceStats>>>,
115    /// Optimization suggestions
116    suggestions: Arc<RwLock<Vec<QueryOptimizationSuggestion>>>,
117    /// Maximum number of metrics to keep
118    max_metrics: usize,
119    /// Slow query threshold in milliseconds
120    slow_query_threshold_ms: u64,
121    /// Fast query threshold in milliseconds
122    fast_query_threshold_ms: u64,
123}
124
125impl QueryPerformanceTracker {
126    /// Create a new query performance tracker
127    #[must_use]
128    pub fn new(
129        max_metrics: usize,
130        slow_query_threshold_ms: u64,
131        fast_query_threshold_ms: u64,
132    ) -> Self {
133        Self {
134            metrics: Arc::new(RwLock::new(Vec::new())),
135            stats: Arc::new(RwLock::new(HashMap::new())),
136            suggestions: Arc::new(RwLock::new(Vec::new())),
137            max_metrics,
138            slow_query_threshold_ms,
139            fast_query_threshold_ms,
140        }
141    }
142
143    /// Create a new tracker with default settings
144    #[must_use]
145    pub fn new_default() -> Self {
146        Self::new(10000, 1000, 100) // 10k metrics, 1s slow, 100ms fast
147    }
148
149    /// Start tracking a query execution
150    #[must_use]
151    pub fn start_query(
152        &self,
153        query_type: &str,
154        query_text: &str,
155        parameters: Vec<String>,
156    ) -> QueryContext {
157        QueryContext {
158            query_id: Uuid::new_v4(),
159            query_type: query_type.to_string(),
160            query_text: query_text.to_string(),
161            parameters,
162            start_time: Instant::now(),
163            cache_hit: false,
164            result_size: None,
165        }
166    }
167
168    /// Complete query tracking with results
169    #[allow(clippy::cast_possible_truncation)]
170    pub fn complete_query(
171        &self,
172        context: QueryContext,
173        cache_hit: bool,
174        result_size: Option<usize>,
175        memory_usage_bytes: Option<u64>,
176        cpu_usage_percent: Option<f64>,
177        optimization_applied: Vec<String>,
178    ) {
179        let execution_time = context.start_time.elapsed();
180        let execution_time_ms = execution_time.as_millis() as u64;
181
182        let metric = QueryPerformanceMetrics {
183            query_id: context.query_id,
184            query_type: context.query_type.clone(),
185            query_text: context.query_text,
186            execution_time_ms,
187            cache_hit,
188            result_size,
189            memory_usage_bytes,
190            cpu_usage_percent,
191            timestamp: Utc::now(),
192            parameters: context.parameters,
193            optimization_applied,
194        };
195
196        // Add to metrics
197        {
198            let mut metrics = self.metrics.write();
199            metrics.push(metric.clone());
200
201            // Trim if we exceed max_metrics
202            if metrics.len() > self.max_metrics {
203                let excess = metrics.len() - self.max_metrics;
204                metrics.drain(0..excess);
205            }
206        }
207
208        // Update aggregated statistics
209        self.update_stats(&metric);
210
211        // Generate optimization suggestions if needed
212        self.generate_optimization_suggestions(&metric);
213
214        debug!(
215            "Query completed: {} ({}ms, cache_hit: {}, size: {:?})",
216            context.query_type, execution_time_ms, cache_hit, result_size
217        );
218    }
219
220    /// Get performance statistics for a specific query type
221    #[must_use]
222    pub fn get_stats(&self, query_type: &str) -> Option<QueryPerformanceStats> {
223        let stats = self.stats.read();
224        stats.get(query_type).cloned()
225    }
226
227    /// Get all performance statistics
228    #[must_use]
229    pub fn get_all_stats(&self) -> HashMap<String, QueryPerformanceStats> {
230        let stats = self.stats.read();
231        stats.clone()
232    }
233
234    /// Get optimization suggestions
235    #[must_use]
236    pub fn get_optimization_suggestions(&self) -> Vec<QueryOptimizationSuggestion> {
237        let suggestions = self.suggestions.read();
238        suggestions.clone()
239    }
240
241    /// Get slow queries (above threshold)
242    #[must_use]
243    pub fn get_slow_queries(&self) -> Vec<QueryPerformanceMetrics> {
244        let metrics = self.metrics.read();
245        metrics
246            .iter()
247            .filter(|m| m.execution_time_ms >= self.slow_query_threshold_ms)
248            .cloned()
249            .collect()
250    }
251
252    /// Get fast queries (below threshold)
253    #[must_use]
254    pub fn get_fast_queries(&self) -> Vec<QueryPerformanceMetrics> {
255        let metrics = self.metrics.read();
256        metrics
257            .iter()
258            .filter(|m| m.execution_time_ms <= self.fast_query_threshold_ms)
259            .cloned()
260            .collect()
261    }
262
263    /// Get query performance summary
264    #[must_use]
265    #[allow(clippy::cast_precision_loss)]
266    pub fn get_performance_summary(&self) -> QueryPerformanceSummary {
267        let stats = self.get_all_stats();
268        let suggestions = self.get_optimization_suggestions();
269        let slow_queries = self.get_slow_queries();
270        let fast_queries = self.get_fast_queries();
271
272        let total_queries: u64 = stats.values().map(|s| s.total_executions).sum();
273        let total_cache_hits: u64 = stats.values().map(|s| s.cache_hits).sum();
274        let overall_cache_hit_rate = if total_queries > 0 {
275            total_cache_hits as f64 / total_queries as f64
276        } else {
277            0.0
278        };
279
280        let average_execution_time = if stats.is_empty() {
281            0.0
282        } else {
283            stats
284                .values()
285                .map(|s| s.average_execution_time_ms)
286                .sum::<f64>()
287                / stats.len() as f64
288        };
289
290        QueryPerformanceSummary {
291            timestamp: Utc::now(),
292            total_queries,
293            overall_cache_hit_rate,
294            average_execution_time_ms: average_execution_time,
295            slow_queries_count: slow_queries.len() as u64,
296            fast_queries_count: fast_queries.len() as u64,
297            optimization_suggestions_count: suggestions.len() as u64,
298            stats,
299            suggestions,
300        }
301    }
302
303    /// Update aggregated statistics
304    #[allow(clippy::cast_precision_loss)]
305    fn update_stats(&self, metric: &QueryPerformanceMetrics) {
306        let mut stats = self.stats.write();
307        let entry =
308            stats
309                .entry(metric.query_type.clone())
310                .or_insert_with(|| QueryPerformanceStats {
311                    query_type: metric.query_type.clone(),
312                    total_executions: 0,
313                    cache_hits: 0,
314                    cache_misses: 0,
315                    average_execution_time_ms: 0.0,
316                    min_execution_time_ms: u64::MAX,
317                    max_execution_time_ms: 0,
318                    p95_execution_time_ms: 0,
319                    p99_execution_time_ms: 0,
320                    average_result_size: 0.0,
321                    total_memory_usage_bytes: 0,
322                    average_cpu_usage_percent: 0.0,
323                    cache_hit_rate: 0.0,
324                    slow_queries_count: 0,
325                    fast_queries_count: 0,
326                    last_executed: None,
327                });
328
329        entry.total_executions += 1;
330        entry.last_executed = Some(metric.timestamp);
331
332        if metric.cache_hit {
333            entry.cache_hits += 1;
334        } else {
335            entry.cache_misses += 1;
336        }
337
338        // Update execution time statistics
339        if metric.execution_time_ms < entry.min_execution_time_ms {
340            entry.min_execution_time_ms = metric.execution_time_ms;
341        }
342        if metric.execution_time_ms > entry.max_execution_time_ms {
343            entry.max_execution_time_ms = metric.execution_time_ms;
344        }
345
346        // Recalculate average execution time
347        entry.average_execution_time_ms = (entry.average_execution_time_ms
348            * (entry.total_executions - 1) as f64
349            + metric.execution_time_ms as f64)
350            / entry.total_executions as f64;
351
352        // Update result size statistics
353        if let Some(size) = metric.result_size {
354            entry.average_result_size =
355                (entry.average_result_size * (entry.total_executions - 1) as f64 + size as f64)
356                    / entry.total_executions as f64;
357        }
358
359        // Update memory usage
360        if let Some(memory) = metric.memory_usage_bytes {
361            entry.total_memory_usage_bytes += memory;
362        }
363
364        // Update CPU usage
365        if let Some(cpu) = metric.cpu_usage_percent {
366            entry.average_cpu_usage_percent =
367                (entry.average_cpu_usage_percent * (entry.total_executions - 1) as f64 + cpu)
368                    / entry.total_executions as f64;
369        }
370
371        // Update cache hit rate
372        entry.cache_hit_rate = if entry.total_executions > 0 {
373            entry.cache_hits as f64 / entry.total_executions as f64
374        } else {
375            0.0
376        };
377
378        // Update slow/fast query counts
379        if metric.execution_time_ms >= self.slow_query_threshold_ms {
380            entry.slow_queries_count += 1;
381        }
382        if metric.execution_time_ms <= self.fast_query_threshold_ms {
383            entry.fast_queries_count += 1;
384        }
385
386        // Calculate percentiles (simplified - in production, use proper percentile calculation)
387        self.calculate_percentiles(entry);
388    }
389
390    /// Calculate percentiles for execution time
391    #[allow(
392        clippy::cast_precision_loss,
393        clippy::cast_possible_truncation,
394        clippy::cast_sign_loss
395    )]
396    fn calculate_percentiles(&self, stats: &mut QueryPerformanceStats) {
397        // Get all execution times for this query type
398        let metrics = self.metrics.read();
399        let mut execution_times: Vec<u64> = metrics
400            .iter()
401            .filter(|m| m.query_type == stats.query_type)
402            .map(|m| m.execution_time_ms)
403            .collect();
404
405        execution_times.sort_unstable();
406
407        if !execution_times.is_empty() {
408            let len = execution_times.len();
409
410            // P95
411            let p95_index = (len as f64 * 0.95) as usize;
412            stats.p95_execution_time_ms = execution_times[p95_index.min(len - 1)];
413
414            // P99
415            let p99_index = (len as f64 * 0.99) as usize;
416            stats.p99_execution_time_ms = execution_times[p99_index.min(len - 1)];
417        }
418    }
419
420    /// Generate optimization suggestions based on query performance
421    fn generate_optimization_suggestions(&self, metric: &QueryPerformanceMetrics) {
422        let mut suggestions = self.suggestions.write();
423
424        // Remove existing suggestions for this query type
425        suggestions.retain(|s| s.query_type != metric.query_type);
426
427        let mut new_suggestions = Vec::new();
428
429        // Slow query suggestions
430        if metric.execution_time_ms >= self.slow_query_threshold_ms {
431            new_suggestions.push(QueryOptimizationSuggestion {
432                query_type: metric.query_type.clone(),
433                suggestion_type: OptimizationType::AddIndex,
434                description: format!(
435                    "Query is slow ({}ms). Consider adding database indexes.",
436                    metric.execution_time_ms
437                ),
438                potential_improvement_percent: 50.0,
439                priority: OptimizationPriority::High,
440                implementation_effort: ImplementationEffort::Medium,
441            });
442
443            new_suggestions.push(QueryOptimizationSuggestion {
444                query_type: metric.query_type.clone(),
445                suggestion_type: OptimizationType::OptimizeQuery,
446                description: "Query structure could be optimized for better performance."
447                    .to_string(),
448                potential_improvement_percent: 30.0,
449                priority: OptimizationPriority::Medium,
450                implementation_effort: ImplementationEffort::High,
451            });
452        }
453
454        // Low cache hit rate suggestions
455        if !metric.cache_hit {
456            new_suggestions.push(QueryOptimizationSuggestion {
457                query_type: metric.query_type.clone(),
458                suggestion_type: OptimizationType::AddCaching,
459                description:
460                    "Query is not cached. Consider implementing caching for better performance."
461                        .to_string(),
462                potential_improvement_percent: 80.0,
463                priority: OptimizationPriority::High,
464                implementation_effort: ImplementationEffort::Medium,
465            });
466        }
467
468        // Large result set suggestions
469        if let Some(size) = metric.result_size {
470            if size > 1000 {
471                new_suggestions.push(QueryOptimizationSuggestion {
472                    query_type: metric.query_type.clone(),
473                    suggestion_type: OptimizationType::ReduceResultSet,
474                    description: format!(
475                        "Large result set ({size} items). Consider pagination or filtering."
476                    ),
477                    potential_improvement_percent: 40.0,
478                    priority: OptimizationPriority::Medium,
479                    implementation_effort: ImplementationEffort::Low,
480                });
481            }
482        }
483
484        // High memory usage suggestions
485        if let Some(memory) = metric.memory_usage_bytes {
486            if memory > 10 * 1024 * 1024 {
487                // 10MB
488                new_suggestions.push(QueryOptimizationSuggestion {
489                    query_type: metric.query_type.clone(),
490                    suggestion_type: OptimizationType::UsePreparedStatement,
491                    description: "High memory usage. Consider using prepared statements."
492                        .to_string(),
493                    potential_improvement_percent: 20.0,
494                    priority: OptimizationPriority::Low,
495                    implementation_effort: ImplementationEffort::Low,
496                });
497            }
498        }
499
500        suggestions.extend(new_suggestions);
501    }
502}
503
504/// Query performance summary
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct QueryPerformanceSummary {
507    pub timestamp: DateTime<Utc>,
508    pub total_queries: u64,
509    pub overall_cache_hit_rate: f64,
510    pub average_execution_time_ms: f64,
511    pub slow_queries_count: u64,
512    pub fast_queries_count: u64,
513    pub optimization_suggestions_count: u64,
514    pub stats: HashMap<String, QueryPerformanceStats>,
515    pub suggestions: Vec<QueryOptimizationSuggestion>,
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use std::thread;
522    use std::time::Duration;
523
524    #[test]
525    fn test_query_performance_tracking() {
526        let tracker = QueryPerformanceTracker::new_default();
527
528        // Start a query
529        let context = tracker.start_query(
530            "test_query",
531            "SELECT * FROM tasks",
532            vec!["param1".to_string()],
533        );
534
535        // Simulate query execution time
536        thread::sleep(Duration::from_millis(100));
537
538        // Complete the query
539        tracker.complete_query(
540            context,
541            false,      // cache miss
542            Some(100),  // result size
543            Some(1024), // memory usage
544            Some(5.0),  // CPU usage
545            vec!["index_optimization".to_string()],
546        );
547
548        // Check statistics
549        let stats = tracker.get_stats("test_query");
550        assert!(stats.is_some());
551        let stats = stats.unwrap();
552        assert_eq!(stats.total_executions, 1);
553        assert_eq!(stats.cache_misses, 1);
554        assert_eq!(stats.cache_hits, 0);
555        assert!(stats.average_execution_time_ms >= 100.0);
556    }
557
558    #[test]
559    fn test_optimization_suggestions() {
560        let tracker = QueryPerformanceTracker::new(1000, 50, 10); // Very low thresholds for testing
561
562        // Start a slow query
563        let context = tracker.start_query("slow_query", "SELECT * FROM tasks", vec![]);
564        thread::sleep(Duration::from_millis(60)); // Above 50ms threshold
565        tracker.complete_query(context, false, Some(2000), None, None, vec![]);
566
567        // Check for optimization suggestions
568        let suggestions = tracker.get_optimization_suggestions();
569        assert!(!suggestions.is_empty());
570        assert!(suggestions.iter().any(|s| s.query_type == "slow_query"));
571    }
572
573    #[test]
574    fn test_performance_summary() {
575        let tracker = QueryPerformanceTracker::new_default();
576
577        // Execute some queries
578        for i in 0..5 {
579            let context = tracker.start_query("test_query", "SELECT * FROM tasks", vec![]);
580            thread::sleep(Duration::from_millis(10));
581            tracker.complete_query(
582                context,
583                i % 2 == 0, // Alternate cache hits/misses
584                Some(100),
585                None,
586                None,
587                vec![],
588            );
589        }
590
591        let summary = tracker.get_performance_summary();
592        assert_eq!(summary.total_queries, 5);
593        assert!(summary.overall_cache_hit_rate > 0.0);
594        assert!(summary.average_execution_time_ms > 0.0);
595    }
596}