| 1 | use crate::stats::univariate::Sample; |
| 2 | use crate::stats::univariate::{self, mixed}; |
| 3 | use crate::stats::Distribution; |
| 4 | |
| 5 | use crate::benchmark::BenchmarkConfig; |
| 6 | use crate::error::Result; |
| 7 | use crate::estimate::{ |
| 8 | build_change_estimates, ChangeDistributions, ChangeEstimates, ChangePointEstimates, Estimates, |
| 9 | }; |
| 10 | use crate::measurement::Measurement; |
| 11 | use crate::report::BenchmarkId; |
| 12 | use crate::{fs, Criterion, SavedSample}; |
| 13 | |
| 14 | // Common comparison procedure |
| 15 | #[cfg_attr (feature = "cargo-clippy" , allow(clippy::type_complexity))] |
| 16 | pub(crate) fn common<M: Measurement>( |
| 17 | id: &BenchmarkId, |
| 18 | avg_times: &Sample<f64>, |
| 19 | config: &BenchmarkConfig, |
| 20 | criterion: &Criterion<M>, |
| 21 | ) -> Result<( |
| 22 | f64, |
| 23 | Distribution<f64>, |
| 24 | ChangeEstimates, |
| 25 | ChangeDistributions, |
| 26 | Vec<f64>, |
| 27 | Vec<f64>, |
| 28 | Vec<f64>, |
| 29 | Estimates, |
| 30 | )> { |
| 31 | let mut sample_file = criterion.output_directory.clone(); |
| 32 | sample_file.push(id.as_directory_name()); |
| 33 | sample_file.push(&criterion.baseline_directory); |
| 34 | sample_file.push("sample.json" ); |
| 35 | let sample: SavedSample = fs::load(&sample_file)?; |
| 36 | let SavedSample { iters, times, .. } = sample; |
| 37 | |
| 38 | let mut estimates_file = criterion.output_directory.clone(); |
| 39 | estimates_file.push(id.as_directory_name()); |
| 40 | estimates_file.push(&criterion.baseline_directory); |
| 41 | estimates_file.push("estimates.json" ); |
| 42 | let base_estimates: Estimates = fs::load(&estimates_file)?; |
| 43 | |
| 44 | let base_avg_times: Vec<f64> = iters |
| 45 | .iter() |
| 46 | .zip(times.iter()) |
| 47 | .map(|(iters, elapsed)| elapsed / iters) |
| 48 | .collect(); |
| 49 | let base_avg_time_sample = Sample::new(&base_avg_times); |
| 50 | |
| 51 | let mut change_dir = criterion.output_directory.clone(); |
| 52 | change_dir.push(id.as_directory_name()); |
| 53 | change_dir.push("change" ); |
| 54 | fs::mkdirp(&change_dir)?; |
| 55 | let (t_statistic, t_distribution) = t_test(avg_times, base_avg_time_sample, config); |
| 56 | |
| 57 | let (estimates, relative_distributions) = |
| 58 | estimates(id, avg_times, base_avg_time_sample, config, criterion); |
| 59 | Ok(( |
| 60 | t_statistic, |
| 61 | t_distribution, |
| 62 | estimates, |
| 63 | relative_distributions, |
| 64 | iters, |
| 65 | times, |
| 66 | base_avg_times.clone(), |
| 67 | base_estimates, |
| 68 | )) |
| 69 | } |
| 70 | |
| 71 | // Performs a two sample t-test |
| 72 | fn t_test( |
| 73 | avg_times: &Sample<f64>, |
| 74 | base_avg_times: &Sample<f64>, |
| 75 | config: &BenchmarkConfig, |
| 76 | ) -> (f64, Distribution<f64>) { |
| 77 | let nresamples = config.nresamples; |
| 78 | |
| 79 | let t_statistic = avg_times.t(base_avg_times); |
| 80 | let t_distribution = elapsed!( |
| 81 | "Bootstrapping the T distribution" , |
| 82 | mixed::bootstrap(avg_times, base_avg_times, nresamples, |a, b| (a.t(b),)) |
| 83 | ) |
| 84 | .0; |
| 85 | |
| 86 | // HACK: Filter out non-finite numbers, which can happen sometimes when sample size is very small. |
| 87 | // Downstream code doesn't like non-finite values here. |
| 88 | let t_distribution = Distribution::from( |
| 89 | t_distribution |
| 90 | .iter() |
| 91 | .filter(|a| a.is_finite()) |
| 92 | .cloned() |
| 93 | .collect::<Vec<_>>() |
| 94 | .into_boxed_slice(), |
| 95 | ); |
| 96 | |
| 97 | (t_statistic, t_distribution) |
| 98 | } |
| 99 | |
| 100 | // Estimates the relative change in the statistics of the population |
| 101 | fn estimates<M: Measurement>( |
| 102 | id: &BenchmarkId, |
| 103 | avg_times: &Sample<f64>, |
| 104 | base_avg_times: &Sample<f64>, |
| 105 | config: &BenchmarkConfig, |
| 106 | criterion: &Criterion<M>, |
| 107 | ) -> (ChangeEstimates, ChangeDistributions) { |
| 108 | fn stats(a: &Sample<f64>, b: &Sample<f64>) -> (f64, f64) { |
| 109 | ( |
| 110 | a.mean() / b.mean() - 1., |
| 111 | a.percentiles().median() / b.percentiles().median() - 1., |
| 112 | ) |
| 113 | } |
| 114 | |
| 115 | let cl = config.confidence_level; |
| 116 | let nresamples = config.nresamples; |
| 117 | |
| 118 | let (dist_mean, dist_median) = elapsed!( |
| 119 | "Bootstrapping the relative statistics" , |
| 120 | univariate::bootstrap(avg_times, base_avg_times, nresamples, stats) |
| 121 | ); |
| 122 | |
| 123 | let distributions = ChangeDistributions { |
| 124 | mean: dist_mean, |
| 125 | median: dist_median, |
| 126 | }; |
| 127 | |
| 128 | let (mean, median) = stats(avg_times, base_avg_times); |
| 129 | let points = ChangePointEstimates { mean, median }; |
| 130 | |
| 131 | let estimates = build_change_estimates(&distributions, &points, cl); |
| 132 | |
| 133 | { |
| 134 | log_if_err!({ |
| 135 | let mut estimates_path = criterion.output_directory.clone(); |
| 136 | estimates_path.push(id.as_directory_name()); |
| 137 | estimates_path.push("change" ); |
| 138 | estimates_path.push("estimates.json" ); |
| 139 | fs::save(&estimates, &estimates_path) |
| 140 | }); |
| 141 | } |
| 142 | (estimates, distributions) |
| 143 | } |
| 144 | |