oxirs_vec/sparql_integration/
monitoring.rs

1//! Performance monitoring and statistics for SPARQL vector operations
2
3use parking_lot::RwLock;
4use std::collections::HashMap;
5use std::sync::Arc;
6use std::time::Duration;
7
8/// Performance monitoring for vector operations
9#[derive(Debug, Clone)]
10pub struct PerformanceMonitor {
11    pub query_stats: Arc<RwLock<QueryStats>>,
12    pub operation_timings: Arc<RwLock<HashMap<String, Vec<Duration>>>>,
13    pub cache_stats: Arc<RwLock<CacheStats>>,
14}
15
16#[derive(Debug, Clone, Default)]
17pub struct QueryStats {
18    pub total_queries: u64,
19    pub successful_queries: u64,
20    pub failed_queries: u64,
21    pub avg_response_time: Duration,
22    pub max_response_time: Duration,
23    pub min_response_time: Duration,
24}
25
26#[derive(Debug, Clone, Default)]
27pub struct CacheStats {
28    pub cache_hits: u64,
29    pub cache_misses: u64,
30    pub cache_size: usize,
31    pub cache_capacity: usize,
32}
33
34impl PerformanceMonitor {
35    pub fn new() -> Self {
36        Self {
37            query_stats: Arc::new(RwLock::new(QueryStats::default())),
38            operation_timings: Arc::new(RwLock::new(HashMap::new())),
39            cache_stats: Arc::new(RwLock::new(CacheStats::default())),
40        }
41    }
42
43    pub fn record_query(&self, duration: Duration, success: bool) {
44        let mut stats = self.query_stats.write();
45        stats.total_queries += 1;
46
47        if success {
48            stats.successful_queries += 1;
49        } else {
50            stats.failed_queries += 1;
51        }
52
53        if stats.total_queries == 1 {
54            stats.avg_response_time = duration;
55            stats.max_response_time = duration;
56            stats.min_response_time = duration;
57        } else {
58            // Update running average
59            let total_time = stats
60                .avg_response_time
61                .mul_f64(stats.total_queries as f64 - 1.0)
62                + duration;
63            stats.avg_response_time = total_time.div_f64(stats.total_queries as f64);
64
65            if duration > stats.max_response_time {
66                stats.max_response_time = duration;
67            }
68            if duration < stats.min_response_time {
69                stats.min_response_time = duration;
70            }
71        }
72    }
73
74    pub fn record_operation(&self, operation: &str, duration: Duration) {
75        let mut timings = self.operation_timings.write();
76        timings
77            .entry(operation.to_string())
78            .or_default()
79            .push(duration);
80    }
81
82    pub fn record_cache_hit(&self) {
83        let mut stats = self.cache_stats.write();
84        stats.cache_hits += 1;
85    }
86
87    pub fn record_cache_miss(&self) {
88        let mut stats = self.cache_stats.write();
89        stats.cache_misses += 1;
90    }
91
92    pub fn update_cache_size(&self, size: usize, capacity: usize) {
93        let mut stats = self.cache_stats.write();
94        stats.cache_size = size;
95        stats.cache_capacity = capacity;
96    }
97
98    pub fn get_stats(&self) -> (QueryStats, CacheStats) {
99        let query_stats = self.query_stats.read().clone();
100        let cache_stats = self.cache_stats.read().clone();
101        (query_stats, cache_stats)
102    }
103
104    pub fn get_operation_timings(&self) -> HashMap<String, Vec<Duration>> {
105        self.operation_timings.read().clone()
106    }
107
108    pub fn reset_stats(&self) {
109        *self.query_stats.write() = QueryStats::default();
110        *self.operation_timings.write() = HashMap::new();
111        *self.cache_stats.write() = CacheStats::default();
112    }
113
114    /// Generate a comprehensive performance report
115    pub fn generate_report(&self) -> PerformanceReport {
116        let (query_stats, cache_stats) = self.get_stats();
117        let operation_timings = self.get_operation_timings();
118
119        let mut operation_summaries = HashMap::new();
120        for (op, timings) in operation_timings {
121            if !timings.is_empty() {
122                let total_time: Duration = timings.iter().sum();
123                let avg_time = total_time / timings.len() as u32;
124                let max_time = timings.iter().max().copied().unwrap_or_default();
125                let min_time = timings.iter().min().copied().unwrap_or_default();
126
127                operation_summaries.insert(
128                    op,
129                    OperationSummary {
130                        count: timings.len(),
131                        total_time,
132                        avg_time,
133                        max_time,
134                        min_time,
135                    },
136                );
137            }
138        }
139
140        PerformanceReport {
141            query_stats,
142            cache_stats,
143            operation_summaries,
144            report_time: std::time::SystemTime::now(),
145        }
146    }
147}
148
149impl Default for PerformanceMonitor {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155/// Performance report containing comprehensive statistics
156#[derive(Debug, Clone)]
157pub struct PerformanceReport {
158    pub query_stats: QueryStats,
159    pub cache_stats: CacheStats,
160    pub operation_summaries: HashMap<String, OperationSummary>,
161    pub report_time: std::time::SystemTime,
162}
163
164#[derive(Debug, Clone)]
165pub struct OperationSummary {
166    pub count: usize,
167    pub total_time: Duration,
168    pub avg_time: Duration,
169    pub max_time: Duration,
170    pub min_time: Duration,
171}
172
173impl PerformanceReport {
174    /// Get cache hit ratio as a percentage
175    pub fn cache_hit_ratio(&self) -> f64 {
176        let total_requests = self.cache_stats.cache_hits + self.cache_stats.cache_misses;
177        if total_requests == 0 {
178            0.0
179        } else {
180            (self.cache_stats.cache_hits as f64 / total_requests as f64) * 100.0
181        }
182    }
183
184    /// Get query success ratio as a percentage
185    pub fn query_success_ratio(&self) -> f64 {
186        if self.query_stats.total_queries == 0 {
187            0.0
188        } else {
189            (self.query_stats.successful_queries as f64 / self.query_stats.total_queries as f64)
190                * 100.0
191        }
192    }
193
194    /// Format the report as a human-readable string
195    pub fn format_summary(&self) -> String {
196        format!(
197            "Performance Report (generated at {:?})\n\
198            Query Statistics:\n\
199            - Total queries: {}\n\
200            - Successful: {} ({:.1}%)\n\
201            - Failed: {}\n\
202            - Avg response time: {:?}\n\
203            - Min/Max response time: {:?}/{:?}\n\
204            Cache Statistics:\n\
205            - Cache hits: {}\n\
206            - Cache misses: {}\n\
207            - Hit ratio: {:.1}%\n\
208            - Cache utilization: {}/{} ({:.1}%)\n\
209            Operation Summaries: {} operations tracked",
210            self.report_time,
211            self.query_stats.total_queries,
212            self.query_stats.successful_queries,
213            self.query_success_ratio(),
214            self.query_stats.failed_queries,
215            self.query_stats.avg_response_time,
216            self.query_stats.min_response_time,
217            self.query_stats.max_response_time,
218            self.cache_stats.cache_hits,
219            self.cache_stats.cache_misses,
220            self.cache_hit_ratio(),
221            self.cache_stats.cache_size,
222            self.cache_stats.cache_capacity,
223            if self.cache_stats.cache_capacity > 0 {
224                (self.cache_stats.cache_size as f64 / self.cache_stats.cache_capacity as f64)
225                    * 100.0
226            } else {
227                0.0
228            },
229            self.operation_summaries.len()
230        )
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use std::time::Duration;
238
239    #[test]
240    fn test_performance_monitor_basic() {
241        let monitor = PerformanceMonitor::new();
242
243        // Record some queries
244        monitor.record_query(Duration::from_millis(100), true);
245        monitor.record_query(Duration::from_millis(200), true);
246        monitor.record_query(Duration::from_millis(50), false);
247
248        let (query_stats, _) = monitor.get_stats();
249        assert_eq!(query_stats.total_queries, 3);
250        assert_eq!(query_stats.successful_queries, 2);
251        assert_eq!(query_stats.failed_queries, 1);
252    }
253
254    #[test]
255    fn test_cache_stats() {
256        let monitor = PerformanceMonitor::new();
257
258        monitor.record_cache_hit();
259        monitor.record_cache_hit();
260        monitor.record_cache_miss();
261
262        let report = monitor.generate_report();
263        assert_eq!(report.cache_stats.cache_hits, 2);
264        assert_eq!(report.cache_stats.cache_misses, 1);
265        assert!((report.cache_hit_ratio() - 66.67).abs() < 0.1);
266    }
267
268    #[test]
269    fn test_operation_timings() {
270        let monitor = PerformanceMonitor::new();
271
272        monitor.record_operation("search", Duration::from_millis(100));
273        monitor.record_operation("search", Duration::from_millis(200));
274        monitor.record_operation("embed", Duration::from_millis(50));
275
276        let report = monitor.generate_report();
277        assert_eq!(report.operation_summaries.len(), 2);
278
279        let search_summary = report.operation_summaries.get("search").unwrap();
280        assert_eq!(search_summary.count, 2);
281        assert_eq!(search_summary.avg_time, Duration::from_millis(150));
282    }
283}