criterion/plot/gnuplot_backend/
summary.rs

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