oxify_connect_vision/
benchmark.rs

1//! Benchmark and comparison utilities.
2//!
3//! Provides tools for comparing provider performance,
4//! generating benchmark reports, and profiling memory usage.
5
6use crate::providers::VisionProvider;
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
9use std::time::Instant;
10
11/// Benchmark result for a single operation.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct BenchmarkResult {
14    /// Provider name
15    pub provider: String,
16    /// Number of iterations
17    pub iterations: usize,
18    /// Minimum latency (milliseconds)
19    pub min_ms: f64,
20    /// Maximum latency (milliseconds)
21    pub max_ms: f64,
22    /// Mean latency (milliseconds)
23    pub mean_ms: f64,
24    /// Median latency (milliseconds)
25    pub median_ms: f64,
26    /// Standard deviation (milliseconds)
27    pub std_dev_ms: f64,
28    /// Percentile 95 (milliseconds)
29    pub p95_ms: f64,
30    /// Percentile 99 (milliseconds)
31    pub p99_ms: f64,
32    /// Total time (milliseconds)
33    pub total_ms: f64,
34    /// Operations per second
35    pub ops_per_sec: f64,
36}
37
38impl BenchmarkResult {
39    /// Create from timing measurements.
40    pub fn from_timings(provider: String, mut timings: Vec<f64>) -> Self {
41        if timings.is_empty() {
42            return Self::empty(provider);
43        }
44
45        timings.sort_by(|a, b| a.partial_cmp(b).unwrap());
46
47        let iterations = timings.len();
48        let min_ms = timings[0];
49        let max_ms = timings[timings.len() - 1];
50        let total_ms: f64 = timings.iter().sum();
51        let mean_ms = total_ms / iterations as f64;
52
53        let median_ms = if iterations.is_multiple_of(2) {
54            (timings[iterations / 2 - 1] + timings[iterations / 2]) / 2.0
55        } else {
56            timings[iterations / 2]
57        };
58
59        let variance: f64 = timings
60            .iter()
61            .map(|t| {
62                let diff = t - mean_ms;
63                diff * diff
64            })
65            .sum::<f64>()
66            / iterations as f64;
67        let std_dev_ms = variance.sqrt();
68
69        let p95_idx = ((iterations as f64 * 0.95) as usize).min(iterations - 1);
70        let p99_idx = ((iterations as f64 * 0.99) as usize).min(iterations - 1);
71
72        let p95_ms = timings[p95_idx];
73        let p99_ms = timings[p99_idx];
74
75        let ops_per_sec = if mean_ms > 0.0 { 1000.0 / mean_ms } else { 0.0 };
76
77        Self {
78            provider,
79            iterations,
80            min_ms,
81            max_ms,
82            mean_ms,
83            median_ms,
84            std_dev_ms,
85            p95_ms,
86            p99_ms,
87            total_ms,
88            ops_per_sec,
89        }
90    }
91
92    /// Create an empty result.
93    fn empty(provider: String) -> Self {
94        Self {
95            provider,
96            iterations: 0,
97            min_ms: 0.0,
98            max_ms: 0.0,
99            mean_ms: 0.0,
100            median_ms: 0.0,
101            std_dev_ms: 0.0,
102            p95_ms: 0.0,
103            p99_ms: 0.0,
104            total_ms: 0.0,
105            ops_per_sec: 0.0,
106        }
107    }
108
109    /// Format as a table row.
110    pub fn format_row(&self) -> String {
111        format!(
112            "{:<12} {:>8} {:>8.2} {:>8.2} {:>8.2} {:>8.2} {:>8.2}",
113            self.provider,
114            self.iterations,
115            self.mean_ms,
116            self.median_ms,
117            self.p95_ms,
118            self.p99_ms,
119            self.ops_per_sec
120        )
121    }
122}
123
124/// Comparison report for multiple providers.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ComparisonReport {
127    /// Benchmark results for each provider
128    pub results: Vec<BenchmarkResult>,
129    /// Test image size in bytes
130    pub image_size_bytes: usize,
131    /// Report timestamp
132    pub timestamp: String,
133}
134
135impl ComparisonReport {
136    /// Create a new comparison report.
137    pub fn new(results: Vec<BenchmarkResult>, image_size_bytes: usize) -> Self {
138        Self {
139            results,
140            image_size_bytes,
141            timestamp: chrono::Utc::now().to_rfc3339(),
142        }
143    }
144
145    /// Format as a readable table.
146    pub fn format_table(&self) -> String {
147        let mut output = String::new();
148
149        output.push('\n');
150        output.push_str("═══════════════════════════════════════════════════════════════════\n");
151        output.push_str("  PROVIDER PERFORMANCE COMPARISON\n");
152        output.push_str("═══════════════════════════════════════════════════════════════════\n");
153        output.push_str(&format!("Image Size: {} bytes\n", self.image_size_bytes));
154        output.push_str(&format!("Timestamp:  {}\n", self.timestamp));
155        output.push_str("───────────────────────────────────────────────────────────────────\n");
156        output.push_str(&format!(
157            "{:<12} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8}\n",
158            "Provider", "Iters", "Mean", "Median", "P95", "P99", "Ops/sec"
159        ));
160        output.push_str(&format!(
161            "{:<12} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8}\n",
162            "", "", "(ms)", "(ms)", "(ms)", "(ms)", ""
163        ));
164        output.push_str("───────────────────────────────────────────────────────────────────\n");
165
166        for result in &self.results {
167            output.push_str(&result.format_row());
168            output.push('\n');
169        }
170
171        output.push_str("═══════════════════════════════════════════════════════════════════\n");
172
173        output
174    }
175
176    /// Get the fastest provider.
177    pub fn fastest_provider(&self) -> Option<&BenchmarkResult> {
178        self.results
179            .iter()
180            .min_by(|a, b| a.mean_ms.partial_cmp(&b.mean_ms).unwrap())
181    }
182
183    /// Save report as JSON.
184    pub fn save_json(&self, path: &str) -> crate::Result<()> {
185        let json = serde_json::to_string_pretty(self).map_err(|e| {
186            crate::VisionError::config(format!("Failed to serialize report: {}", e))
187        })?;
188
189        std::fs::write(path, json)
190            .map_err(|e| crate::VisionError::config(format!("Failed to write report: {}", e)))?;
191
192        Ok(())
193    }
194}
195
196/// Memory profiling result.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct MemoryProfile {
199    /// Provider name
200    pub provider: String,
201    /// Peak memory usage (bytes)
202    pub peak_bytes: usize,
203    /// Average memory usage (bytes)
204    pub avg_bytes: usize,
205    /// Memory usage per operation (bytes)
206    pub bytes_per_op: usize,
207}
208
209impl MemoryProfile {
210    /// Format as human-readable string.
211    pub fn format(&self) -> String {
212        format!(
213            "Provider: {}\n  Peak: {} MB\n  Average: {} MB\n  Per Operation: {} KB",
214            self.provider,
215            self.peak_bytes / (1024 * 1024),
216            self.avg_bytes / (1024 * 1024),
217            self.bytes_per_op / 1024
218        )
219    }
220}
221
222/// Benchmark runner for comparing providers.
223pub struct BenchmarkRunner {
224    iterations: usize,
225    warmup_iterations: usize,
226}
227
228impl Default for BenchmarkRunner {
229    fn default() -> Self {
230        Self {
231            iterations: 10,
232            warmup_iterations: 2,
233        }
234    }
235}
236
237impl BenchmarkRunner {
238    /// Create a new benchmark runner.
239    pub fn new(iterations: usize) -> Self {
240        Self {
241            iterations,
242            warmup_iterations: iterations.min(2),
243        }
244    }
245
246    /// Set warmup iterations.
247    pub fn with_warmup(mut self, warmup: usize) -> Self {
248        self.warmup_iterations = warmup;
249        self
250    }
251
252    /// Run benchmark for a single provider.
253    pub async fn benchmark_provider(
254        &self,
255        provider: Arc<dyn VisionProvider>,
256        image_data: &[u8],
257    ) -> crate::Result<BenchmarkResult> {
258        let provider_name = provider.provider_name().to_string();
259
260        // Warmup
261        for _ in 0..self.warmup_iterations {
262            let _ = provider.process_image(image_data).await;
263        }
264
265        // Benchmark
266        let mut timings = Vec::with_capacity(self.iterations);
267
268        for _ in 0..self.iterations {
269            let start = Instant::now();
270            let _ = provider.process_image(image_data).await?;
271            let elapsed = start.elapsed();
272            timings.push(elapsed.as_secs_f64() * 1000.0);
273        }
274
275        Ok(BenchmarkResult::from_timings(provider_name, timings))
276    }
277
278    /// Compare multiple providers.
279    pub async fn compare_providers(
280        &self,
281        providers: Vec<Arc<dyn VisionProvider>>,
282        image_data: &[u8],
283    ) -> crate::Result<ComparisonReport> {
284        let mut results = Vec::new();
285
286        for provider in providers {
287            let result = self.benchmark_provider(provider, image_data).await?;
288            results.push(result);
289        }
290
291        Ok(ComparisonReport::new(results, image_data.len()))
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn test_benchmark_result_from_timings() {
301        let timings = vec![10.0, 20.0, 15.0, 25.0, 12.0];
302        let result = BenchmarkResult::from_timings("test".to_string(), timings);
303
304        assert_eq!(result.provider, "test");
305        assert_eq!(result.iterations, 5);
306        assert_eq!(result.min_ms, 10.0);
307        assert_eq!(result.max_ms, 25.0);
308        assert!((result.mean_ms - 16.4).abs() < 0.1);
309    }
310
311    #[test]
312    fn test_benchmark_result_empty() {
313        let result = BenchmarkResult::from_timings("test".to_string(), vec![]);
314        assert_eq!(result.iterations, 0);
315        assert_eq!(result.mean_ms, 0.0);
316    }
317
318    #[test]
319    fn test_comparison_report() {
320        let results = vec![
321            BenchmarkResult::from_timings("provider1".to_string(), vec![10.0, 12.0, 11.0]),
322            BenchmarkResult::from_timings("provider2".to_string(), vec![20.0, 22.0, 21.0]),
323        ];
324
325        let report = ComparisonReport::new(results, 1024);
326        assert_eq!(report.results.len(), 2);
327        assert_eq!(report.image_size_bytes, 1024);
328
329        let fastest = report.fastest_provider().unwrap();
330        assert_eq!(fastest.provider, "provider1");
331    }
332
333    #[test]
334    fn test_benchmark_result_format_row() {
335        let result = BenchmarkResult::from_timings("test".to_string(), vec![10.0, 20.0, 15.0]);
336        let row = result.format_row();
337        assert!(row.contains("test"));
338        assert!(row.contains("3")); // iterations
339    }
340
341    #[test]
342    fn test_comparison_report_format_table() {
343        let results = vec![BenchmarkResult::from_timings(
344            "mock".to_string(),
345            vec![5.0, 6.0, 5.5],
346        )];
347
348        let report = ComparisonReport::new(results, 1024);
349        let table = report.format_table();
350        assert!(table.contains("PROVIDER PERFORMANCE COMPARISON"));
351        assert!(table.contains("mock"));
352    }
353
354    #[test]
355    fn test_benchmark_runner_creation() {
356        let runner = BenchmarkRunner::new(20);
357        assert_eq!(runner.iterations, 20);
358
359        let runner = runner.with_warmup(5);
360        assert_eq!(runner.warmup_iterations, 5);
361    }
362
363    #[test]
364    fn test_memory_profile_format() {
365        let profile = MemoryProfile {
366            provider: "test".to_string(),
367            peak_bytes: 100 * 1024 * 1024,
368            avg_bytes: 80 * 1024 * 1024,
369            bytes_per_op: 512 * 1024,
370        };
371
372        let formatted = profile.format();
373        assert!(formatted.contains("test"));
374        assert!(formatted.contains("100 MB"));
375        assert!(formatted.contains("80 MB"));
376        assert!(formatted.contains("512 KB"));
377    }
378}