1 use std::path::Path;
2 
3 use crate::stats::bivariate::regression::Slope;
4 use crate::stats::bivariate::Data;
5 use crate::stats::univariate::outliers::tukey;
6 use crate::stats::univariate::Sample;
7 use crate::stats::{Distribution, Tails};
8 
9 use crate::benchmark::BenchmarkConfig;
10 use crate::connection::OutgoingMessage;
11 use crate::estimate::{
12     build_estimates, ConfidenceInterval, Distributions, Estimate, Estimates, PointEstimates,
13 };
14 use crate::fs;
15 use crate::measurement::Measurement;
16 use crate::report::{BenchmarkId, Report, ReportContext};
17 use crate::routine::Routine;
18 use crate::{Baseline, Criterion, SavedSample, Throughput};
19 
20 macro_rules! elapsed {
21     ($msg:expr, $block:expr) => {{
22         let start = ::std::time::Instant::now();
23         let out = $block;
24         let elapsed = &start.elapsed();
25 
26         info!(
27             "{} took {}",
28             $msg,
29             crate::format::time(elapsed.as_nanos() as f64)
30         );
31 
32         out
33     }};
34 }
35 
36 mod compare;
37 
38 // Common analysis procedure
common<M: Measurement, T: ?Sized>( id: &BenchmarkId, routine: &mut dyn Routine<M, T>, config: &BenchmarkConfig, criterion: &Criterion<M>, report_context: &ReportContext, parameter: &T, throughput: Option<Throughput>, )39 pub(crate) fn common<M: Measurement, T: ?Sized>(
40     id: &BenchmarkId,
41     routine: &mut dyn Routine<M, T>,
42     config: &BenchmarkConfig,
43     criterion: &Criterion<M>,
44     report_context: &ReportContext,
45     parameter: &T,
46     throughput: Option<Throughput>,
47 ) {
48     criterion.report.benchmark_start(id, report_context);
49 
50     if let Baseline::CompareStrict = criterion.baseline {
51         if !base_dir_exists(
52             id,
53             &criterion.baseline_directory,
54             &criterion.output_directory,
55         ) {
56             panic!(
57                 "Baseline '{base}' must exist before comparison is allowed; try --save-baseline {base}",
58                 base=criterion.baseline_directory,
59             );
60         }
61     }
62 
63     let (sampling_mode, iters, times);
64     if let Some(baseline) = &criterion.load_baseline {
65         let mut sample_path = criterion.output_directory.clone();
66         sample_path.push(id.as_directory_name());
67         sample_path.push(baseline);
68         sample_path.push("sample.json");
69         let loaded = fs::load::<SavedSample, _>(&sample_path);
70 
71         match loaded {
72             Err(err) => panic!(
73                 "Baseline '{base}' must exist before it can be loaded; try --save-baseline {base}. Error: {err}",
74                 base = baseline, err = err
75             ),
76             Ok(samples) => {
77                 sampling_mode = samples.sampling_mode;
78                 iters = samples.iters.into_boxed_slice();
79                 times = samples.times.into_boxed_slice();
80             }
81         }
82     } else {
83         let sample = routine.sample(
84             &criterion.measurement,
85             id,
86             config,
87             criterion,
88             report_context,
89             parameter,
90         );
91         sampling_mode = sample.0;
92         iters = sample.1;
93         times = sample.2;
94 
95         if let Some(conn) = &criterion.connection {
96             conn.send(&OutgoingMessage::MeasurementComplete {
97                 id: id.into(),
98                 iters: &iters,
99                 times: &times,
100                 plot_config: (&report_context.plot_config).into(),
101                 sampling_method: sampling_mode.into(),
102                 benchmark_config: config.into(),
103             })
104             .unwrap();
105 
106             conn.serve_value_formatter(criterion.measurement.formatter())
107                 .unwrap();
108             return;
109         }
110     }
111 
112     criterion.report.analysis(id, report_context);
113 
114     if times.iter().any(|&f| f == 0.0) {
115         error!(
116             "At least one measurement of benchmark {} took zero time per \
117             iteration. This should not be possible. If using iter_custom, please verify \
118             that your routine is correctly measured.",
119             id.as_title()
120         );
121         return;
122     }
123 
124     let avg_times = iters
125         .iter()
126         .zip(times.iter())
127         .map(|(&iters, &elapsed)| elapsed / iters)
128         .collect::<Vec<f64>>();
129     let avg_times = Sample::new(&avg_times);
130 
131     if criterion.should_save_baseline() {
132         log_if_err!({
133             let mut new_dir = criterion.output_directory.clone();
134             new_dir.push(id.as_directory_name());
135             new_dir.push("new");
136             fs::mkdirp(&new_dir)
137         });
138     }
139 
140     let data = Data::new(&iters, &times);
141     let labeled_sample = tukey::classify(avg_times);
142     if criterion.should_save_baseline() {
143         log_if_err!({
144             let mut tukey_file = criterion.output_directory.to_owned();
145             tukey_file.push(id.as_directory_name());
146             tukey_file.push("new");
147             tukey_file.push("tukey.json");
148             fs::save(&labeled_sample.fences(), &tukey_file)
149         });
150     }
151     let (mut distributions, mut estimates) = estimates(avg_times, config);
152     if sampling_mode.is_linear() {
153         let (distribution, slope) = regression(&data, config);
154 
155         estimates.slope = Some(slope);
156         distributions.slope = Some(distribution);
157     }
158 
159     if criterion.should_save_baseline() {
160         log_if_err!({
161             let mut sample_file = criterion.output_directory.clone();
162             sample_file.push(id.as_directory_name());
163             sample_file.push("new");
164             sample_file.push("sample.json");
165             fs::save(
166                 &SavedSample {
167                     sampling_mode,
168                     iters: data.x().as_ref().to_vec(),
169                     times: data.y().as_ref().to_vec(),
170                 },
171                 &sample_file,
172             )
173         });
174         log_if_err!({
175             let mut estimates_file = criterion.output_directory.clone();
176             estimates_file.push(id.as_directory_name());
177             estimates_file.push("new");
178             estimates_file.push("estimates.json");
179             fs::save(&estimates, &estimates_file)
180         });
181     }
182 
183     let compare_data = if base_dir_exists(
184         id,
185         &criterion.baseline_directory,
186         &criterion.output_directory,
187     ) {
188         let result = compare::common(id, avg_times, config, criterion);
189         match result {
190             Ok((
191                 t_value,
192                 t_distribution,
193                 relative_estimates,
194                 relative_distributions,
195                 base_iter_counts,
196                 base_sample_times,
197                 base_avg_times,
198                 base_estimates,
199             )) => {
200                 let p_value = t_distribution.p_value(t_value, &Tails::Two);
201                 Some(crate::report::ComparisonData {
202                     p_value,
203                     t_distribution,
204                     t_value,
205                     relative_estimates,
206                     relative_distributions,
207                     significance_threshold: config.significance_level,
208                     noise_threshold: config.noise_threshold,
209                     base_iter_counts,
210                     base_sample_times,
211                     base_avg_times,
212                     base_estimates,
213                 })
214             }
215             Err(e) => {
216                 crate::error::log_error(&e);
217                 None
218             }
219         }
220     } else {
221         None
222     };
223 
224     let measurement_data = crate::report::MeasurementData {
225         data: Data::new(&*iters, &*times),
226         avg_times: labeled_sample,
227         absolute_estimates: estimates,
228         distributions,
229         comparison: compare_data,
230         throughput,
231     };
232 
233     criterion.report.measurement_complete(
234         id,
235         report_context,
236         &measurement_data,
237         criterion.measurement.formatter(),
238     );
239 
240     if criterion.should_save_baseline() {
241         log_if_err!({
242             let mut benchmark_file = criterion.output_directory.clone();
243             benchmark_file.push(id.as_directory_name());
244             benchmark_file.push("new");
245             benchmark_file.push("benchmark.json");
246             fs::save(&id, &benchmark_file)
247         });
248     }
249 
250     if criterion.connection.is_none() {
251         if let Baseline::Save = criterion.baseline {
252             copy_new_dir_to_base(
253                 id.as_directory_name(),
254                 &criterion.baseline_directory,
255                 &criterion.output_directory,
256             );
257         }
258     }
259 }
260 
base_dir_exists(id: &BenchmarkId, baseline: &str, output_directory: &Path) -> bool261 fn base_dir_exists(id: &BenchmarkId, baseline: &str, output_directory: &Path) -> bool {
262     let mut base_dir = output_directory.to_owned();
263     base_dir.push(id.as_directory_name());
264     base_dir.push(baseline);
265     base_dir.exists()
266 }
267 
268 // Performs a simple linear regression on the sample
regression( data: &Data<'_, f64, f64>, config: &BenchmarkConfig, ) -> (Distribution<f64>, Estimate)269 fn regression(
270     data: &Data<'_, f64, f64>,
271     config: &BenchmarkConfig,
272 ) -> (Distribution<f64>, Estimate) {
273     let cl = config.confidence_level;
274 
275     let distribution = elapsed!(
276         "Bootstrapped linear regression",
277         data.bootstrap(config.nresamples, |d| (Slope::fit(&d).0,))
278     )
279     .0;
280 
281     let point = Slope::fit(data);
282     let (lb, ub) = distribution.confidence_interval(config.confidence_level);
283     let se = distribution.std_dev(None);
284 
285     (
286         distribution,
287         Estimate {
288             confidence_interval: ConfidenceInterval {
289                 confidence_level: cl,
290                 lower_bound: lb,
291                 upper_bound: ub,
292             },
293             point_estimate: point.0,
294             standard_error: se,
295         },
296     )
297 }
298 
299 // Estimates the statistics of the population from the sample
estimates(avg_times: &Sample<f64>, config: &BenchmarkConfig) -> (Distributions, Estimates)300 fn estimates(avg_times: &Sample<f64>, config: &BenchmarkConfig) -> (Distributions, Estimates) {
301     fn stats(sample: &Sample<f64>) -> (f64, f64, f64, f64) {
302         let mean = sample.mean();
303         let std_dev = sample.std_dev(Some(mean));
304         let median = sample.percentiles().median();
305         let mad = sample.median_abs_dev(Some(median));
306 
307         (mean, std_dev, median, mad)
308     }
309 
310     let cl = config.confidence_level;
311     let nresamples = config.nresamples;
312 
313     let (mean, std_dev, median, mad) = stats(avg_times);
314     let points = PointEstimates {
315         mean,
316         median,
317         std_dev,
318         median_abs_dev: mad,
319     };
320 
321     let (dist_mean, dist_stddev, dist_median, dist_mad) = elapsed!(
322         "Bootstrapping the absolute statistics.",
323         avg_times.bootstrap(nresamples, stats)
324     );
325 
326     let distributions = Distributions {
327         mean: dist_mean,
328         slope: None,
329         median: dist_median,
330         median_abs_dev: dist_mad,
331         std_dev: dist_stddev,
332     };
333 
334     let estimates = build_estimates(&distributions, &points, cl);
335 
336     (distributions, estimates)
337 }
338 
copy_new_dir_to_base(id: &str, baseline: &str, output_directory: &Path)339 fn copy_new_dir_to_base(id: &str, baseline: &str, output_directory: &Path) {
340     let root_dir = Path::new(output_directory).join(id);
341     let base_dir = root_dir.join(baseline);
342     let new_dir = root_dir.join("new");
343 
344     if !new_dir.exists() {
345         return;
346     };
347     if !base_dir.exists() {
348         try_else_return!(fs::mkdirp(&base_dir));
349     }
350 
351     // TODO: consider using walkdir or similar to generically copy.
352     try_else_return!(fs::cp(
353         &new_dir.join("estimates.json"),
354         &base_dir.join("estimates.json")
355     ));
356     try_else_return!(fs::cp(
357         &new_dir.join("sample.json"),
358         &base_dir.join("sample.json")
359     ));
360     try_else_return!(fs::cp(
361         &new_dir.join("tukey.json"),
362         &base_dir.join("tukey.json")
363     ));
364     try_else_return!(fs::cp(
365         &new_dir.join("benchmark.json"),
366         &base_dir.join("benchmark.json")
367     ));
368     #[cfg(feature = "csv_output")]
369     try_else_return!(fs::cp(&new_dir.join("raw.csv"), &base_dir.join("raw.csv")));
370 }
371