Skip to main content

sciforge_hub/tools/
profiler.rs

1//! Experiment profiling: individual and batch timing,
2//! statistics (min, max, median, stddev), and CSV / Markdown export.
3
4use crate::domain::common::errors::{HubError, HubResult};
5use crate::engine::experience::experiment::{DomainType, Experiment, ParameterValue};
6use crate::engine::experience::runner::{ExperimentRunner, RunOutput};
7use std::time::Instant;
8
9/// Profiling result for a single experiment.
10#[derive(Debug, Clone)]
11pub struct ProfileEntry {
12    /// Scientific domain being profiled.
13    pub domain: String,
14    /// Name of the profiled function.
15    pub function_name: String,
16    /// Number of iterations performed.
17    pub iterations: u64,
18    /// Total time in nanoseconds.
19    pub total_ns: u128,
20    /// Minimum time in nanoseconds.
21    pub min_ns: u128,
22    /// Maximum time in nanoseconds.
23    pub max_ns: u128,
24    /// Mean time in nanoseconds.
25    pub mean_ns: f64,
26    /// Standard deviation in nanoseconds.
27    pub stddev_ns: f64,
28    /// Median time in nanoseconds.
29    pub median_ns: u128,
30}
31
32impl ProfileEntry {
33    /// Throughput in calls per second, derived from the mean.
34    pub fn throughput_per_sec(&self) -> f64 {
35        if self.mean_ns == 0.0 {
36            return 0.0;
37        }
38        1e9 / self.mean_ns
39    }
40
41    /// Serializes the entry as a CSV row.
42    pub fn to_csv_row(&self) -> String {
43        format!(
44            "{},{},{},{},{},{},{:.1},{:.1},{}",
45            self.domain,
46            self.function_name,
47            self.iterations,
48            self.total_ns,
49            self.min_ns,
50            self.max_ns,
51            self.mean_ns,
52            self.stddev_ns,
53            self.median_ns,
54        )
55    }
56}
57
58/// CSV header for profiling columns.
59pub const PROFILE_CSV_HEADER: &str =
60    "domain,function,iterations,total_ns,min_ns,max_ns,mean_ns,stddev_ns,median_ns";
61
62/// Profiles an experiment over `iterations` runs and returns statistics.
63pub fn profile_experiment(experiment: &Experiment, iterations: u64) -> HubResult<ProfileEntry> {
64    if iterations == 0 {
65        return Err(HubError::InvalidInput("iterations must be > 0".into()));
66    }
67    let runner = ExperimentRunner::new();
68    let _ = runner.run(experiment)?;
69    let mut timings = Vec::with_capacity(iterations as usize);
70    let mut total: u128 = 0;
71
72    for _ in 0..iterations {
73        let start = Instant::now();
74        let _ = runner.run(experiment);
75        let elapsed = start.elapsed().as_nanos();
76        timings.push(elapsed);
77        total += elapsed;
78    }
79
80    timings.sort_unstable();
81    let n = timings.len();
82    let min_ns = timings[0];
83    let max_ns = timings[n - 1];
84    let median_ns = timings[n / 2];
85    let mean_ns = total as f64 / n as f64;
86    let variance = timings
87        .iter()
88        .map(|&t| {
89            let d = t as f64 - mean_ns;
90            d * d
91        })
92        .sum::<f64>()
93        / n as f64;
94    let stddev_ns = variance.sqrt();
95
96    let domain_str = format!("{:?}", experiment.domain).to_lowercase();
97    Ok(ProfileEntry {
98        domain: domain_str,
99        function_name: experiment.function_name.clone(),
100        iterations,
101        total_ns: total,
102        min_ns,
103        max_ns,
104        mean_ns,
105        stddev_ns,
106        median_ns,
107    })
108}
109
110/// Aggregated profiling report for multiple experiments.
111#[derive(Debug, Clone)]
112pub struct ProfileReport {
113    /// Per-experiment profiling entries.
114    pub entries: Vec<ProfileEntry>,
115}
116
117impl ProfileReport {
118    /// Cumulative total time across all entries.
119    pub fn total_time_ns(&self) -> u128 {
120        self.entries.iter().map(|e| e.total_ns).sum()
121    }
122
123    /// Returns the slowest entry.
124    pub fn slowest(&self) -> Option<&ProfileEntry> {
125        self.entries.iter().max_by(|a, b| {
126            a.mean_ns
127                .partial_cmp(&b.mean_ns)
128                .unwrap_or(std::cmp::Ordering::Equal)
129        })
130    }
131
132    /// Returns the fastest entry.
133    pub fn fastest(&self) -> Option<&ProfileEntry> {
134        self.entries.iter().min_by(|a, b| {
135            a.mean_ns
136                .partial_cmp(&b.mean_ns)
137                .unwrap_or(std::cmp::Ordering::Equal)
138        })
139    }
140
141    /// Exports the report as CSV.
142    pub fn to_csv(&self) -> String {
143        let mut out = String::from(PROFILE_CSV_HEADER);
144        out.push('\n');
145        for e in &self.entries {
146            out.push_str(&e.to_csv_row());
147            out.push('\n');
148        }
149        out
150    }
151
152    /// Exports the report as Markdown.
153    pub fn to_markdown(&self) -> String {
154        let mut out = String::from("# Profile Report\n\n");
155        out.push_str("| Domain | Function | Iters | Mean (ns) | Min (ns) | Max (ns) | Stddev | Throughput/s |\n");
156        out.push_str("|--------|----------|-------|-----------|----------|----------|--------|-------------|\n");
157        for e in &self.entries {
158            out.push_str(&format!(
159                "| {} | {} | {} | {:.0} | {} | {} | {:.0} | {:.0} |\n",
160                e.domain,
161                e.function_name,
162                e.iterations,
163                e.mean_ns,
164                e.min_ns,
165                e.max_ns,
166                e.stddev_ns,
167                e.throughput_per_sec(),
168            ));
169        }
170        out
171    }
172
173    /// Filters entries matching the given `domain`.
174    pub fn filter_domain(&self, domain: &str) -> Vec<&ProfileEntry> {
175        self.entries.iter().filter(|e| e.domain == domain).collect()
176    }
177}
178
179/// Profiles a batch of experiments and returns an aggregated report.
180pub fn profile_batch(experiments: &[Experiment], iterations: u64) -> ProfileReport {
181    let mut entries = Vec::with_capacity(experiments.len());
182    for exp in experiments {
183        if let Ok(entry) = profile_experiment(exp, iterations) {
184            entries.push(entry);
185        }
186    }
187    ProfileReport { entries }
188}
189
190/// Shortcut to profile a domain function with scalar parameters.
191pub fn quick_profile(
192    domain: DomainType,
193    func: &str,
194    params: Vec<(&str, f64)>,
195    iterations: u64,
196) -> HubResult<ProfileEntry> {
197    let mut exp = Experiment::new(domain, func);
198    for (name, val) in params {
199        exp = exp.param(name, ParameterValue::Scalar(val));
200    }
201    profile_experiment(&exp, iterations)
202}
203
204/// Computes the ratio of mean times between two profiling entries.
205pub fn compare_entries(a: &ProfileEntry, b: &ProfileEntry) -> f64 {
206    if b.mean_ns == 0.0 {
207        return 0.0;
208    }
209    a.mean_ns / b.mean_ns
210}
211
212/// Formats a duration in nanoseconds into a human-readable unit (ns, µs, ms, s).
213pub fn format_ns(ns: f64) -> String {
214    if ns >= 1e9 {
215        format!("{:.2}s", ns / 1e9)
216    } else if ns >= 1e6 {
217        format!("{:.2}ms", ns / 1e6)
218    } else if ns >= 1e3 {
219        format!("{:.2}µs", ns / 1e3)
220    } else {
221        format!("{:.0}ns", ns)
222    }
223}
224
225/// Extracts the scalar value from a `RunOutput`, or `None`.
226pub fn scalar_value(output: &RunOutput) -> Option<f64> {
227    match output {
228        RunOutput::Scalar(v) => Some(*v),
229        _ => None,
230    }
231}