| 1 | use std::iter; |
| 2 | use std::path::Path; |
| 3 | use std::process::Child; |
| 4 | |
| 5 | use crate::stats::univariate::Sample; |
| 6 | use criterion_plot::prelude::*; |
| 7 | |
| 8 | mod distributions; |
| 9 | mod iteration_times; |
| 10 | mod pdf; |
| 11 | mod regression; |
| 12 | mod summary; |
| 13 | mod t_test; |
| 14 | use self::distributions::*; |
| 15 | use self::iteration_times::*; |
| 16 | use self::pdf::*; |
| 17 | use self::regression::*; |
| 18 | use self::summary::*; |
| 19 | use self::t_test::*; |
| 20 | |
| 21 | use crate::measurement::ValueFormatter; |
| 22 | use crate::report::{BenchmarkId, ValueType}; |
| 23 | use crate::stats::bivariate::Data; |
| 24 | |
| 25 | use super::{PlotContext, PlotData, Plotter}; |
| 26 | use crate::format; |
| 27 | |
| 28 | fn gnuplot_escape(string: &str) -> String { |
| 29 | string.replace('_' , " \\_" ).replace(' \'' , "''" ) |
| 30 | } |
| 31 | |
| 32 | static DEFAULT_FONT: &str = "Helvetica" ; |
| 33 | static KDE_POINTS: usize = 500; |
| 34 | static SIZE: Size = Size(1280, 720); |
| 35 | |
| 36 | const LINEWIDTH: LineWidth = LineWidth(2.); |
| 37 | const POINT_SIZE: PointSize = PointSize(0.75); |
| 38 | |
| 39 | const DARK_BLUE: Color = Color::Rgb(31, 120, 180); |
| 40 | const DARK_ORANGE: Color = Color::Rgb(255, 127, 0); |
| 41 | const DARK_RED: Color = Color::Rgb(227, 26, 28); |
| 42 | |
| 43 | fn debug_script(path: &Path, figure: &Figure) { |
| 44 | if crate::debug_enabled() { |
| 45 | let mut script_path = path.to_path_buf(); |
| 46 | script_path.set_extension("gnuplot" ); |
| 47 | info!("Writing gnuplot script to {:?}" , script_path); |
| 48 | let result = figure.save(script_path.as_path()); |
| 49 | if let Err(e) = result { |
| 50 | error!("Failed to write debug output: {}" , e); |
| 51 | } |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | /// Private |
| 56 | trait Append<T> { |
| 57 | /// Private |
| 58 | fn append_(self, item: T) -> Self; |
| 59 | } |
| 60 | |
| 61 | // NB I wish this was in the standard library |
| 62 | impl<T> Append<T> for Vec<T> { |
| 63 | fn append_(mut self, item: T) -> Vec<T> { |
| 64 | self.push(item); |
| 65 | self |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | #[derive(Default)] |
| 70 | pub(crate) struct Gnuplot { |
| 71 | process_list: Vec<Child>, |
| 72 | } |
| 73 | |
| 74 | impl Plotter for Gnuplot { |
| 75 | fn pdf(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) { |
| 76 | let size = ctx.size.map(|(w, h)| Size(w, h)); |
| 77 | self.process_list.push(if ctx.is_thumbnail { |
| 78 | if let Some(cmp) = data.comparison { |
| 79 | pdf_comparison_small( |
| 80 | ctx.id, |
| 81 | ctx.context, |
| 82 | data.formatter, |
| 83 | data.measurements, |
| 84 | cmp, |
| 85 | size, |
| 86 | ) |
| 87 | } else { |
| 88 | pdf_small(ctx.id, ctx.context, data.formatter, data.measurements, size) |
| 89 | } |
| 90 | } else if let Some(cmp) = data.comparison { |
| 91 | pdf_comparison( |
| 92 | ctx.id, |
| 93 | ctx.context, |
| 94 | data.formatter, |
| 95 | data.measurements, |
| 96 | cmp, |
| 97 | size, |
| 98 | ) |
| 99 | } else { |
| 100 | pdf(ctx.id, ctx.context, data.formatter, data.measurements, size) |
| 101 | }); |
| 102 | } |
| 103 | |
| 104 | fn regression(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) { |
| 105 | let size = ctx.size.map(|(w, h)| Size(w, h)); |
| 106 | self.process_list.push(if ctx.is_thumbnail { |
| 107 | if let Some(cmp) = data.comparison { |
| 108 | let base_data = Data::new(&cmp.base_iter_counts, &cmp.base_sample_times); |
| 109 | regression_comparison_small( |
| 110 | ctx.id, |
| 111 | ctx.context, |
| 112 | data.formatter, |
| 113 | data.measurements, |
| 114 | cmp, |
| 115 | &base_data, |
| 116 | size, |
| 117 | ) |
| 118 | } else { |
| 119 | regression_small(ctx.id, ctx.context, data.formatter, data.measurements, size) |
| 120 | } |
| 121 | } else if let Some(cmp) = data.comparison { |
| 122 | let base_data = Data::new(&cmp.base_iter_counts, &cmp.base_sample_times); |
| 123 | regression_comparison( |
| 124 | ctx.id, |
| 125 | ctx.context, |
| 126 | data.formatter, |
| 127 | data.measurements, |
| 128 | cmp, |
| 129 | &base_data, |
| 130 | size, |
| 131 | ) |
| 132 | } else { |
| 133 | regression(ctx.id, ctx.context, data.formatter, data.measurements, size) |
| 134 | }); |
| 135 | } |
| 136 | |
| 137 | fn iteration_times(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) { |
| 138 | let size = ctx.size.map(|(w, h)| Size(w, h)); |
| 139 | self.process_list.push(if ctx.is_thumbnail { |
| 140 | if let Some(cmp) = data.comparison { |
| 141 | iteration_times_comparison_small( |
| 142 | ctx.id, |
| 143 | ctx.context, |
| 144 | data.formatter, |
| 145 | data.measurements, |
| 146 | cmp, |
| 147 | size, |
| 148 | ) |
| 149 | } else { |
| 150 | iteration_times_small(ctx.id, ctx.context, data.formatter, data.measurements, size) |
| 151 | } |
| 152 | } else if let Some(cmp) = data.comparison { |
| 153 | iteration_times_comparison( |
| 154 | ctx.id, |
| 155 | ctx.context, |
| 156 | data.formatter, |
| 157 | data.measurements, |
| 158 | cmp, |
| 159 | size, |
| 160 | ) |
| 161 | } else { |
| 162 | iteration_times(ctx.id, ctx.context, data.formatter, data.measurements, size) |
| 163 | }); |
| 164 | } |
| 165 | |
| 166 | fn abs_distributions(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) { |
| 167 | let size = ctx.size.map(|(w, h)| Size(w, h)); |
| 168 | self.process_list.extend(abs_distributions( |
| 169 | ctx.id, |
| 170 | ctx.context, |
| 171 | data.formatter, |
| 172 | data.measurements, |
| 173 | size, |
| 174 | )); |
| 175 | } |
| 176 | |
| 177 | fn rel_distributions(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) { |
| 178 | let size = ctx.size.map(|(w, h)| Size(w, h)); |
| 179 | if let Some(cmp) = data.comparison { |
| 180 | self.process_list.extend(rel_distributions( |
| 181 | ctx.id, |
| 182 | ctx.context, |
| 183 | data.measurements, |
| 184 | cmp, |
| 185 | size, |
| 186 | )); |
| 187 | } else { |
| 188 | error!("Comparison data is not provided for a relative distribution figure" ); |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | fn t_test(&mut self, ctx: PlotContext<'_>, data: PlotData<'_>) { |
| 193 | let size = ctx.size.map(|(w, h)| Size(w, h)); |
| 194 | if let Some(cmp) = data.comparison { |
| 195 | self.process_list |
| 196 | .push(t_test(ctx.id, ctx.context, data.measurements, cmp, size)); |
| 197 | } else { |
| 198 | error!("Comparison data is not provided for t_test plot" ); |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | fn line_comparison( |
| 203 | &mut self, |
| 204 | ctx: PlotContext<'_>, |
| 205 | formatter: &dyn ValueFormatter, |
| 206 | all_curves: &[&(&BenchmarkId, Vec<f64>)], |
| 207 | value_type: ValueType, |
| 208 | ) { |
| 209 | let path = ctx.line_comparison_path(); |
| 210 | self.process_list.push(line_comparison( |
| 211 | formatter, |
| 212 | ctx.id.as_title(), |
| 213 | all_curves, |
| 214 | &path, |
| 215 | value_type, |
| 216 | ctx.context.plot_config.summary_scale, |
| 217 | )); |
| 218 | } |
| 219 | |
| 220 | fn violin( |
| 221 | &mut self, |
| 222 | ctx: PlotContext<'_>, |
| 223 | formatter: &dyn ValueFormatter, |
| 224 | all_curves: &[&(&BenchmarkId, Vec<f64>)], |
| 225 | ) { |
| 226 | let violin_path = ctx.violin_path(); |
| 227 | |
| 228 | self.process_list.push(violin( |
| 229 | formatter, |
| 230 | ctx.id.as_title(), |
| 231 | all_curves, |
| 232 | &violin_path, |
| 233 | ctx.context.plot_config.summary_scale, |
| 234 | )); |
| 235 | } |
| 236 | |
| 237 | fn wait(&mut self) { |
| 238 | let start = std::time::Instant::now(); |
| 239 | let child_count = self.process_list.len(); |
| 240 | for child in self.process_list.drain(..) { |
| 241 | match child.wait_with_output() { |
| 242 | Ok(ref out) if out.status.success() => {} |
| 243 | Ok(out) => error!("Error in Gnuplot: {}" , String::from_utf8_lossy(&out.stderr)), |
| 244 | Err(e) => error!("Got IO error while waiting for Gnuplot to complete: {}" , e), |
| 245 | } |
| 246 | } |
| 247 | let elapsed = &start.elapsed(); |
| 248 | info!( |
| 249 | "Waiting for {} gnuplot processes took {}" , |
| 250 | child_count, |
| 251 | format::time(elapsed.as_nanos() as f64) |
| 252 | ); |
| 253 | } |
| 254 | } |
| 255 | |