criterion/plot/gnuplot_backend/
summary.rs

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}