criterion/plot/plotters_backend/
summary.rs

1use {
2    super::*,
3    crate::AxisScale,
4    itertools::Itertools,
5    plotters::coord::{
6        ranged1d::{AsRangedCoord, ValueFormatter as PlottersValueFormatter},
7        Shift,
8    },
9    std::{cmp::Ordering, path::Path},
10};
11
12const NUM_COLORS: usize = 8;
13static COMPARISON_COLORS: [RGBColor; NUM_COLORS] = [
14    RGBColor(178, 34, 34),
15    RGBColor(46, 139, 87),
16    RGBColor(0, 139, 139),
17    RGBColor(255, 215, 0),
18    RGBColor(0, 0, 139),
19    RGBColor(220, 20, 60),
20    RGBColor(139, 0, 139),
21    RGBColor(0, 255, 127),
22];
23
24pub(crate) fn line_comparison(
25    line_cfg: LinePlotConfig,
26    formatter: &dyn ValueFormatter,
27    title: &str,
28    all_curves: &[&(&BenchmarkId, Vec<f64>)],
29    path: &Path,
30    value_type: ValueType,
31    axis_scale: AxisScale,
32) {
33    let (unit, series_data) = line_comparison_series_data(line_cfg, formatter, all_curves);
34
35    let x_range =
36        plotters::data::fitting_range(series_data.iter().flat_map(|(_, xs, _)| xs.iter()));
37    let y_range =
38        plotters::data::fitting_range(series_data.iter().flat_map(|(_, _, ys)| ys.iter()));
39    let root_area = SVGBackend::new(&path, SIZE)
40        .into_drawing_area()
41        .titled(&format!("{}: Comparison", title), (DEFAULT_FONT, 20))
42        .unwrap();
43
44    match axis_scale {
45        AxisScale::Linear => draw_line_comparison_figure(
46            line_cfg,
47            root_area,
48            unit,
49            x_range,
50            y_range,
51            value_type,
52            series_data,
53        ),
54        AxisScale::Logarithmic => draw_line_comparison_figure(
55            line_cfg,
56            root_area,
57            unit,
58            x_range.log_scale(),
59            y_range.log_scale(),
60            value_type,
61            series_data,
62        ),
63    }
64}
65
66fn draw_line_comparison_figure<XR: AsRangedCoord<Value = f64>, YR: AsRangedCoord<Value = f64>>(
67    line_cfg: LinePlotConfig,
68    root_area: DrawingArea<SVGBackend, Shift>,
69    y_unit: &str,
70    x_range: XR,
71    y_range: YR,
72    value_type: ValueType,
73    data: Vec<(Option<&String>, Vec<f64>, Vec<f64>)>,
74) where
75    XR::CoordDescType: PlottersValueFormatter<f64>,
76    YR::CoordDescType: PlottersValueFormatter<f64>,
77{
78    let input_suffix = match value_type {
79        ValueType::Bytes => " Size (Bytes)",
80        ValueType::Elements => " Size (Elements)",
81        ValueType::Bits => " Size (Bits)",
82        ValueType::Value => "",
83    };
84
85    let mut chart = ChartBuilder::on(&root_area)
86        .margin((5).percent())
87        .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60))
88        .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40))
89        .build_cartesian_2d(x_range, y_range)
90        .unwrap();
91
92    chart
93        .configure_mesh()
94        .disable_mesh()
95        .x_desc(format!("Input{}", input_suffix))
96        .y_desc(format!("Average {} ({})", line_cfg.label, y_unit))
97        .draw()
98        .unwrap();
99
100    for (id, (name, xs, ys)) in (0..).zip(data) {
101        let series = chart
102            .draw_series(
103                LineSeries::new(
104                    xs.into_iter().zip(ys),
105                    COMPARISON_COLORS[id % NUM_COLORS].filled(),
106                )
107                .point_size(POINT_SIZE),
108            )
109            .unwrap();
110        if let Some(name) = name {
111            series.label(name).legend(move |(x, y)| {
112                Rectangle::new(
113                    [(x, y - 5), (x + 20, y + 5)],
114                    COMPARISON_COLORS[id % NUM_COLORS].filled(),
115                )
116            });
117        }
118    }
119
120    chart
121        .configure_series_labels()
122        .position(SeriesLabelPosition::UpperLeft)
123        .draw()
124        .unwrap();
125}
126
127#[allow(clippy::type_complexity)]
128fn line_comparison_series_data<'a>(
129    line_cfg: LinePlotConfig,
130    formatter: &dyn ValueFormatter,
131    all_curves: &[&(&'a BenchmarkId, Vec<f64>)],
132) -> (&'static str, Vec<(Option<&'a String>, Vec<f64>, Vec<f64>)>) {
133    let (max_id, max) = all_curves
134        .iter()
135        .map(|&(id, data)| (*id, Sample::new(data).mean()))
136        .fold(None, |prev: Option<(&BenchmarkId, f64)>, next| match prev {
137            Some(prev) if prev.1 >= next.1 => Some(prev),
138            _ => Some(next),
139        })
140        .unwrap();
141
142    let mut max_formatted = [max];
143    let unit = (line_cfg.scale)(formatter, max_id, max, max_id, &mut max_formatted);
144
145    let mut series_data = vec![];
146
147    // This assumes the curves are sorted. It also assumes that the benchmark IDs all have numeric
148    // values or throughputs and that value is sensible (ie. not a mix of bytes and elements
149    // or whatnot)
150    for (key, group) in &all_curves.iter().chunk_by(|&&&(id, _)| &id.function_id) {
151        let mut tuples: Vec<_> = group
152            .map(|&&(id, ref sample)| {
153                // Unwrap is fine here because it will only fail if the assumptions above are not true
154                // ie. programmer error.
155                let x = id.as_number().unwrap();
156                let mut y = [Sample::new(sample).mean()];
157
158                (line_cfg.scale)(formatter, max_id, max, id, &mut y);
159
160                (x, y[0])
161            })
162            .collect();
163        tuples.sort_by(|&(ax, _), &(bx, _)| ax.partial_cmp(&bx).unwrap_or(Ordering::Less));
164        let function_name = key.as_ref();
165        let (xs, ys): (Vec<_>, Vec<_>) = tuples.into_iter().unzip();
166        series_data.push((function_name, xs, ys));
167    }
168    (unit, series_data)
169}
170
171pub fn violin(
172    formatter: &dyn ValueFormatter,
173    title: &str,
174    all_curves: &[&(&BenchmarkId, Vec<f64>)],
175    path: &Path,
176    axis_scale: AxisScale,
177) {
178    let all_curves_vec = all_curves.iter().rev().cloned().collect::<Vec<_>>();
179    let all_curves: &[&(&BenchmarkId, Vec<f64>)] = &all_curves_vec;
180
181    let mut kdes = all_curves
182        .iter()
183        .map(|&&(id, ref sample)| {
184            let (x, mut y) = kde::sweep(Sample::new(sample), KDE_POINTS, None);
185            let y_max = Sample::new(&y).max();
186            for y in y.iter_mut() {
187                *y /= y_max;
188            }
189
190            (id.as_title(), x, y)
191        })
192        .collect::<Vec<_>>();
193
194    let mut xs = kdes
195        .iter()
196        .flat_map(|(_, x, _)| x.iter())
197        .filter(|&&x| x > 0.);
198    let (mut min, mut max) = {
199        let &first = xs.next().unwrap();
200        (first, first)
201    };
202    for &e in xs {
203        if e < min {
204            min = e;
205        } else if e > max {
206            max = e;
207        }
208    }
209    let mut dummy = [1.0];
210    let unit = formatter.scale_values(max, &mut dummy);
211    kdes.iter_mut().for_each(|&mut (_, ref mut xs, _)| {
212        formatter.scale_values(max, xs);
213    });
214
215    let mut x_range = plotters::data::fitting_range(kdes.iter().flat_map(|(_, xs, _)| xs.iter()));
216    x_range.start = 0.0;
217    let y_range = -0.5..all_curves.len() as f64 - 0.5;
218
219    let size = (960, 150 + (18 * all_curves.len() as u32));
220
221    let root_area = SVGBackend::new(&path, size)
222        .into_drawing_area()
223        .titled(&format!("{}: Violin plot", title), (DEFAULT_FONT, 20))
224        .unwrap();
225
226    match axis_scale {
227        AxisScale::Linear => draw_violin_figure(root_area, unit, x_range, y_range, kdes),
228        AxisScale::Logarithmic => {
229            draw_violin_figure(root_area, unit, x_range.log_scale(), y_range, kdes);
230        }
231    }
232}
233
234#[allow(clippy::type_complexity)]
235fn draw_violin_figure<XR: AsRangedCoord<Value = f64>, YR: AsRangedCoord<Value = f64>>(
236    root_area: DrawingArea<SVGBackend, Shift>,
237    unit: &'static str,
238    x_range: XR,
239    y_range: YR,
240    data: Vec<(&str, Box<[f64]>, Box<[f64]>)>,
241) where
242    XR::CoordDescType: PlottersValueFormatter<f64>,
243    YR::CoordDescType: PlottersValueFormatter<f64>,
244{
245    let mut chart = ChartBuilder::on(&root_area)
246        .margin((5).percent())
247        .set_label_area_size(LabelAreaPosition::Left, (10).percent_width().min(60))
248        .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_width().min(40))
249        .build_cartesian_2d(x_range, y_range)
250        .unwrap();
251
252    chart
253        .configure_mesh()
254        .disable_mesh()
255        .y_desc("Input")
256        .x_desc(format!("Average time ({})", unit))
257        .y_label_style((DEFAULT_FONT, 10))
258        .y_label_formatter(&|v: &f64| data[v.round() as usize].0.to_string())
259        .y_labels(data.len())
260        .draw()
261        .unwrap();
262
263    for (i, (_, x, y)) in data.into_iter().enumerate() {
264        let base = i as f64;
265
266        chart
267            .draw_series(AreaSeries::new(
268                x.iter().zip(y.iter()).map(|(x, y)| (*x, base + *y / 2.0)),
269                base,
270                DARK_BLUE,
271            ))
272            .unwrap();
273
274        chart
275            .draw_series(AreaSeries::new(
276                x.iter().zip(y.iter()).map(|(x, y)| (*x, base - *y / 2.0)),
277                base,
278                DARK_BLUE,
279            ))
280            .unwrap();
281    }
282}