1use super::*;
2use crate::estimate::Estimate;
3use crate::estimate::Statistic;
4use crate::measurement::ValueFormatter;
5use crate::report::{BenchmarkId, MeasurementData, ReportContext};
6use crate::stats::Distribution;
7
8fn abs_distribution(
9 id: &BenchmarkId,
10 context: &ReportContext,
11 formatter: &dyn ValueFormatter,
12 statistic: Statistic,
13 distribution: &Distribution<f64>,
14 estimate: &Estimate,
15 size: Option<(u32, u32)>,
16) {
17 let ci = &estimate.confidence_interval;
18 let typical = ci.upper_bound;
19 let mut ci_values = [ci.lower_bound, ci.upper_bound, estimate.point_estimate];
20 let unit = formatter.scale_values(typical, &mut ci_values);
21 let (lb, ub, point) = (ci_values[0], ci_values[1], ci_values[2]);
22
23 let start = lb - (ub - lb) / 9.;
24 let end = ub + (ub - lb) / 9.;
25 let mut scaled_xs: Vec<f64> = distribution.iter().cloned().collect();
26 let _ = formatter.scale_values(typical, &mut scaled_xs);
27 let scaled_xs_sample = Sample::new(&scaled_xs);
28 let (kde_xs, ys) = kde::sweep(scaled_xs_sample, KDE_POINTS, Some((start, end)));
29
30 // interpolate between two points of the KDE sweep to find the Y position at the point estimate.
31 let n_point = kde_xs
32 .iter()
33 .position(|&x| x >= point)
34 .unwrap_or(kde_xs.len() - 1)
35 .max(1); // Must be at least the second element or this will panic
36 let slope = (ys[n_point] - ys[n_point - 1]) / (kde_xs[n_point] - kde_xs[n_point - 1]);
37 let y_point = ys[n_point - 1] + (slope * (point - kde_xs[n_point - 1]));
38
39 let start = kde_xs
40 .iter()
41 .enumerate()
42 .find(|&(_, &x)| x >= lb)
43 .unwrap()
44 .0;
45 let end = kde_xs
46 .iter()
47 .enumerate()
48 .rev()
49 .find(|&(_, &x)| x <= ub)
50 .unwrap()
51 .0;
52 let len = end - start;
53
54 let kde_xs_sample = Sample::new(&kde_xs);
55
56 let path = context.report_path(id, &format!("{}.svg", statistic));
57 let root_area = SVGBackend::new(&path, size.unwrap_or(SIZE)).into_drawing_area();
58
59 let x_range = plotters::data::fitting_range(kde_xs_sample.iter());
60 let mut y_range = plotters::data::fitting_range(ys.iter());
61
62 y_range.end *= 1.1;
63
64 let mut chart = ChartBuilder::on(&root_area)
65 .margin((5).percent())
66 .caption(
67 format!("{}:{}", id.as_title(), statistic),
68 (DEFAULT_FONT, 20),
69 )
70 .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60))
71 .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40))
72 .build_cartesian_2d(x_range, y_range)
73 .unwrap();
74
75 chart
76 .configure_mesh()
77 .disable_mesh()
78 .x_desc(format!("Average time ({})", unit))
79 .y_desc("Density (a.u.)")
80 .x_label_formatter(&|&v| pretty_print_float(v, true))
81 .y_label_formatter(&|&v| pretty_print_float(v, true))
82 .draw()
83 .unwrap();
84
85 chart
86 .draw_series(LineSeries::new(
87 kde_xs.iter().zip(ys.iter()).map(|(&x, &y)| (x, y)),
88 DARK_BLUE,
89 ))
90 .unwrap()
91 .label("Bootstrap distribution")
92 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
93
94 chart
95 .draw_series(AreaSeries::new(
96 kde_xs
97 .iter()
98 .zip(ys.iter())
99 .skip(start)
100 .take(len)
101 .map(|(&x, &y)| (x, y)),
102 0.0,
103 DARK_BLUE.mix(0.25).filled().stroke_width(3),
104 ))
105 .unwrap()
106 .label("Confidence interval")
107 .legend(|(x, y)| {
108 Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.25).filled())
109 });
110
111 chart
112 .draw_series(std::iter::once(PathElement::new(
113 vec![(point, 0.0), (point, y_point)],
114 DARK_BLUE.filled().stroke_width(3),
115 )))
116 .unwrap()
117 .label("Point estimate")
118 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
119
120 chart
121 .configure_series_labels()
122 .position(SeriesLabelPosition::UpperRight)
123 .draw()
124 .unwrap();
125}
126
127pub(crate) fn abs_distributions(
128 id: &BenchmarkId,
129 context: &ReportContext,
130 formatter: &dyn ValueFormatter,
131 measurements: &MeasurementData<'_>,
132 size: Option<(u32, u32)>,
133) {
134 crate::plot::REPORT_STATS
135 .iter()
136 .filter_map(|stat| {
137 measurements.distributions.get(*stat).and_then(|dist| {
138 measurements
139 .absolute_estimates
140 .get(*stat)
141 .map(|est| (*stat, dist, est))
142 })
143 })
144 .for_each(|(statistic, distribution, estimate)| {
145 abs_distribution(
146 id,
147 context,
148 formatter,
149 statistic,
150 distribution,
151 estimate,
152 size,
153 )
154 })
155}
156
157fn rel_distribution(
158 id: &BenchmarkId,
159 context: &ReportContext,
160 statistic: Statistic,
161 distribution: &Distribution<f64>,
162 estimate: &Estimate,
163 noise_threshold: f64,
164 size: Option<(u32, u32)>,
165) {
166 let ci = &estimate.confidence_interval;
167 let (lb, ub) = (ci.lower_bound, ci.upper_bound);
168
169 let start = lb - (ub - lb) / 9.;
170 let end = ub + (ub - lb) / 9.;
171 let (xs, ys) = kde::sweep(distribution, KDE_POINTS, Some((start, end)));
172 let xs_ = Sample::new(&xs);
173
174 // interpolate between two points of the KDE sweep to find the Y position at the point estimate.
175 let point = estimate.point_estimate;
176 let n_point = xs
177 .iter()
178 .position(|&x| x >= point)
179 .unwrap_or(ys.len() - 1)
180 .max(1);
181 let slope = (ys[n_point] - ys[n_point - 1]) / (xs[n_point] - xs[n_point - 1]);
182 let y_point = ys[n_point - 1] + (slope * (point - xs[n_point - 1]));
183
184 let start = xs.iter().enumerate().find(|&(_, &x)| x >= lb).unwrap().0;
185 let end = xs
186 .iter()
187 .enumerate()
188 .rev()
189 .find(|&(_, &x)| x <= ub)
190 .unwrap()
191 .0;
192 let len = end - start;
193
194 let x_min = xs_.min();
195 let x_max = xs_.max();
196
197 let (fc_start, fc_end) = if noise_threshold < x_min || -noise_threshold > x_max {
198 let middle = (x_min + x_max) / 2.;
199
200 (middle, middle)
201 } else {
202 (
203 if -noise_threshold < x_min {
204 x_min
205 } else {
206 -noise_threshold
207 },
208 if noise_threshold > x_max {
209 x_max
210 } else {
211 noise_threshold
212 },
213 )
214 };
215 let y_range = plotters::data::fitting_range(ys.iter());
216 let path = context.report_path(id, &format!("change/{}.svg", statistic));
217 let root_area = SVGBackend::new(&path, size.unwrap_or(SIZE)).into_drawing_area();
218
219 let mut chart = ChartBuilder::on(&root_area)
220 .margin((5).percent())
221 .caption(
222 format!("{}:{}", id.as_title(), statistic),
223 (DEFAULT_FONT, 20),
224 )
225 .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60))
226 .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40))
227 .build_cartesian_2d(x_min..x_max, y_range.clone())
228 .unwrap();
229
230 chart
231 .configure_mesh()
232 .disable_mesh()
233 .x_desc("Relative change (%)")
234 .y_desc("Density (a.u.)")
235 .x_label_formatter(&|&v| pretty_print_float(v, true))
236 .y_label_formatter(&|&v| pretty_print_float(v, true))
237 .draw()
238 .unwrap();
239
240 chart
241 .draw_series(LineSeries::new(
242 xs.iter().zip(ys.iter()).map(|(x, y)| (*x, *y)),
243 DARK_BLUE,
244 ))
245 .unwrap()
246 .label("Bootstrap distribution")
247 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
248
249 chart
250 .draw_series(AreaSeries::new(
251 xs.iter()
252 .zip(ys.iter())
253 .skip(start)
254 .take(len)
255 .map(|(x, y)| (*x, *y)),
256 0.0,
257 DARK_BLUE.mix(0.25).filled().stroke_width(3),
258 ))
259 .unwrap()
260 .label("Confidence interval")
261 .legend(|(x, y)| {
262 Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.25).filled())
263 });
264
265 chart
266 .draw_series(std::iter::once(PathElement::new(
267 vec![(point, 0.0), (point, y_point)],
268 DARK_BLUE.filled().stroke_width(3),
269 )))
270 .unwrap()
271 .label("Point estimate")
272 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
273
274 chart
275 .draw_series(std::iter::once(Rectangle::new(
276 [(fc_start, y_range.start), (fc_end, y_range.end)],
277 DARK_RED.mix(0.1).filled(),
278 )))
279 .unwrap()
280 .label("Noise threshold")
281 .legend(|(x, y)| {
282 Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_RED.mix(0.25).filled())
283 });
284 chart
285 .configure_series_labels()
286 .position(SeriesLabelPosition::UpperRight)
287 .draw()
288 .unwrap();
289}
290
291pub(crate) fn rel_distributions(
292 id: &BenchmarkId,
293 context: &ReportContext,
294 _measurements: &MeasurementData<'_>,
295 comparison: &ComparisonData,
296 size: Option<(u32, u32)>,
297) {
298 crate::plot::CHANGE_STATS.iter().for_each(|&statistic| {
299 rel_distribution(
300 id,
301 context,
302 statistic,
303 comparison.relative_distributions.get(statistic),
304 comparison.relative_estimates.get(statistic),
305 comparison.noise_threshold,
306 size,
307 )
308 });
309}
310