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 {
to_gnuplot(self) -> Scale27     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))]
line_comparison( formatter: &dyn ValueFormatter, title: &str, all_curves: &[&(&BenchmarkId, Vec<f64>)], path: &Path, value_type: ValueType, axis_scale: AxisScale, ) -> Child36 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(|&&(_, ref 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(|&&&(ref id, _)| &id.function_id) {
87         let mut tuples: Vec<_> = group
88             .map(|&&(ref 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 
violin( formatter: &dyn ValueFormatter, title: &str, all_curves: &[&(&BenchmarkId, Vec<f64>)], path: &Path, axis_scale: AxisScale, ) -> Child124 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(|&&(_, ref 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
148         .iter()
149         .flat_map(|&(ref x, _)| x.iter())
150         .filter(|&&x| x > 0.);
151     let (mut min, mut max) = {
152         let &first = xs.next().unwrap();
153         (first, first)
154     };
155     for &e in xs {
156         if e < min {
157             min = e;
158         } else if e > max {
159             max = e;
160         }
161     }
162     let mut one = [1.0];
163     // Scale the X axis units. Use the middle as a "typical value". E.g. if
164     // it is 0.002 s then this function will decide that milliseconds are an
165     // appropriate unit. It will multiple `one` by 1000, and return "ms".
166     let unit = formatter.scale_values((min + max) / 2.0, &mut one);
167 
168     let tics = || (0..).map(|x| (f64::from(x)) + 0.5);
169     let size = Size(1280, 200 + (25 * all_curves.len()));
170     let mut f = Figure::new();
171     f.set(Font(DEFAULT_FONT))
172         .set(size)
173         .set(Title(format!("{}: Violin plot", gnuplot_escape(title))))
174         .configure(Axis::BottomX, |a| {
175             a.configure(Grid::Major, |g| g.show())
176                 .configure(Grid::Minor, |g| g.hide())
177                 .set(Range::Limits(0., max as f64 * one[0]))
178                 .set(Label(format!("Average time ({})", unit)))
179                 .set(axis_scale.to_gnuplot())
180         })
181         .configure(Axis::LeftY, |a| {
182             a.set(Label("Input"))
183                 .set(Range::Limits(0., all_curves.len() as f64))
184                 .set(TicLabels {
185                     positions: tics(),
186                     labels: all_curves
187                         .iter()
188                         .map(|&&(ref id, _)| gnuplot_escape(id.as_title())),
189                 })
190         });
191 
192     let mut is_first = true;
193     for (i, &(ref x, ref y)) in kdes.iter().enumerate() {
194         let i = i as f64 + 0.5;
195         let y1: Vec<_> = y.iter().map(|&y| i + y * 0.45).collect();
196         let y2: Vec<_> = y.iter().map(|&y| i - y * 0.45).collect();
197 
198         let x: Vec<_> = x.iter().map(|&x| x * one[0]).collect();
199 
200         f.plot(FilledCurve { x, y1, y2 }, |c| {
201             if is_first {
202                 is_first = false;
203 
204                 c.set(DARK_BLUE).set(Label("PDF"))
205             } else {
206                 c.set(DARK_BLUE)
207             }
208         });
209     }
210     debug_script(&path, &f);
211     f.set(Output(path)).draw().unwrap()
212 }
213