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 | |