1use std::time::{Duration, Instant};
12
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub struct BenchmarkConfig {
20 pub warmup_iterations: usize,
22 pub benchmark_iterations: usize,
24 pub batch_sizes: Vec<usize>,
26 pub measure_memory: bool,
28 pub measure_throughput: bool,
30}
31
32impl Default for BenchmarkConfig {
33 fn default() -> Self {
34 Self {
35 warmup_iterations: 10,
36 benchmark_iterations: 100,
37 batch_sizes: vec![1, 8, 16, 32, 64, 128],
38 measure_memory: true,
39 measure_throughput: true,
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47pub struct BenchmarkResult {
48 pub batch_size: usize,
50 pub mean_latency_ms: f64,
52 pub std_latency_ms: f64,
54 pub min_latency_ms: f64,
56 pub max_latency_ms: f64,
58 pub median_latency_ms: f64,
60 pub throughput_samples_per_sec: f64,
62 pub memory_mb: Option<f64>,
64}
65
66impl BenchmarkResult {
67 pub fn new(batch_size: usize, latencies: Vec<Duration>) -> Self {
69 let latencies_ms: Vec<f64> = latencies.iter().map(|d| d.as_secs_f64() * 1000.0).collect();
70
71 let mean = latencies_ms.iter().sum::<f64>() / latencies_ms.len() as f64;
72 let variance = latencies_ms.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
73 / latencies_ms.len() as f64;
74 let std = variance.sqrt();
75
76 let mut sorted_latencies = latencies_ms.clone();
77 sorted_latencies.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
78
79 let min = *sorted_latencies
80 .first()
81 .expect("collection should not be empty");
82 let max = *sorted_latencies.last().expect("empty collection");
83 let median = sorted_latencies[sorted_latencies.len() / 2];
84
85 let throughput = (batch_size as f64) / (mean / 1000.0);
87
88 Self {
89 batch_size,
90 mean_latency_ms: mean,
91 std_latency_ms: std,
92 min_latency_ms: min,
93 max_latency_ms: max,
94 median_latency_ms: median,
95 throughput_samples_per_sec: throughput,
96 memory_mb: None,
97 }
98 }
99
100 pub fn with_memory(mut self, memory_mb: f64) -> Self {
102 self.memory_mb = Some(memory_mb);
103 self
104 }
105
106 pub fn latency_percentile(&self, percentile: f64) -> f64 {
108 self.mean_latency_ms + self.std_latency_ms * percentile
110 }
111}
112
113#[derive(Debug, Clone)]
115#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
116pub struct BenchmarkSuite {
117 pub name: String,
119 pub results: Vec<BenchmarkResult>,
121 pub total_duration_secs: f64,
123}
124
125impl BenchmarkSuite {
126 pub fn new(name: String) -> Self {
128 Self {
129 name,
130 results: Vec::new(),
131 total_duration_secs: 0.0,
132 }
133 }
134
135 pub fn add_result(&mut self, result: BenchmarkResult) {
137 self.results.push(result);
138 }
139
140 pub fn set_duration(&mut self, duration: Duration) {
142 self.total_duration_secs = duration.as_secs_f64();
143 }
144
145 pub fn get_result(&self, batch_size: usize) -> Option<&BenchmarkResult> {
147 self.results.iter().find(|r| r.batch_size == batch_size)
148 }
149
150 pub fn best_throughput(&self) -> Option<&BenchmarkResult> {
152 self.results.iter().max_by(|a, b| {
153 a.throughput_samples_per_sec
154 .partial_cmp(&b.throughput_samples_per_sec)
155 .expect("value should be present")
156 })
157 }
158
159 pub fn best_latency(&self) -> Option<&BenchmarkResult> {
161 self.results.iter().min_by(|a, b| {
162 a.mean_latency_ms
163 .partial_cmp(&b.mean_latency_ms)
164 .unwrap_or(std::cmp::Ordering::Equal)
165 })
166 }
167
168 pub fn print_summary(&self) {
170 println!("\n=== Benchmark Suite: {} ===", self.name);
171 println!("Total Duration: {:.2}s", self.total_duration_secs);
172 println!(
173 "\n{:<12} {:<15} {:<15} {:<15} {:<15}",
174 "Batch Size", "Mean (ms)", "Std (ms)", "Throughput", "Memory (MB)"
175 );
176 println!("{}", "-".repeat(75));
177
178 for result in &self.results {
179 let memory_str = result
180 .memory_mb
181 .map(|m| format!("{:.2}", m))
182 .unwrap_or_else(|| "N/A".to_string());
183
184 println!(
185 "{:<12} {:<15.2} {:<15.2} {:<15.2} {:<15}",
186 result.batch_size,
187 result.mean_latency_ms,
188 result.std_latency_ms,
189 result.throughput_samples_per_sec,
190 memory_str
191 );
192 }
193
194 if let Some(best_throughput) = self.best_throughput() {
195 println!(
196 "\nBest Throughput: {:.2} samples/sec @ batch_size={}",
197 best_throughput.throughput_samples_per_sec, best_throughput.batch_size
198 );
199 }
200
201 if let Some(best_latency) = self.best_latency() {
202 println!(
203 "Best Latency: {:.2}ms @ batch_size={}",
204 best_latency.mean_latency_ms, best_latency.batch_size
205 );
206 }
207 }
208}
209
210pub struct Benchmarker {
212 config: BenchmarkConfig,
213}
214
215impl Default for Benchmarker {
216 fn default() -> Self {
217 Self::new(BenchmarkConfig::default())
218 }
219}
220
221impl Benchmarker {
222 pub fn new(config: BenchmarkConfig) -> Self {
224 Self { config }
225 }
226
227 pub fn benchmark<F, T>(&self, name: &str, mut f: F) -> BenchmarkSuite
229 where
230 F: FnMut() -> T,
231 {
232 let total_start = Instant::now();
233 let mut suite = BenchmarkSuite::new(name.to_string());
234
235 let mut latencies = Vec::new();
237
238 for _ in 0..self.config.warmup_iterations {
240 let _ = f();
241 }
242
243 for _ in 0..self.config.benchmark_iterations {
245 let start = Instant::now();
246 let _ = f();
247 let duration = start.elapsed();
248 latencies.push(duration);
249 }
250
251 let result = BenchmarkResult::new(1, latencies);
252 suite.add_result(result);
253
254 suite.set_duration(total_start.elapsed());
255 suite
256 }
257
258 pub fn benchmark_batch<F, T>(&self, name: &str, mut f: F) -> BenchmarkSuite
260 where
261 F: FnMut(usize) -> T,
262 {
263 let total_start = Instant::now();
264 let mut suite = BenchmarkSuite::new(name.to_string());
265
266 for &batch_size in &self.config.batch_sizes {
267 let mut latencies = Vec::new();
268
269 for _ in 0..self.config.warmup_iterations {
271 let _ = f(batch_size);
272 }
273
274 for _ in 0..self.config.benchmark_iterations {
276 let start = Instant::now();
277 let _ = f(batch_size);
278 let duration = start.elapsed();
279 latencies.push(duration);
280 }
281
282 let result = BenchmarkResult::new(batch_size, latencies);
283 suite.add_result(result);
284 }
285
286 suite.set_duration(total_start.elapsed());
287 suite
288 }
289
290 pub fn compare<F1, F2, T1, T2>(
292 &self,
293 name1: &str,
294 mut f1: F1,
295 name2: &str,
296 mut f2: F2,
297 ) -> (BenchmarkSuite, BenchmarkSuite, f64)
298 where
299 F1: FnMut() -> T1,
300 F2: FnMut() -> T2,
301 {
302 let suite1 = self.benchmark(name1, &mut f1);
303 let suite2 = self.benchmark(name2, &mut f2);
304
305 let speedup = if let (Some(r1), Some(r2)) = (suite1.results.first(), suite2.results.first())
307 {
308 r2.mean_latency_ms / r1.mean_latency_ms
309 } else {
310 1.0
311 };
312
313 (suite1, suite2, speedup)
314 }
315}
316
317#[derive(Debug, Clone)]
319#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
320pub struct TrainingBenchmark {
321 pub epochs: usize,
323 pub total_time_secs: f64,
325 pub time_per_epoch_secs: f64,
327 pub samples_per_sec: f64,
329 pub final_loss: f64,
331 pub final_accuracy: Option<f64>,
333}
334
335impl TrainingBenchmark {
336 pub fn new(
338 epochs: usize,
339 total_time: Duration,
340 total_samples: usize,
341 final_loss: f64,
342 final_accuracy: Option<f64>,
343 ) -> Self {
344 let total_time_secs = total_time.as_secs_f64();
345 let time_per_epoch_secs = total_time_secs / epochs as f64;
346 let samples_per_sec = total_samples as f64 / total_time_secs;
347
348 Self {
349 epochs,
350 total_time_secs,
351 time_per_epoch_secs,
352 samples_per_sec,
353 final_loss,
354 final_accuracy,
355 }
356 }
357
358 pub fn print_summary(&self) {
360 println!("\n=== Training Benchmark ===");
361 println!("Epochs: {}", self.epochs);
362 println!("Total Time: {:.2}s", self.total_time_secs);
363 println!("Time per Epoch: {:.2}s", self.time_per_epoch_secs);
364 println!("Throughput: {:.2} samples/sec", self.samples_per_sec);
365 println!("Final Loss: {:.4}", self.final_loss);
366 if let Some(acc) = self.final_accuracy {
367 println!("Final Accuracy: {:.2}%", acc * 100.0);
368 }
369 }
370}
371
372#[derive(Debug, Clone)]
374#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
375pub struct MemoryProfile {
376 pub peak_memory_mb: f64,
378 pub avg_memory_mb: f64,
380 pub component_memory: Vec<(String, f64)>,
382}
383
384impl MemoryProfile {
385 pub fn new() -> Self {
387 Self {
388 peak_memory_mb: 0.0,
389 avg_memory_mb: 0.0,
390 component_memory: Vec::new(),
391 }
392 }
393
394 pub fn add_component(&mut self, name: String, memory_mb: f64) {
396 self.component_memory.push((name, memory_mb));
397 }
398
399 pub fn print_summary(&self) {
401 println!("\n=== Memory Profile ===");
402 println!("Peak Memory: {:.2} MB", self.peak_memory_mb);
403 println!("Average Memory: {:.2} MB", self.avg_memory_mb);
404
405 if !self.component_memory.is_empty() {
406 println!("\nMemory by Component:");
407 for (name, memory) in &self.component_memory {
408 println!(" {}: {:.2} MB", name, memory);
409 }
410 }
411 }
412}
413
414impl Default for MemoryProfile {
415 fn default() -> Self {
416 Self::new()
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_benchmark_config_default() {
426 let config = BenchmarkConfig::default();
427 assert_eq!(config.warmup_iterations, 10);
428 assert_eq!(config.benchmark_iterations, 100);
429 assert!(!config.batch_sizes.is_empty());
430 }
431
432 #[test]
433 fn test_benchmark_result_creation() {
434 let durations = vec![
435 Duration::from_millis(10),
436 Duration::from_millis(12),
437 Duration::from_millis(11),
438 ];
439
440 let result = BenchmarkResult::new(32, durations);
441
442 assert_eq!(result.batch_size, 32);
443 assert!(result.mean_latency_ms > 0.0);
444 assert!(result.std_latency_ms >= 0.0);
445 assert!(result.throughput_samples_per_sec > 0.0);
446 }
447
448 #[test]
449 fn test_benchmark_suite() {
450 let mut suite = BenchmarkSuite::new("Test Suite".to_string());
451
452 let result1 = BenchmarkResult::new(8, vec![Duration::from_millis(10)]);
453 let result2 = BenchmarkResult::new(16, vec![Duration::from_millis(8)]);
454
455 suite.add_result(result1);
456 suite.add_result(result2);
457
458 assert_eq!(suite.results.len(), 2);
459 assert!(suite.get_result(8).is_some());
460 assert!(suite.get_result(16).is_some());
461 assert!(suite.get_result(32).is_none());
462 }
463
464 #[test]
465 fn test_benchmark_best_throughput() {
466 let mut suite = BenchmarkSuite::new("Test".to_string());
467
468 let result1 = BenchmarkResult::new(8, vec![Duration::from_millis(10)]);
469 let result2 = BenchmarkResult::new(16, vec![Duration::from_millis(8)]);
470
471 suite.add_result(result1);
472 suite.add_result(result2);
473
474 let best = suite.best_throughput().expect("operation should succeed");
475 assert_eq!(best.batch_size, 16); }
477
478 #[test]
479 fn test_benchmark_best_latency() {
480 let mut suite = BenchmarkSuite::new("Test".to_string());
481
482 let result1 = BenchmarkResult::new(8, vec![Duration::from_millis(10)]);
483 let result2 = BenchmarkResult::new(16, vec![Duration::from_millis(8)]);
484
485 suite.add_result(result1);
486 suite.add_result(result2);
487
488 let best = suite.best_latency().expect("operation should succeed");
489 assert_eq!(best.batch_size, 16); }
491
492 #[test]
493 fn test_benchmarker_creation() {
494 let config = BenchmarkConfig::default();
495 let benchmarker = Benchmarker::new(config);
496
497 assert!(benchmarker.config.warmup_iterations > 0);
498 }
499
500 #[test]
501 fn test_benchmarker_simple() {
502 let config = BenchmarkConfig {
503 warmup_iterations: 2,
504 benchmark_iterations: 5,
505 batch_sizes: vec![1],
506 measure_memory: false,
507 measure_throughput: true,
508 };
509
510 let benchmarker = Benchmarker::new(config);
511
512 let suite = benchmarker.benchmark("simple_test", || {
513 let mut sum = 0;
515 for i in 0..100 {
516 sum += i;
517 }
518 sum
519 });
520
521 assert_eq!(suite.results.len(), 1);
522 assert!(suite.results[0].mean_latency_ms >= 0.0);
523 }
524
525 #[test]
526 fn test_training_benchmark() {
527 let benchmark =
528 TrainingBenchmark::new(10, Duration::from_secs(100), 1000, 0.123, Some(0.95));
529
530 assert_eq!(benchmark.epochs, 10);
531 assert_eq!(benchmark.total_time_secs, 100.0);
532 assert_eq!(benchmark.time_per_epoch_secs, 10.0);
533 assert_eq!(benchmark.samples_per_sec, 10.0);
534 assert_eq!(benchmark.final_loss, 0.123);
535 assert_eq!(benchmark.final_accuracy, Some(0.95));
536 }
537
538 #[test]
539 fn test_memory_profile() {
540 let mut profile = MemoryProfile::new();
541 profile.peak_memory_mb = 512.0;
542 profile.avg_memory_mb = 256.0;
543
544 profile.add_component("Weights".to_string(), 100.0);
545 profile.add_component("Activations".to_string(), 50.0);
546
547 assert_eq!(profile.peak_memory_mb, 512.0);
548 assert_eq!(profile.component_memory.len(), 2);
549 }
550
551 #[test]
552 fn test_benchmark_result_with_memory() {
553 let result = BenchmarkResult::new(32, vec![Duration::from_millis(10)]);
554 let result_with_mem = result.with_memory(256.0);
555
556 assert_eq!(result_with_mem.memory_mb, Some(256.0));
557 }
558
559 #[test]
560 fn test_benchmarker_compare() {
561 let config = BenchmarkConfig {
562 warmup_iterations: 1,
563 benchmark_iterations: 3,
564 batch_sizes: vec![1],
565 measure_memory: false,
566 measure_throughput: true,
567 };
568
569 let benchmarker = Benchmarker::new(config);
570
571 let (suite1, suite2, speedup) = benchmarker.compare(
572 "impl1",
573 || {
574 std::thread::sleep(Duration::from_micros(100));
575 42
576 },
577 "impl2",
578 || {
579 std::thread::sleep(Duration::from_micros(200));
580 42
581 },
582 );
583
584 assert!(!suite1.results.is_empty());
585 assert!(!suite2.results.is_empty());
586 assert!(speedup > 0.0);
587 }
588}