| 1 | use super::{debug_script, gnuplot_escape}; |
| 2 | use super::{DARK_BLUE, DEFAULT_FONT, KDE_POINTS, LINEWIDTH, POINT_SIZE, SIZE}; |
| 3 | use crate::kde; |
| 4 | use crate::measurement::ValueFormatter; |
| 5 | use crate::report::{BenchmarkId, ValueType}; |
| 6 | use crate::stats::univariate::Sample; |
| 7 | use crate::AxisScale; |
| 8 | use criterion_plot::prelude::*; |
| 9 | use itertools::Itertools; |
| 10 | use std::cmp::Ordering; |
| 11 | use std::path::{Path, PathBuf}; |
| 12 | use std::process::Child; |
| 13 | |
| 14 | const NUM_COLORS: usize = 8; |
| 15 | static COMPARISON_COLORS: [Color; NUM_COLORS] = [ |
| 16 | Color::Rgb(178, 34, 34), |
| 17 | Color::Rgb(46, 139, 87), |
| 18 | Color::Rgb(0, 139, 139), |
| 19 | Color::Rgb(255, 215, 0), |
| 20 | Color::Rgb(0, 0, 139), |
| 21 | Color::Rgb(220, 20, 60), |
| 22 | Color::Rgb(139, 0, 139), |
| 23 | Color::Rgb(0, 255, 127), |
| 24 | ]; |
| 25 | |
| 26 | impl AxisScale { |
| 27 | fn to_gnuplot(self) -> Scale { |
| 28 | match self { |
| 29 | AxisScale::Linear => Scale::Linear, |
| 30 | AxisScale::Logarithmic => Scale::Logarithmic, |
| 31 | } |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | #[cfg_attr (feature = "cargo-clippy" , allow(clippy::explicit_counter_loop))] |
| 36 | pub fn line_comparison( |
| 37 | formatter: &dyn ValueFormatter, |
| 38 | title: &str, |
| 39 | all_curves: &[&(&BenchmarkId, Vec<f64>)], |
| 40 | path: &Path, |
| 41 | value_type: ValueType, |
| 42 | axis_scale: AxisScale, |
| 43 | ) -> Child { |
| 44 | let path = PathBuf::from(path); |
| 45 | let mut f = Figure::new(); |
| 46 | |
| 47 | let input_suffix = match value_type { |
| 48 | ValueType::Bytes => " Size (Bytes)" , |
| 49 | ValueType::Elements => " Size (Elements)" , |
| 50 | ValueType::Value => "" , |
| 51 | }; |
| 52 | |
| 53 | f.set(Font(DEFAULT_FONT)) |
| 54 | .set(SIZE) |
| 55 | .configure(Key, |k| { |
| 56 | k.set(Justification::Left) |
| 57 | .set(Order::SampleText) |
| 58 | .set(Position::Outside(Vertical::Top, Horizontal::Right)) |
| 59 | }) |
| 60 | .set(Title(format!("{}: Comparison" , gnuplot_escape(title)))) |
| 61 | .configure(Axis::BottomX, |a| { |
| 62 | a.set(Label(format!("Input{}" , input_suffix))) |
| 63 | .set(axis_scale.to_gnuplot()) |
| 64 | }); |
| 65 | |
| 66 | let mut i = 0; |
| 67 | |
| 68 | let max = all_curves |
| 69 | .iter() |
| 70 | .map(|&(_, data)| Sample::new(data).mean()) |
| 71 | .fold(::std::f64::NAN, f64::max); |
| 72 | |
| 73 | let mut dummy = [1.0]; |
| 74 | let unit = formatter.scale_values(max, &mut dummy); |
| 75 | |
| 76 | f.configure(Axis::LeftY, |a| { |
| 77 | a.configure(Grid::Major, |g| g.show()) |
| 78 | .configure(Grid::Minor, |g| g.hide()) |
| 79 | .set(Label(format!("Average time ({})" , unit))) |
| 80 | .set(axis_scale.to_gnuplot()) |
| 81 | }); |
| 82 | |
| 83 | // This assumes the curves are sorted. It also assumes that the benchmark IDs all have numeric |
| 84 | // values or throughputs and that value is sensible (ie. not a mix of bytes and elements |
| 85 | // or whatnot) |
| 86 | for (key, group) in &all_curves.iter().group_by(|&&&(id, _)| &id.function_id) { |
| 87 | let mut tuples: Vec<_> = group |
| 88 | .map(|&&(id, ref sample)| { |
| 89 | // Unwrap is fine here because it will only fail if the assumptions above are not true |
| 90 | // ie. programmer error. |
| 91 | let x = id.as_number().unwrap(); |
| 92 | let y = Sample::new(sample).mean(); |
| 93 | |
| 94 | (x, y) |
| 95 | }) |
| 96 | .collect(); |
| 97 | tuples.sort_by(|&(ax, _), &(bx, _)| (ax.partial_cmp(&bx).unwrap_or(Ordering::Less))); |
| 98 | let (xs, mut ys): (Vec<_>, Vec<_>) = tuples.into_iter().unzip(); |
| 99 | formatter.scale_values(max, &mut ys); |
| 100 | |
| 101 | let function_name = key.as_ref().map(|string| gnuplot_escape(string)); |
| 102 | |
| 103 | f.plot(Lines { x: &xs, y: &ys }, |c| { |
| 104 | if let Some(name) = function_name { |
| 105 | c.set(Label(name)); |
| 106 | } |
| 107 | c.set(LINEWIDTH) |
| 108 | .set(LineType::Solid) |
| 109 | .set(COMPARISON_COLORS[i % NUM_COLORS]) |
| 110 | }) |
| 111 | .plot(Points { x: &xs, y: &ys }, |p| { |
| 112 | p.set(PointType::FilledCircle) |
| 113 | .set(POINT_SIZE) |
| 114 | .set(COMPARISON_COLORS[i % NUM_COLORS]) |
| 115 | }); |
| 116 | |
| 117 | i += 1; |
| 118 | } |
| 119 | |
| 120 | debug_script(&path, &f); |
| 121 | f.set(Output(path)).draw().unwrap() |
| 122 | } |
| 123 | |
| 124 | pub fn violin( |
| 125 | formatter: &dyn ValueFormatter, |
| 126 | title: &str, |
| 127 | all_curves: &[&(&BenchmarkId, Vec<f64>)], |
| 128 | path: &Path, |
| 129 | axis_scale: AxisScale, |
| 130 | ) -> Child { |
| 131 | let path = PathBuf::from(&path); |
| 132 | let all_curves_vec = all_curves.iter().rev().cloned().collect::<Vec<_>>(); |
| 133 | let all_curves: &[&(&BenchmarkId, Vec<f64>)] = &all_curves_vec; |
| 134 | |
| 135 | let kdes = all_curves |
| 136 | .iter() |
| 137 | .map(|&(_, sample)| { |
| 138 | let (x, mut y) = kde::sweep(Sample::new(sample), KDE_POINTS, None); |
| 139 | let y_max = Sample::new(&y).max(); |
| 140 | for y in y.iter_mut() { |
| 141 | *y /= y_max; |
| 142 | } |
| 143 | |
| 144 | (x, y) |
| 145 | }) |
| 146 | .collect::<Vec<_>>(); |
| 147 | let mut xs = kdes.iter().flat_map(|(x, _)| x.iter()).filter(|&&x| x > 0.); |
| 148 | let (mut min, mut max) = { |
| 149 | let &first = xs.next().unwrap(); |
| 150 | (first, first) |
| 151 | }; |
| 152 | for &e in xs { |
| 153 | if e < min { |
| 154 | min = e; |
| 155 | } else if e > max { |
| 156 | max = e; |
| 157 | } |
| 158 | } |
| 159 | let mut one = [1.0]; |
| 160 | // Scale the X axis units. Use the middle as a "typical value". E.g. if |
| 161 | // it is 0.002 s then this function will decide that milliseconds are an |
| 162 | // appropriate unit. It will multiple `one` by 1000, and return "ms". |
| 163 | let unit = formatter.scale_values((min + max) / 2.0, &mut one); |
| 164 | |
| 165 | let tics = || (0..).map(|x| (f64::from(x)) + 0.5); |
| 166 | let size = Size(1280, 200 + (25 * all_curves.len())); |
| 167 | let mut f = Figure::new(); |
| 168 | f.set(Font(DEFAULT_FONT)) |
| 169 | .set(size) |
| 170 | .set(Title(format!("{}: Violin plot" , gnuplot_escape(title)))) |
| 171 | .configure(Axis::BottomX, |a| { |
| 172 | a.configure(Grid::Major, |g| g.show()) |
| 173 | .configure(Grid::Minor, |g| g.hide()) |
| 174 | .set(Range::Limits(0., max * one[0])) |
| 175 | .set(Label(format!("Average time ({})" , unit))) |
| 176 | .set(axis_scale.to_gnuplot()) |
| 177 | }) |
| 178 | .configure(Axis::LeftY, |a| { |
| 179 | a.set(Label("Input" )) |
| 180 | .set(Range::Limits(0., all_curves.len() as f64)) |
| 181 | .set(TicLabels { |
| 182 | positions: tics(), |
| 183 | labels: all_curves |
| 184 | .iter() |
| 185 | .map(|&&(id, _)| gnuplot_escape(id.as_title())), |
| 186 | }) |
| 187 | }); |
| 188 | |
| 189 | let mut is_first = true; |
| 190 | for (i, (x, y)) in kdes.iter().enumerate() { |
| 191 | let i = i as f64 + 0.5; |
| 192 | let y1: Vec<_> = y.iter().map(|&y| i + y * 0.45).collect(); |
| 193 | let y2: Vec<_> = y.iter().map(|&y| i - y * 0.45).collect(); |
| 194 | |
| 195 | let x: Vec<_> = x.iter().map(|&x| x * one[0]).collect(); |
| 196 | |
| 197 | f.plot(FilledCurve { x, y1, y2 }, |c| { |
| 198 | if is_first { |
| 199 | is_first = false; |
| 200 | |
| 201 | c.set(DARK_BLUE).set(Label("PDF" )) |
| 202 | } else { |
| 203 | c.set(DARK_BLUE) |
| 204 | } |
| 205 | }); |
| 206 | } |
| 207 | debug_script(&path, &f); |
| 208 | f.set(Output(path)).draw().unwrap() |
| 209 | } |
| 210 | |