use crate::domain::common::errors::{HubError, HubResult};
use crate::engine::experience::experiment::{DomainType, Experiment, ParameterValue};
use crate::engine::experience::runner::{ExperimentRunner, RunOutput};
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct ProfileEntry {
pub domain: String,
pub function_name: String,
pub iterations: u64,
pub total_ns: u128,
pub min_ns: u128,
pub max_ns: u128,
pub mean_ns: f64,
pub stddev_ns: f64,
pub median_ns: u128,
}
impl ProfileEntry {
pub fn throughput_per_sec(&self) -> f64 {
if self.mean_ns == 0.0 {
return 0.0;
}
1e9 / self.mean_ns
}
pub fn to_csv_row(&self) -> String {
format!(
"{},{},{},{},{},{},{:.1},{:.1},{}",
self.domain,
self.function_name,
self.iterations,
self.total_ns,
self.min_ns,
self.max_ns,
self.mean_ns,
self.stddev_ns,
self.median_ns,
)
}
}
pub const PROFILE_CSV_HEADER: &str =
"domain,function,iterations,total_ns,min_ns,max_ns,mean_ns,stddev_ns,median_ns";
pub fn profile_experiment(experiment: &Experiment, iterations: u64) -> HubResult<ProfileEntry> {
if iterations == 0 {
return Err(HubError::InvalidInput("iterations must be > 0".into()));
}
let runner = ExperimentRunner::new();
let _ = runner.run(experiment)?;
let mut timings = Vec::with_capacity(iterations as usize);
let mut total: u128 = 0;
for _ in 0..iterations {
let start = Instant::now();
let _ = runner.run(experiment);
let elapsed = start.elapsed().as_nanos();
timings.push(elapsed);
total += elapsed;
}
timings.sort_unstable();
let n = timings.len();
let min_ns = timings[0];
let max_ns = timings[n - 1];
let median_ns = timings[n / 2];
let mean_ns = total as f64 / n as f64;
let variance = timings
.iter()
.map(|&t| {
let d = t as f64 - mean_ns;
d * d
})
.sum::<f64>()
/ n as f64;
let stddev_ns = variance.sqrt();
let domain_str = format!("{:?}", experiment.domain).to_lowercase();
Ok(ProfileEntry {
domain: domain_str,
function_name: experiment.function_name.clone(),
iterations,
total_ns: total,
min_ns,
max_ns,
mean_ns,
stddev_ns,
median_ns,
})
}
#[derive(Debug, Clone)]
pub struct ProfileReport {
pub entries: Vec<ProfileEntry>,
}
impl ProfileReport {
pub fn total_time_ns(&self) -> u128 {
self.entries.iter().map(|e| e.total_ns).sum()
}
pub fn slowest(&self) -> Option<&ProfileEntry> {
self.entries.iter().max_by(|a, b| {
a.mean_ns
.partial_cmp(&b.mean_ns)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn fastest(&self) -> Option<&ProfileEntry> {
self.entries.iter().min_by(|a, b| {
a.mean_ns
.partial_cmp(&b.mean_ns)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn to_csv(&self) -> String {
let mut out = String::from(PROFILE_CSV_HEADER);
out.push('\n');
for e in &self.entries {
out.push_str(&e.to_csv_row());
out.push('\n');
}
out
}
pub fn to_markdown(&self) -> String {
let mut out = String::from("# Profile Report\n\n");
out.push_str("| Domain | Function | Iters | Mean (ns) | Min (ns) | Max (ns) | Stddev | Throughput/s |\n");
out.push_str("|--------|----------|-------|-----------|----------|----------|--------|-------------|\n");
for e in &self.entries {
out.push_str(&format!(
"| {} | {} | {} | {:.0} | {} | {} | {:.0} | {:.0} |\n",
e.domain,
e.function_name,
e.iterations,
e.mean_ns,
e.min_ns,
e.max_ns,
e.stddev_ns,
e.throughput_per_sec(),
));
}
out
}
pub fn filter_domain(&self, domain: &str) -> Vec<&ProfileEntry> {
self.entries.iter().filter(|e| e.domain == domain).collect()
}
}
pub fn profile_batch(experiments: &[Experiment], iterations: u64) -> ProfileReport {
let mut entries = Vec::with_capacity(experiments.len());
for exp in experiments {
if let Ok(entry) = profile_experiment(exp, iterations) {
entries.push(entry);
}
}
ProfileReport { entries }
}
pub fn quick_profile(
domain: DomainType,
func: &str,
params: Vec<(&str, f64)>,
iterations: u64,
) -> HubResult<ProfileEntry> {
let mut exp = Experiment::new(domain, func);
for (name, val) in params {
exp = exp.param(name, ParameterValue::Scalar(val));
}
profile_experiment(&exp, iterations)
}
pub fn compare_entries(a: &ProfileEntry, b: &ProfileEntry) -> f64 {
if b.mean_ns == 0.0 {
return 0.0;
}
a.mean_ns / b.mean_ns
}
pub fn format_ns(ns: f64) -> String {
if ns >= 1e9 {
format!("{:.2}s", ns / 1e9)
} else if ns >= 1e6 {
format!("{:.2}ms", ns / 1e6)
} else if ns >= 1e3 {
format!("{:.2}µs", ns / 1e3)
} else {
format!("{:.0}ns", ns)
}
}
pub fn scalar_value(output: &RunOutput) -> Option<f64> {
match output {
RunOutput::Scalar(v) => Some(*v),
_ => None,
}
}