criterion/plot/plotters_backend/
summary.rs

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