use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkResult {
pub model_id: String,
pub execution_provider: String,
pub iterations: u32,
pub mean_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
pub median_ms: f64,
pub p95_ms: f64,
pub std_dev_ms: f64,
pub total_time_ms: f64,
pub iteration_times_ms: Vec<f64>,
}
impl BenchmarkResult {
pub fn from_times(
model_id: String,
execution_provider: String,
times_ms: Vec<f64>,
total_time_ms: f64,
) -> Self {
let iterations = times_ms.len() as u32;
if times_ms.is_empty() {
return Self {
model_id,
execution_provider,
iterations: 0,
mean_ms: 0.0,
min_ms: 0.0,
max_ms: 0.0,
median_ms: 0.0,
p95_ms: 0.0,
std_dev_ms: 0.0,
total_time_ms,
iteration_times_ms: vec![],
};
}
let mut sorted = times_ms.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mean = sorted.iter().sum::<f64>() / sorted.len() as f64;
let min = sorted.first().copied().unwrap_or(0.0);
let max = sorted.last().copied().unwrap_or(0.0);
let median = if sorted.len().is_multiple_of(2) {
let mid = sorted.len() / 2;
(sorted[mid - 1] + sorted[mid]) / 2.0
} else {
sorted[sorted.len() / 2]
};
let p95_idx = ((sorted.len() as f64) * 0.95).ceil() as usize - 1;
let p95 = sorted.get(p95_idx).copied().unwrap_or(max);
let variance = sorted.iter().map(|t| (t - mean).powi(2)).sum::<f64>() / sorted.len() as f64;
let std_dev = variance.sqrt();
Self {
model_id,
execution_provider,
iterations,
mean_ms: mean,
min_ms: min,
max_ms: max,
median_ms: median,
p95_ms: p95,
std_dev_ms: std_dev,
total_time_ms,
iteration_times_ms: times_ms,
}
}
pub fn summary(&self) -> String {
format!(
"{} ({}):\n Mean: {:.2}ms | Median: {:.2}ms | Std Dev: {:.2}ms\n Min: {:.2}ms | Max: {:.2}ms | P95: {:.2}ms\n Iterations: {}",
self.model_id,
self.execution_provider,
self.mean_ms,
self.median_ms,
self.std_dev_ms,
self.min_ms,
self.max_ms,
self.p95_ms,
self.iterations
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionProviderInfo {
pub name: String,
pub description: String,
pub coreml_available: bool,
pub apple_silicon: bool,
pub platform: String,
}
impl ExecutionProviderInfo {
pub fn current() -> Self {
let platform = if cfg!(target_os = "ios") {
"ios"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "android") {
"android"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else {
"other"
};
let coreml_available = cfg!(feature = "ort-coreml");
let apple_silicon =
cfg!(any(target_os = "ios", target_os = "macos")) && cfg!(target_arch = "aarch64");
let (name, description) = if coreml_available && apple_silicon {
(
"coreml-ane".to_string(),
"CoreML with Neural Engine acceleration (Apple Silicon)".to_string(),
)
} else if coreml_available {
(
"coreml".to_string(),
"CoreML execution provider available".to_string(),
)
} else {
(
"cpu".to_string(),
"CPU execution (no hardware acceleration)".to_string(),
)
};
Self {
name,
description,
coreml_available,
apple_silicon,
platform: platform.to_string(),
}
}
}
pub fn compare_benchmarks(baseline: &BenchmarkResult, comparison: &BenchmarkResult) -> String {
let speedup = if comparison.mean_ms > 0.0 {
baseline.mean_ms / comparison.mean_ms
} else {
0.0
};
let diff_ms = baseline.mean_ms - comparison.mean_ms;
let diff_pct = if baseline.mean_ms > 0.0 {
(diff_ms / baseline.mean_ms) * 100.0
} else {
0.0
};
format!(
"Benchmark Comparison\n\
====================\n\
Baseline ({}):\n\
Mean: {:.2}ms | Median: {:.2}ms\n\
Comparison ({}):\n\
Mean: {:.2}ms | Median: {:.2}ms\n\
\n\
Speedup: {:.2}x\n\
Difference: {:.2}ms ({:.1}%)",
baseline.execution_provider,
baseline.mean_ms,
baseline.median_ms,
comparison.execution_provider,
comparison.mean_ms,
comparison.median_ms,
speedup,
diff_ms,
diff_pct
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_benchmark_result_from_times() {
let times = vec![10.0, 12.0, 11.0, 15.0, 9.0];
let result =
BenchmarkResult::from_times("test-model".to_string(), "cpu".to_string(), times, 100.0);
assert_eq!(result.iterations, 5);
assert_eq!(result.min_ms, 9.0);
assert_eq!(result.max_ms, 15.0);
assert!((result.mean_ms - 11.4).abs() < 0.01);
assert_eq!(result.median_ms, 11.0);
}
#[test]
fn test_benchmark_result_empty() {
let result =
BenchmarkResult::from_times("test".to_string(), "cpu".to_string(), vec![], 0.0);
assert_eq!(result.iterations, 0);
assert_eq!(result.mean_ms, 0.0);
assert_eq!(result.min_ms, 0.0);
assert_eq!(result.max_ms, 0.0);
}
#[test]
fn test_benchmark_result_single_value() {
let result =
BenchmarkResult::from_times("test".to_string(), "cpu".to_string(), vec![42.0], 42.0);
assert_eq!(result.iterations, 1);
assert_eq!(result.mean_ms, 42.0);
assert_eq!(result.median_ms, 42.0);
assert_eq!(result.min_ms, 42.0);
assert_eq!(result.max_ms, 42.0);
assert_eq!(result.std_dev_ms, 0.0);
}
#[test]
fn test_benchmark_result_even_count_median() {
let times = vec![10.0, 20.0, 30.0, 40.0];
let result =
BenchmarkResult::from_times("test".to_string(), "cpu".to_string(), times, 100.0);
assert_eq!(result.median_ms, 25.0);
}
#[test]
fn test_benchmark_summary_format() {
let result = BenchmarkResult {
model_id: "test-model".to_string(),
execution_provider: "coreml-ane".to_string(),
iterations: 10,
mean_ms: 15.5,
min_ms: 10.0,
max_ms: 20.0,
median_ms: 15.0,
p95_ms: 19.0,
std_dev_ms: 2.5,
total_time_ms: 200.0,
iteration_times_ms: vec![],
};
let summary = result.summary();
assert!(summary.contains("test-model"));
assert!(summary.contains("coreml-ane"));
assert!(summary.contains("15.50ms"));
assert!(summary.contains("Iterations: 10"));
}
#[test]
fn test_execution_provider_info_current() {
let info = ExecutionProviderInfo::current();
assert!(!info.name.is_empty());
assert!(!info.platform.is_empty());
assert!(!info.description.is_empty());
assert!(["ios", "macos", "android", "linux", "windows", "other"]
.contains(&info.platform.as_str()));
}
#[test]
fn test_compare_benchmarks_speedup() {
let baseline = BenchmarkResult {
model_id: "test".to_string(),
execution_provider: "cpu".to_string(),
iterations: 10,
mean_ms: 100.0,
min_ms: 90.0,
max_ms: 110.0,
median_ms: 100.0,
p95_ms: 108.0,
std_dev_ms: 5.0,
total_time_ms: 1000.0,
iteration_times_ms: vec![],
};
let comparison = BenchmarkResult {
model_id: "test".to_string(),
execution_provider: "coreml-ane".to_string(),
iterations: 10,
mean_ms: 20.0,
min_ms: 18.0,
max_ms: 22.0,
median_ms: 20.0,
p95_ms: 21.0,
std_dev_ms: 1.0,
total_time_ms: 200.0,
iteration_times_ms: vec![],
};
let comparison_str = compare_benchmarks(&baseline, &comparison);
assert!(comparison_str.contains("5.00x"));
assert!(comparison_str.contains("cpu"));
assert!(comparison_str.contains("coreml-ane"));
assert!(comparison_str.contains("Benchmark Comparison"));
assert!(comparison_str.contains("80.00ms"));
}
#[test]
fn test_compare_benchmarks_zero_comparison_mean() {
let baseline = BenchmarkResult {
model_id: "test".to_string(),
execution_provider: "cpu".to_string(),
iterations: 10,
mean_ms: 100.0,
min_ms: 90.0,
max_ms: 110.0,
median_ms: 100.0,
p95_ms: 108.0,
std_dev_ms: 5.0,
total_time_ms: 1000.0,
iteration_times_ms: vec![],
};
let comparison = BenchmarkResult {
model_id: "test".to_string(),
execution_provider: "broken".to_string(),
iterations: 0,
mean_ms: 0.0,
min_ms: 0.0,
max_ms: 0.0,
median_ms: 0.0,
p95_ms: 0.0,
std_dev_ms: 0.0,
total_time_ms: 0.0,
iteration_times_ms: vec![],
};
let comparison_str = compare_benchmarks(&baseline, &comparison);
assert!(comparison_str.contains("0.00x"));
}
#[test]
fn test_compare_benchmarks_zero_baseline_mean() {
let baseline = BenchmarkResult {
model_id: "test".to_string(),
execution_provider: "broken".to_string(),
iterations: 0,
mean_ms: 0.0,
min_ms: 0.0,
max_ms: 0.0,
median_ms: 0.0,
p95_ms: 0.0,
std_dev_ms: 0.0,
total_time_ms: 0.0,
iteration_times_ms: vec![],
};
let comparison = BenchmarkResult {
model_id: "test".to_string(),
execution_provider: "cpu".to_string(),
iterations: 10,
mean_ms: 100.0,
min_ms: 90.0,
max_ms: 110.0,
median_ms: 100.0,
p95_ms: 108.0,
std_dev_ms: 5.0,
total_time_ms: 1000.0,
iteration_times_ms: vec![],
};
let comparison_str = compare_benchmarks(&baseline, &comparison);
assert!(comparison_str.contains("0.0%"));
}
}