kaccy_ai/
profiling.rs

1//! Performance profiling utilities for AI operations
2//!
3//! This module provides tools for monitoring, measuring, and optimizing
4//! AI service performance including latency, cost, and resource usage.
5
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use serde::{Deserialize, Serialize};
11
12/// Performance metrics for a single operation
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct OperationMetrics {
15    /// Operation name/identifier
16    pub operation: String,
17    /// Duration of the operation
18    pub duration: Duration,
19    /// Number of tokens used (if applicable)
20    pub tokens_used: Option<u32>,
21    /// Estimated cost in USD (if applicable)
22    pub estimated_cost: Option<f64>,
23    /// Whether the operation succeeded
24    pub success: bool,
25    /// Error message (if failed)
26    pub error: Option<String>,
27    /// Timestamp when the operation completed
28    pub timestamp: std::time::SystemTime,
29}
30
31impl OperationMetrics {
32    /// Create new operation metrics
33    #[must_use]
34    pub fn new(operation: String, duration: Duration, success: bool) -> Self {
35        Self {
36            operation,
37            duration,
38            tokens_used: None,
39            estimated_cost: None,
40            success,
41            error: None,
42            timestamp: std::time::SystemTime::now(),
43        }
44    }
45
46    /// Set token usage
47    #[must_use]
48    pub fn with_tokens(mut self, tokens: u32) -> Self {
49        self.tokens_used = Some(tokens);
50        self
51    }
52
53    /// Set estimated cost
54    #[must_use]
55    pub fn with_cost(mut self, cost: f64) -> Self {
56        self.estimated_cost = Some(cost);
57        self
58    }
59
60    /// Set error message
61    #[must_use]
62    pub fn with_error(mut self, error: String) -> Self {
63        self.error = Some(error);
64        self
65    }
66}
67
68/// Aggregated statistics for an operation type
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct OperationStats {
71    /// Total number of operations
72    pub total_count: u64,
73    /// Number of successful operations
74    pub success_count: u64,
75    /// Number of failed operations
76    pub failure_count: u64,
77    /// Average duration
78    pub avg_duration: Duration,
79    /// Minimum duration
80    pub min_duration: Duration,
81    /// Maximum duration
82    pub max_duration: Duration,
83    /// Total tokens used
84    pub total_tokens: u64,
85    /// Total estimated cost
86    pub total_cost: f64,
87    /// Success rate (0.0 to 1.0)
88    pub success_rate: f64,
89}
90
91impl Default for OperationStats {
92    fn default() -> Self {
93        Self {
94            total_count: 0,
95            success_count: 0,
96            failure_count: 0,
97            avg_duration: Duration::ZERO,
98            min_duration: Duration::MAX,
99            max_duration: Duration::ZERO,
100            total_tokens: 0,
101            total_cost: 0.0,
102            success_rate: 0.0,
103        }
104    }
105}
106
107impl OperationStats {
108    /// Update stats with a new operation metric
109    fn update(&mut self, metric: &OperationMetrics) {
110        self.total_count += 1;
111
112        if metric.success {
113            self.success_count += 1;
114        } else {
115            self.failure_count += 1;
116        }
117
118        // Update duration stats
119        if metric.duration < self.min_duration {
120            self.min_duration = metric.duration;
121        }
122        if metric.duration > self.max_duration {
123            self.max_duration = metric.duration;
124        }
125
126        // Update average (incremental mean)
127        let total_ms = self.avg_duration.as_millis() as u64 * (self.total_count - 1)
128            + metric.duration.as_millis() as u64;
129        self.avg_duration = Duration::from_millis(total_ms / self.total_count);
130
131        // Update tokens and cost
132        if let Some(tokens) = metric.tokens_used {
133            self.total_tokens += u64::from(tokens);
134        }
135        if let Some(cost) = metric.estimated_cost {
136            self.total_cost += cost;
137        }
138
139        // Update success rate
140        self.success_rate = self.success_count as f64 / self.total_count as f64;
141    }
142
143    /// Get average cost per operation
144    #[must_use]
145    pub fn avg_cost(&self) -> f64 {
146        if self.total_count == 0 {
147            0.0
148        } else {
149            self.total_cost / self.total_count as f64
150        }
151    }
152
153    /// Get average tokens per operation
154    #[must_use]
155    pub fn avg_tokens(&self) -> f64 {
156        if self.total_count == 0 {
157            0.0
158        } else {
159            self.total_tokens as f64 / self.total_count as f64
160        }
161    }
162}
163
164/// Performance profiler for tracking AI operations
165pub struct PerformanceProfiler {
166    /// All recorded metrics
167    metrics: Arc<Mutex<Vec<OperationMetrics>>>,
168    /// Aggregated statistics per operation type
169    stats: Arc<Mutex<HashMap<String, OperationStats>>>,
170    /// Whether profiling is enabled
171    enabled: bool,
172}
173
174impl Default for PerformanceProfiler {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180impl PerformanceProfiler {
181    /// Create a new performance profiler
182    #[must_use]
183    pub fn new() -> Self {
184        Self {
185            metrics: Arc::new(Mutex::new(Vec::new())),
186            stats: Arc::new(Mutex::new(HashMap::new())),
187            enabled: true,
188        }
189    }
190
191    /// Enable profiling
192    pub fn enable(&mut self) {
193        self.enabled = true;
194    }
195
196    /// Disable profiling
197    pub fn disable(&mut self) {
198        self.enabled = false;
199    }
200
201    /// Check if profiling is enabled
202    #[must_use]
203    pub fn is_enabled(&self) -> bool {
204        self.enabled
205    }
206
207    /// Record an operation metric
208    pub fn record(&self, metric: OperationMetrics) {
209        if !self.enabled {
210            return;
211        }
212
213        // Update aggregated stats
214        if let Ok(mut stats) = self.stats.lock() {
215            let operation_stats = stats
216                .entry(metric.operation.clone())
217                .or_insert_with(OperationStats::default);
218            operation_stats.update(&metric);
219        }
220
221        // Store the metric
222        if let Ok(mut metrics) = self.metrics.lock() {
223            metrics.push(metric);
224        }
225    }
226
227    /// Get statistics for a specific operation
228    #[must_use]
229    pub fn get_stats(&self, operation: &str) -> Option<OperationStats> {
230        self.stats
231            .lock()
232            .ok()
233            .and_then(|stats| stats.get(operation).cloned())
234    }
235
236    /// Get statistics for all operations
237    #[must_use]
238    pub fn get_all_stats(&self) -> HashMap<String, OperationStats> {
239        self.stats
240            .lock()
241            .ok()
242            .map(|s| s.clone())
243            .unwrap_or_default()
244    }
245
246    /// Get all recorded metrics
247    #[must_use]
248    pub fn get_all_metrics(&self) -> Vec<OperationMetrics> {
249        self.metrics
250            .lock()
251            .ok()
252            .map(|m| m.clone())
253            .unwrap_or_default()
254    }
255
256    /// Clear all recorded data
257    pub fn clear(&self) {
258        if let Ok(mut metrics) = self.metrics.lock() {
259            metrics.clear();
260        }
261        if let Ok(mut stats) = self.stats.lock() {
262            stats.clear();
263        }
264    }
265
266    /// Get total operations count
267    #[must_use]
268    pub fn total_operations(&self) -> u64 {
269        self.stats
270            .lock()
271            .ok()
272            .map_or(0, |s| s.values().map(|stat| stat.total_count).sum())
273    }
274
275    /// Get total cost across all operations
276    #[must_use]
277    pub fn total_cost(&self) -> f64 {
278        self.stats
279            .lock()
280            .ok()
281            .map_or(0.0, |s| s.values().map(|stat| stat.total_cost).sum())
282    }
283
284    /// Get total tokens used across all operations
285    #[must_use]
286    pub fn total_tokens(&self) -> u64 {
287        self.stats
288            .lock()
289            .ok()
290            .map_or(0, |s| s.values().map(|stat| stat.total_tokens).sum())
291    }
292
293    /// Generate a performance report
294    #[must_use]
295    pub fn generate_report(&self) -> PerformanceReport {
296        let stats = self.get_all_stats();
297        let total_ops = self.total_operations();
298        let total_cost = self.total_cost();
299        let total_tokens = self.total_tokens();
300
301        let overall_success_rate = if total_ops > 0 {
302            stats.values().map(|s| s.success_count).sum::<u64>() as f64 / total_ops as f64
303        } else {
304            0.0
305        };
306
307        PerformanceReport {
308            total_operations: total_ops,
309            total_cost,
310            total_tokens,
311            overall_success_rate,
312            operation_stats: stats,
313        }
314    }
315}
316
317/// Complete performance report
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct PerformanceReport {
320    /// Total number of operations across all types
321    pub total_operations: u64,
322    /// Total cost across all operations
323    pub total_cost: f64,
324    /// Total tokens used across all operations
325    pub total_tokens: u64,
326    /// Overall success rate
327    pub overall_success_rate: f64,
328    /// Statistics per operation type
329    pub operation_stats: HashMap<String, OperationStats>,
330}
331
332impl PerformanceReport {
333    /// Print a human-readable report
334    pub fn print(&self) {
335        println!("=== Performance Report ===");
336        println!("Total Operations: {}", self.total_operations);
337        println!("Total Cost: ${:.4}", self.total_cost);
338        println!("Total Tokens: {}", self.total_tokens);
339        println!(
340            "Overall Success Rate: {:.2}%",
341            self.overall_success_rate * 100.0
342        );
343        println!("\nPer-Operation Statistics:");
344
345        for (operation, stats) in &self.operation_stats {
346            println!("\n  {operation}:");
347            println!("    Count: {}", stats.total_count);
348            println!("    Success Rate: {:.2}%", stats.success_rate * 100.0);
349            println!("    Avg Duration: {:?}", stats.avg_duration);
350            println!("    Min Duration: {:?}", stats.min_duration);
351            println!("    Max Duration: {:?}", stats.max_duration);
352            println!("    Avg Cost: ${:.6}", stats.avg_cost());
353            println!("    Avg Tokens: {:.1}", stats.avg_tokens());
354        }
355    }
356}
357
358/// Scoped profiler for automatic timing
359pub struct ScopedProfiler {
360    operation: String,
361    start: Instant,
362    profiler: Arc<PerformanceProfiler>,
363    tokens: Option<u32>,
364    cost: Option<f64>,
365}
366
367impl ScopedProfiler {
368    /// Create a new scoped profiler
369    #[must_use]
370    pub fn new(operation: String, profiler: Arc<PerformanceProfiler>) -> Self {
371        Self {
372            operation,
373            start: Instant::now(),
374            profiler,
375            tokens: None,
376            cost: None,
377        }
378    }
379
380    /// Set token usage
381    pub fn set_tokens(&mut self, tokens: u32) {
382        self.tokens = Some(tokens);
383    }
384
385    /// Set cost
386    pub fn set_cost(&mut self, cost: f64) {
387        self.cost = Some(cost);
388    }
389
390    /// Complete with success
391    pub fn complete_success(self) {
392        self.complete(true, None);
393    }
394
395    /// Complete with error
396    pub fn complete_error(self, error: String) {
397        self.complete(false, Some(error));
398    }
399
400    /// Internal completion
401    fn complete(self, success: bool, error: Option<String>) {
402        let duration = self.start.elapsed();
403        let mut metric = OperationMetrics::new(self.operation.clone(), duration, success);
404
405        if let Some(tokens) = self.tokens {
406            metric = metric.with_tokens(tokens);
407        }
408        if let Some(cost) = self.cost {
409            metric = metric.with_cost(cost);
410        }
411        if let Some(err) = error {
412            metric = metric.with_error(err);
413        }
414
415        self.profiler.record(metric);
416    }
417}
418
419impl Drop for ScopedProfiler {
420    fn drop(&mut self) {
421        // Auto-complete as success if not explicitly completed
422        let duration = self.start.elapsed();
423        let mut metric = OperationMetrics::new(self.operation.clone(), duration, true);
424
425        if let Some(tokens) = self.tokens {
426            metric = metric.with_tokens(tokens);
427        }
428        if let Some(cost) = self.cost {
429            metric = metric.with_cost(cost);
430        }
431
432        self.profiler.record(metric);
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn test_operation_metrics_creation() {
442        let metric = OperationMetrics::new("test_op".to_string(), Duration::from_millis(100), true)
443            .with_tokens(500)
444            .with_cost(0.01);
445
446        assert_eq!(metric.operation, "test_op");
447        assert_eq!(metric.duration, Duration::from_millis(100));
448        assert!(metric.success);
449        assert_eq!(metric.tokens_used, Some(500));
450        assert_eq!(metric.estimated_cost, Some(0.01));
451    }
452
453    #[test]
454    fn test_operation_stats_update() {
455        let mut stats = OperationStats::default();
456
457        let metric1 = OperationMetrics::new("test".to_string(), Duration::from_millis(100), true)
458            .with_tokens(500)
459            .with_cost(0.01);
460
461        let metric2 = OperationMetrics::new("test".to_string(), Duration::from_millis(200), true)
462            .with_tokens(600)
463            .with_cost(0.02);
464
465        stats.update(&metric1);
466        stats.update(&metric2);
467
468        assert_eq!(stats.total_count, 2);
469        assert_eq!(stats.success_count, 2);
470        assert_eq!(stats.total_tokens, 1100);
471        assert_eq!(stats.total_cost, 0.03);
472        assert_eq!(stats.success_rate, 1.0);
473    }
474
475    #[test]
476    fn test_profiler_record() {
477        let profiler = PerformanceProfiler::new();
478
479        let metric = OperationMetrics::new("eval".to_string(), Duration::from_millis(100), true)
480            .with_tokens(500)
481            .with_cost(0.01);
482
483        profiler.record(metric);
484
485        assert_eq!(profiler.total_operations(), 1);
486        assert_eq!(profiler.total_tokens(), 500);
487        assert_eq!(profiler.total_cost(), 0.01);
488    }
489
490    #[test]
491    fn test_profiler_stats() {
492        let profiler = PerformanceProfiler::new();
493
494        profiler.record(OperationMetrics::new(
495            "op1".to_string(),
496            Duration::from_millis(100),
497            true,
498        ));
499        profiler.record(OperationMetrics::new(
500            "op1".to_string(),
501            Duration::from_millis(200),
502            true,
503        ));
504        profiler.record(OperationMetrics::new(
505            "op2".to_string(),
506            Duration::from_millis(150),
507            true,
508        ));
509
510        let stats = profiler.get_stats("op1").unwrap();
511        assert_eq!(stats.total_count, 2);
512        assert_eq!(stats.success_count, 2);
513
514        assert_eq!(profiler.total_operations(), 3);
515    }
516
517    #[test]
518    fn test_profiler_clear() {
519        let profiler = PerformanceProfiler::new();
520
521        profiler.record(OperationMetrics::new(
522            "test".to_string(),
523            Duration::from_millis(100),
524            true,
525        ));
526
527        assert_eq!(profiler.total_operations(), 1);
528
529        profiler.clear();
530
531        assert_eq!(profiler.total_operations(), 0);
532    }
533
534    #[test]
535    fn test_performance_report() {
536        let profiler = PerformanceProfiler::new();
537
538        profiler.record(
539            OperationMetrics::new("eval".to_string(), Duration::from_millis(100), true)
540                .with_tokens(500)
541                .with_cost(0.01),
542        );
543
544        let report = profiler.generate_report();
545
546        assert_eq!(report.total_operations, 1);
547        assert_eq!(report.total_tokens, 500);
548        assert_eq!(report.total_cost, 0.01);
549        assert_eq!(report.overall_success_rate, 1.0);
550    }
551
552    #[test]
553    fn test_profiler_enable_disable() {
554        let mut profiler = PerformanceProfiler::new();
555
556        profiler.disable();
557        assert!(!profiler.is_enabled());
558
559        profiler.record(OperationMetrics::new(
560            "test".to_string(),
561            Duration::from_millis(100),
562            true,
563        ));
564
565        assert_eq!(profiler.total_operations(), 0);
566
567        profiler.enable();
568        assert!(profiler.is_enabled());
569
570        profiler.record(OperationMetrics::new(
571            "test".to_string(),
572            Duration::from_millis(100),
573            true,
574        ));
575
576        assert_eq!(profiler.total_operations(), 1);
577    }
578}