referrerpolicy=no-referrer-when-downgrade

frame_benchmarking/
analysis.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Tools for analyzing the benchmark results.
19
20use crate::BenchmarkResult;
21use std::collections::BTreeMap;
22
23pub struct Analysis {
24	pub base: u128,
25	pub slopes: Vec<u128>,
26	pub names: Vec<String>,
27	pub value_dists: Option<Vec<(Vec<u32>, u128, u128)>>,
28	pub errors: Option<Vec<u128>>,
29	pub minimum: u128,
30	selector: BenchmarkSelector,
31}
32
33#[derive(Clone, Copy)]
34pub enum BenchmarkSelector {
35	ExtrinsicTime,
36	StorageRootTime,
37	Reads,
38	Writes,
39	ProofSize,
40}
41
42/// Multiplies the value by 1000 and converts it into an u128.
43fn mul_1000_into_u128(value: f64) -> u128 {
44	// This is slightly more precise than the alternative of `(value * 1000.0) as u128`.
45	(value as u128)
46		.saturating_mul(1000)
47		.saturating_add((value.fract() * 1000.0) as u128)
48}
49
50impl BenchmarkSelector {
51	fn scale_and_cast_weight(self, value: f64, round_up: bool) -> u128 {
52		if let BenchmarkSelector::ExtrinsicTime = self {
53			// We add a very slight bias here to counteract the numerical imprecision of the linear
54			// regression where due to rounding issues it can emit a number like `2999999.999999998`
55			// which we most certainly always want to round up instead of truncating.
56			mul_1000_into_u128(value + 0.000_000_005)
57		} else {
58			if round_up {
59				(value + 0.5) as u128
60			} else {
61				value as u128
62			}
63		}
64	}
65
66	fn scale_weight(self, value: u128) -> u128 {
67		if let BenchmarkSelector::ExtrinsicTime = self {
68			value.saturating_mul(1000)
69		} else {
70			value
71		}
72	}
73
74	fn nanos_from_weight(self, value: u128) -> u128 {
75		if let BenchmarkSelector::ExtrinsicTime = self {
76			value / 1000
77		} else {
78			value
79		}
80	}
81
82	fn get_value(self, result: &BenchmarkResult) -> u128 {
83		match self {
84			BenchmarkSelector::ExtrinsicTime => result.extrinsic_time,
85			BenchmarkSelector::StorageRootTime => result.storage_root_time,
86			BenchmarkSelector::Reads => result.reads.into(),
87			BenchmarkSelector::Writes => result.writes.into(),
88			BenchmarkSelector::ProofSize => result.proof_size.into(),
89		}
90	}
91
92	fn get_minimum(self, results: &[BenchmarkResult]) -> u128 {
93		results
94			.iter()
95			.map(|result| self.get_value(result))
96			.min()
97			.expect("results cannot be empty")
98	}
99}
100
101#[derive(Debug)]
102pub enum AnalysisChoice {
103	/// Use minimum squares regression for analyzing the benchmarking results.
104	MinSquares,
105	/// Use median slopes for analyzing the benchmarking results.
106	MedianSlopes,
107	/// Use the maximum values among all other analysis functions for the benchmarking results.
108	Max,
109}
110
111impl Default for AnalysisChoice {
112	fn default() -> Self {
113		AnalysisChoice::MinSquares
114	}
115}
116
117impl TryFrom<Option<String>> for AnalysisChoice {
118	type Error = &'static str;
119
120	fn try_from(s: Option<String>) -> Result<Self, Self::Error> {
121		match s {
122			None => Ok(AnalysisChoice::default()),
123			Some(i) => match &i[..] {
124				"min-squares" | "min_squares" => Ok(AnalysisChoice::MinSquares),
125				"median-slopes" | "median_slopes" => Ok(AnalysisChoice::MedianSlopes),
126				"max" => Ok(AnalysisChoice::Max),
127				_ => Err("invalid analysis string"),
128			},
129		}
130	}
131}
132
133fn raw_linear_regression(
134	xs: &[f64],
135	ys: &[f64],
136	x_vars: usize,
137	with_intercept: bool,
138) -> Option<(f64, Vec<f64>, Vec<f64>)> {
139	let mut data: Vec<f64> = Vec::new();
140
141	// Here we build a raw matrix of linear equations for the `linregress` crate to solve for us
142	// and build a linear regression model around it.
143	//
144	// Each row of the matrix contains as the first column the actual value which we want
145	// the model to predict for us (the `y`), and the rest of the columns contain the input
146	// parameters on which the model will base its predictions on (the `xs`).
147	//
148	// In machine learning terms this is essentially the training data for the model.
149	//
150	// As a special case the very first input parameter represents the constant factor
151	// of the linear equation: the so called "intercept value". Since it's supposed to
152	// be constant we can just put a dummy input parameter of either a `1` (in case we want it)
153	// or a `0` (in case we do not).
154	for (&y, xs) in ys.iter().zip(xs.chunks_exact(x_vars)) {
155		data.push(y);
156		if with_intercept {
157			data.push(1.0);
158		} else {
159			data.push(0.0);
160		}
161		data.extend(xs);
162	}
163	let model = linregress::fit_low_level_regression_model(&data, ys.len(), x_vars + 2).ok()?;
164	Some((model.parameters()[0], model.parameters()[1..].to_vec(), model.se().to_vec()))
165}
166
167fn linear_regression(
168	xs: Vec<f64>,
169	mut ys: Vec<f64>,
170	x_vars: usize,
171) -> Option<(f64, Vec<f64>, Vec<f64>)> {
172	let (intercept, params, errors) = raw_linear_regression(&xs, &ys, x_vars, true)?;
173	if intercept >= -0.0001 {
174		// The intercept is positive, or is effectively zero.
175		return Some((intercept, params, errors[1..].to_vec()))
176	}
177
178	// The intercept is negative.
179	// The weights must be always positive, so we can't have that.
180
181	let mut min = ys[0];
182	for &value in &ys {
183		if value < min {
184			min = value;
185		}
186	}
187
188	for value in &mut ys {
189		*value -= min;
190	}
191
192	let (intercept, params, errors) = raw_linear_regression(&xs, &ys, x_vars, false)?;
193	assert!(intercept.abs() <= 0.0001);
194	Some((min, params, errors[1..].to_vec()))
195}
196
197impl Analysis {
198	// Useful for when there are no components, and we just need an median value of the benchmark
199	// results. Note: We choose the median value because it is more robust to outliers.
200	fn median_value(r: &Vec<BenchmarkResult>, selector: BenchmarkSelector) -> Option<Self> {
201		if r.is_empty() {
202			return None
203		}
204
205		let mut values: Vec<u128> = r
206			.iter()
207			.map(|result| match selector {
208				BenchmarkSelector::ExtrinsicTime => result.extrinsic_time,
209				BenchmarkSelector::StorageRootTime => result.storage_root_time,
210				BenchmarkSelector::Reads => result.reads.into(),
211				BenchmarkSelector::Writes => result.writes.into(),
212				BenchmarkSelector::ProofSize => result.proof_size.into(),
213			})
214			.collect();
215
216		values.sort();
217		let mid = values.len() / 2;
218
219		Some(Self {
220			base: selector.scale_weight(values[mid]),
221			slopes: Vec::new(),
222			names: Vec::new(),
223			value_dists: None,
224			errors: None,
225			minimum: selector.get_minimum(&r),
226			selector,
227		})
228	}
229
230	pub fn median_slopes(r: &Vec<BenchmarkResult>, selector: BenchmarkSelector) -> Option<Self> {
231		if r[0].components.is_empty() {
232			return Self::median_value(r, selector)
233		}
234
235		let results = r[0]
236			.components
237			.iter()
238			.enumerate()
239			.map(|(i, &(param, _))| {
240				let mut counted = BTreeMap::<Vec<u32>, usize>::new();
241				for result in r.iter() {
242					let mut p = result.components.iter().map(|x| x.1).collect::<Vec<_>>();
243					p[i] = 0;
244					*counted.entry(p).or_default() += 1;
245				}
246				let others: Vec<u32> =
247					counted.iter().max_by_key(|i| i.1).expect("r is not empty; qed").0.clone();
248				let values = r
249					.iter()
250					.filter(|v| {
251						v.components
252							.iter()
253							.map(|x| x.1)
254							.zip(others.iter())
255							.enumerate()
256							.all(|(j, (v1, v2))| j == i || v1 == *v2)
257					})
258					.map(|result| {
259						// Extract the data we are interested in analyzing
260						let data = match selector {
261							BenchmarkSelector::ExtrinsicTime => result.extrinsic_time,
262							BenchmarkSelector::StorageRootTime => result.storage_root_time,
263							BenchmarkSelector::Reads => result.reads.into(),
264							BenchmarkSelector::Writes => result.writes.into(),
265							BenchmarkSelector::ProofSize => result.proof_size.into(),
266						};
267						(result.components[i].1, data)
268					})
269					.collect::<Vec<_>>();
270				(format!("{:?}", param), i, others, values)
271			})
272			.collect::<Vec<_>>();
273
274		let models = results
275			.iter()
276			.map(|(_, _, _, ref values)| {
277				let mut slopes = vec![];
278				for (i, &(x1, y1)) in values.iter().enumerate() {
279					for &(x2, y2) in values.iter().skip(i + 1) {
280						if x1 != x2 {
281							slopes.push((y1 as f64 - y2 as f64) / (x1 as f64 - x2 as f64));
282						}
283					}
284				}
285				slopes.sort_by(|a, b| a.partial_cmp(b).expect("values well defined; qed"));
286				let slope = slopes[slopes.len() / 2];
287
288				let mut offsets = vec![];
289				for &(x, y) in values.iter() {
290					offsets.push(y as f64 - slope * x as f64);
291				}
292				offsets.sort_by(|a, b| a.partial_cmp(b).expect("values well defined; qed"));
293				let offset = offsets[offsets.len() / 2];
294
295				(offset, slope)
296			})
297			.collect::<Vec<_>>();
298
299		let models = models
300			.iter()
301			.zip(results.iter())
302			.map(|((offset, slope), (_, i, others, _))| {
303				let over = others
304					.iter()
305					.enumerate()
306					.filter(|(j, _)| j != i)
307					.map(|(j, v)| models[j].1 * *v as f64)
308					.fold(0f64, |acc, i| acc + i);
309				(*offset - over, *slope)
310			})
311			.collect::<Vec<_>>();
312
313		let base = selector.scale_and_cast_weight(models[0].0.max(0f64), false);
314		let slopes = models
315			.iter()
316			.map(|x| selector.scale_and_cast_weight(x.1.max(0f64), false))
317			.collect::<Vec<_>>();
318
319		Some(Self {
320			base,
321			slopes,
322			names: results.into_iter().map(|x| x.0).collect::<Vec<_>>(),
323			value_dists: None,
324			errors: None,
325			minimum: selector.get_minimum(&r),
326			selector,
327		})
328	}
329
330	pub fn min_squares_iqr(r: &Vec<BenchmarkResult>, selector: BenchmarkSelector) -> Option<Self> {
331		if r[0].components.is_empty() || r.len() <= 2 {
332			return Self::median_value(r, selector)
333		}
334
335		let mut results = BTreeMap::<Vec<u32>, Vec<u128>>::new();
336		for result in r.iter() {
337			let p = result.components.iter().map(|x| x.1).collect::<Vec<_>>();
338			results.entry(p).or_default().push(match selector {
339				BenchmarkSelector::ExtrinsicTime => result.extrinsic_time,
340				BenchmarkSelector::StorageRootTime => result.storage_root_time,
341				BenchmarkSelector::Reads => result.reads.into(),
342				BenchmarkSelector::Writes => result.writes.into(),
343				BenchmarkSelector::ProofSize => result.proof_size.into(),
344			})
345		}
346
347		for (_, rs) in results.iter_mut() {
348			rs.sort();
349			let ql = rs.len() / 4;
350			*rs = rs[ql..rs.len() - ql].to_vec();
351		}
352
353		let names = r[0].components.iter().map(|x| format!("{:?}", x.0)).collect::<Vec<_>>();
354		let value_dists = results
355			.iter()
356			.map(|(p, vs)| {
357				// Avoid divide by zero
358				if vs.is_empty() {
359					return (p.clone(), 0, 0)
360				}
361				let total = vs.iter().fold(0u128, |acc, v| acc + *v);
362				let mean = total / vs.len() as u128;
363				let sum_sq_diff = vs.iter().fold(0u128, |acc, v| {
364					let d = mean.max(*v) - mean.min(*v);
365					acc + d * d
366				});
367				let stddev = (sum_sq_diff as f64 / vs.len() as f64).sqrt() as u128;
368				(p.clone(), mean, stddev)
369			})
370			.collect::<Vec<_>>();
371
372		let mut ys: Vec<f64> = Vec::new();
373		let mut xs: Vec<f64> = Vec::new();
374		for result in results {
375			let x: Vec<f64> = result.0.iter().map(|value| *value as f64).collect();
376			for y in result.1 {
377				xs.extend(x.iter().copied());
378				ys.push(y as f64);
379			}
380		}
381
382		let (intercept, slopes, errors) = linear_regression(xs, ys, r[0].components.len())?;
383
384		Some(Self {
385			base: selector.scale_and_cast_weight(intercept, true),
386			slopes: slopes
387				.into_iter()
388				.map(|value| selector.scale_and_cast_weight(value, true))
389				.collect(),
390			names,
391			value_dists: Some(value_dists),
392			errors: Some(
393				errors
394					.into_iter()
395					.map(|value| selector.scale_and_cast_weight(value, false))
396					.collect(),
397			),
398			minimum: selector.get_minimum(&r),
399			selector,
400		})
401	}
402
403	pub fn max(r: &Vec<BenchmarkResult>, selector: BenchmarkSelector) -> Option<Self> {
404		let median_slopes = Self::median_slopes(r, selector);
405		let min_squares = Self::min_squares_iqr(r, selector);
406
407		if median_slopes.is_none() || min_squares.is_none() {
408			return None
409		}
410
411		let median_slopes = median_slopes.unwrap();
412		let min_squares = min_squares.unwrap();
413
414		let base = median_slopes.base.max(min_squares.base);
415		let slopes = median_slopes
416			.slopes
417			.into_iter()
418			.zip(min_squares.slopes.into_iter())
419			.map(|(a, b): (u128, u128)| a.max(b))
420			.collect::<Vec<u128>>();
421		// components should always be in the same order
422		median_slopes
423			.names
424			.iter()
425			.zip(min_squares.names.iter())
426			.for_each(|(a, b)| assert!(a == b, "benchmark results not in the same order"));
427		let names = median_slopes.names;
428		let value_dists = min_squares.value_dists;
429		let errors = min_squares.errors;
430		let minimum = selector.get_minimum(&r);
431
432		Some(Self { base, slopes, names, value_dists, errors, selector, minimum })
433	}
434}
435
436fn ms(mut nanos: u128) -> String {
437	let mut x = 100_000u128;
438	while x > 1 {
439		if nanos > x * 1_000 {
440			nanos = nanos / x * x;
441			break
442		}
443		x /= 10;
444	}
445	format!("{}", nanos as f64 / 1_000f64)
446}
447
448impl std::fmt::Display for Analysis {
449	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
450		if let Some(ref value_dists) = self.value_dists {
451			writeln!(f, "\nData points distribution:")?;
452			writeln!(
453				f,
454				"{}   mean µs  sigma µs       %",
455				self.names.iter().map(|p| format!("{:>5}", p)).collect::<Vec<_>>().join(" ")
456			)?;
457			for (param_values, mean, sigma) in value_dists.iter() {
458				if *mean == 0 {
459					writeln!(
460						f,
461						"{}  {:>8}  {:>8}  {:>3}.{}%",
462						param_values
463							.iter()
464							.map(|v| format!("{:>5}", v))
465							.collect::<Vec<_>>()
466							.join(" "),
467						ms(*mean),
468						ms(*sigma),
469						"?",
470						"?"
471					)?;
472				} else {
473					writeln!(
474						f,
475						"{}  {:>8}  {:>8}  {:>3}.{}%",
476						param_values
477							.iter()
478							.map(|v| format!("{:>5}", v))
479							.collect::<Vec<_>>()
480							.join(" "),
481						ms(*mean),
482						ms(*sigma),
483						(sigma * 100 / mean),
484						(sigma * 1000 / mean % 10)
485					)?;
486				}
487			}
488		}
489
490		if let Some(ref errors) = self.errors {
491			writeln!(f, "\nQuality and confidence:")?;
492			writeln!(f, "param     error")?;
493			for (p, se) in self.names.iter().zip(errors.iter()) {
494				writeln!(f, "{}      {:>8}", p, ms(self.selector.nanos_from_weight(*se)))?;
495			}
496		}
497
498		writeln!(f, "\nModel:")?;
499		writeln!(f, "Time ~= {:>8}", ms(self.selector.nanos_from_weight(self.base)))?;
500		for (&t, n) in self.slopes.iter().zip(self.names.iter()) {
501			writeln!(f, "    + {} {:>8}", n, ms(self.selector.nanos_from_weight(t)))?;
502		}
503		writeln!(f, "              µs")
504	}
505}
506
507impl std::fmt::Debug for Analysis {
508	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
509		write!(f, "{}", self.base)?;
510		for (&m, n) in self.slopes.iter().zip(self.names.iter()) {
511			write!(f, " + ({} * {})", m, n)?;
512		}
513		write!(f, "")
514	}
515}
516
517#[cfg(test)]
518mod tests {
519	use super::*;
520	use crate::BenchmarkParameter;
521
522	fn benchmark_result(
523		components: Vec<(BenchmarkParameter, u32)>,
524		extrinsic_time: u128,
525		storage_root_time: u128,
526		reads: u32,
527		writes: u32,
528	) -> BenchmarkResult {
529		BenchmarkResult {
530			components,
531			extrinsic_time,
532			storage_root_time,
533			reads,
534			repeat_reads: 0,
535			writes,
536			repeat_writes: 0,
537			proof_size: 0,
538			keys: vec![],
539		}
540	}
541
542	#[test]
543	fn test_linear_regression() {
544		let ys = vec![
545			3797981.0,
546			37857779.0,
547			70569402.0,
548			104004114.0,
549			137233924.0,
550			169826237.0,
551			203521133.0,
552			237552333.0,
553			271082065.0,
554			305554637.0,
555			335218347.0,
556			371759065.0,
557			405086197.0,
558			438353555.0,
559			472891417.0,
560			505339532.0,
561			527784778.0,
562			562590596.0,
563			635291991.0,
564			673027090.0,
565			708119408.0,
566		];
567		let xs = vec![
568			0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
569			16.0, 17.0, 18.0, 19.0, 20.0,
570		];
571
572		let (intercept, params, errors) = raw_linear_regression(&xs, &ys, 1, true).unwrap();
573		assert_eq!(intercept as i64, -2712997);
574		assert_eq!(params.len(), 1);
575		assert_eq!(params[0] as i64, 34444926);
576		assert_eq!(errors.len(), 2);
577		assert_eq!(errors[0] as i64, 4805766);
578		assert_eq!(errors[1] as i64, 411084);
579
580		let (intercept, params, errors) = linear_regression(xs, ys, 1).unwrap();
581		assert_eq!(intercept as i64, 3797981);
582		assert_eq!(params.len(), 1);
583		assert_eq!(params[0] as i64, 33968513);
584		assert_eq!(errors.len(), 1);
585		assert_eq!(errors[0] as i64, 217331);
586	}
587
588	#[test]
589	fn analysis_median_slopes_should_work() {
590		let data = vec![
591			benchmark_result(
592				vec![(BenchmarkParameter::n, 1), (BenchmarkParameter::m, 5)],
593				11_500_000,
594				0,
595				3,
596				10,
597			),
598			benchmark_result(
599				vec![(BenchmarkParameter::n, 2), (BenchmarkParameter::m, 5)],
600				12_500_000,
601				0,
602				4,
603				10,
604			),
605			benchmark_result(
606				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 5)],
607				13_500_000,
608				0,
609				5,
610				10,
611			),
612			benchmark_result(
613				vec![(BenchmarkParameter::n, 4), (BenchmarkParameter::m, 5)],
614				14_500_000,
615				0,
616				6,
617				10,
618			),
619			benchmark_result(
620				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 1)],
621				13_100_000,
622				0,
623				5,
624				2,
625			),
626			benchmark_result(
627				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 3)],
628				13_300_000,
629				0,
630				5,
631				6,
632			),
633			benchmark_result(
634				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 7)],
635				13_700_000,
636				0,
637				5,
638				14,
639			),
640			benchmark_result(
641				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 10)],
642				14_000_000,
643				0,
644				5,
645				20,
646			),
647		];
648
649		let extrinsic_time =
650			Analysis::median_slopes(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
651		assert_eq!(extrinsic_time.base, 10_000_000_000);
652		assert_eq!(extrinsic_time.slopes, vec![1_000_000_000, 100_000_000]);
653
654		let reads = Analysis::median_slopes(&data, BenchmarkSelector::Reads).unwrap();
655		assert_eq!(reads.base, 2);
656		assert_eq!(reads.slopes, vec![1, 0]);
657
658		let writes = Analysis::median_slopes(&data, BenchmarkSelector::Writes).unwrap();
659		assert_eq!(writes.base, 0);
660		assert_eq!(writes.slopes, vec![0, 2]);
661	}
662
663	#[test]
664	fn analysis_median_min_squares_should_work() {
665		let data = vec![
666			benchmark_result(
667				vec![(BenchmarkParameter::n, 1), (BenchmarkParameter::m, 5)],
668				11_500_000,
669				0,
670				3,
671				10,
672			),
673			benchmark_result(
674				vec![(BenchmarkParameter::n, 2), (BenchmarkParameter::m, 5)],
675				12_500_000,
676				0,
677				4,
678				10,
679			),
680			benchmark_result(
681				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 5)],
682				13_500_000,
683				0,
684				5,
685				10,
686			),
687			benchmark_result(
688				vec![(BenchmarkParameter::n, 4), (BenchmarkParameter::m, 5)],
689				14_500_000,
690				0,
691				6,
692				10,
693			),
694			benchmark_result(
695				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 1)],
696				13_100_000,
697				0,
698				5,
699				2,
700			),
701			benchmark_result(
702				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 3)],
703				13_300_000,
704				0,
705				5,
706				6,
707			),
708			benchmark_result(
709				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 7)],
710				13_700_000,
711				0,
712				5,
713				14,
714			),
715			benchmark_result(
716				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 10)],
717				14_000_000,
718				0,
719				5,
720				20,
721			),
722		];
723
724		let extrinsic_time =
725			Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
726		assert_eq!(extrinsic_time.base, 10_000_000_000);
727		assert_eq!(extrinsic_time.slopes, vec![1000000000, 100000000]);
728
729		let reads = Analysis::min_squares_iqr(&data, BenchmarkSelector::Reads).unwrap();
730		assert_eq!(reads.base, 2);
731		assert_eq!(reads.slopes, vec![1, 0]);
732
733		let writes = Analysis::min_squares_iqr(&data, BenchmarkSelector::Writes).unwrap();
734		assert_eq!(writes.base, 0);
735		assert_eq!(writes.slopes, vec![0, 2]);
736	}
737
738	#[test]
739	fn analysis_min_squares_iqr_uses_multiple_samples_for_same_parameters() {
740		let data = vec![
741			benchmark_result(vec![(BenchmarkParameter::n, 0)], 2_000_000, 0, 0, 0),
742			benchmark_result(vec![(BenchmarkParameter::n, 0)], 4_000_000, 0, 0, 0),
743			benchmark_result(vec![(BenchmarkParameter::n, 1)], 4_000_000, 0, 0, 0),
744			benchmark_result(vec![(BenchmarkParameter::n, 1)], 8_000_000, 0, 0, 0),
745		];
746
747		let extrinsic_time =
748			Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
749		assert_eq!(extrinsic_time.base, 3_000_000_000);
750		assert_eq!(extrinsic_time.slopes, vec![3_000_000_000]);
751	}
752
753	#[test]
754	fn intercept_of_a_little_under_zero_is_rounded_up_to_zero() {
755		// Analytically this should result in an intercept of 0, but
756		// due to numerical imprecision this will generate an intercept
757		// equal to roughly -0.0000000000000004440892098500626
758		let data = vec![
759			benchmark_result(vec![(BenchmarkParameter::n, 1)], 2, 0, 0, 0),
760			benchmark_result(vec![(BenchmarkParameter::n, 2)], 4, 0, 0, 0),
761			benchmark_result(vec![(BenchmarkParameter::n, 3)], 6, 0, 0, 0),
762		];
763
764		let extrinsic_time =
765			Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
766		assert_eq!(extrinsic_time.base, 0);
767		assert_eq!(extrinsic_time.slopes, vec![2000]);
768	}
769}