sciforge_hub/tools/
profiler.rs1use 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#[derive(Debug, Clone)]
11pub struct ProfileEntry {
12 pub domain: String,
14 pub function_name: String,
16 pub iterations: u64,
18 pub total_ns: u128,
20 pub min_ns: u128,
22 pub max_ns: u128,
24 pub mean_ns: f64,
26 pub stddev_ns: f64,
28 pub median_ns: u128,
30}
31
32impl ProfileEntry {
33 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 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
58pub const PROFILE_CSV_HEADER: &str =
60 "domain,function,iterations,total_ns,min_ns,max_ns,mean_ns,stddev_ns,median_ns";
61
62pub 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#[derive(Debug, Clone)]
112pub struct ProfileReport {
113 pub entries: Vec<ProfileEntry>,
115}
116
117impl ProfileReport {
118 pub fn total_time_ns(&self) -> u128 {
120 self.entries.iter().map(|e| e.total_ns).sum()
121 }
122
123 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 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 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 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 pub fn filter_domain(&self, domain: &str) -> Vec<&ProfileEntry> {
175 self.entries.iter().filter(|e| e.domain == domain).collect()
176 }
177}
178
179pub 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
190pub 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
204pub 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
212pub 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
225pub fn scalar_value(output: &RunOutput) -> Option<f64> {
227 match output {
228 RunOutput::Scalar(v) => Some(*v),
229 _ => None,
230 }
231}