1use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct OperationMetrics {
15 pub operation: String,
17 pub duration: Duration,
19 pub tokens_used: Option<u32>,
21 pub estimated_cost: Option<f64>,
23 pub success: bool,
25 pub error: Option<String>,
27 pub timestamp: std::time::SystemTime,
29}
30
31impl OperationMetrics {
32 #[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 #[must_use]
48 pub fn with_tokens(mut self, tokens: u32) -> Self {
49 self.tokens_used = Some(tokens);
50 self
51 }
52
53 #[must_use]
55 pub fn with_cost(mut self, cost: f64) -> Self {
56 self.estimated_cost = Some(cost);
57 self
58 }
59
60 #[must_use]
62 pub fn with_error(mut self, error: String) -> Self {
63 self.error = Some(error);
64 self
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct OperationStats {
71 pub total_count: u64,
73 pub success_count: u64,
75 pub failure_count: u64,
77 pub avg_duration: Duration,
79 pub min_duration: Duration,
81 pub max_duration: Duration,
83 pub total_tokens: u64,
85 pub total_cost: f64,
87 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 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 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 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 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 self.success_rate = self.success_count as f64 / self.total_count as f64;
141 }
142
143 #[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 #[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
164pub struct PerformanceProfiler {
166 metrics: Arc<Mutex<Vec<OperationMetrics>>>,
168 stats: Arc<Mutex<HashMap<String, OperationStats>>>,
170 enabled: bool,
172}
173
174impl Default for PerformanceProfiler {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180impl PerformanceProfiler {
181 #[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 pub fn enable(&mut self) {
193 self.enabled = true;
194 }
195
196 pub fn disable(&mut self) {
198 self.enabled = false;
199 }
200
201 #[must_use]
203 pub fn is_enabled(&self) -> bool {
204 self.enabled
205 }
206
207 pub fn record(&self, metric: OperationMetrics) {
209 if !self.enabled {
210 return;
211 }
212
213 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 if let Ok(mut metrics) = self.metrics.lock() {
223 metrics.push(metric);
224 }
225 }
226
227 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct PerformanceReport {
320 pub total_operations: u64,
322 pub total_cost: f64,
324 pub total_tokens: u64,
326 pub overall_success_rate: f64,
328 pub operation_stats: HashMap<String, OperationStats>,
330}
331
332impl PerformanceReport {
333 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
358pub 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 #[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 pub fn set_tokens(&mut self, tokens: u32) {
382 self.tokens = Some(tokens);
383 }
384
385 pub fn set_cost(&mut self, cost: f64) {
387 self.cost = Some(cost);
388 }
389
390 pub fn complete_success(self) {
392 self.complete(true, None);
393 }
394
395 pub fn complete_error(self, error: String) {
397 self.complete(false, Some(error));
398 }
399
400 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 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}