1use super::{debug_script, gnuplot_escape};
2use super::{DARK_BLUE, DEFAULT_FONT, KDE_POINTS, LINEWIDTH, POINT_SIZE, SIZE};
3use crate::kde;
4use crate::measurement::ValueFormatter;
5use crate::report::{BenchmarkId, ValueType};
6use crate::stats::univariate::Sample;
7use crate::AxisScale;
8use criterion_plot::prelude::*;
9use itertools::Itertools;
10use std::cmp::Ordering;
11use std::path::{Path, PathBuf};
12use std::process::Child;
13
14const NUM_COLORS: usize = 8;
15static 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
26impl 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))]
36pub 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
124pub 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