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 a median value of the benchmark
199	// results. Note: We choose the median value because it is more robust to outliers.
200	fn median_value(
201		r: &Vec<BenchmarkResult>,
202		selector: BenchmarkSelector,
203	) -> Result<Self, anyhow::Error> {
204		anyhow::ensure!(!r.is_empty(), "benchmark results cannot be empty");
205
206		let mut values: Vec<u128> = r.iter().map(|result| selector.get_value(result)).collect();
207
208		values.sort();
209		let mid = values.len() / 2;
210
211		Ok(Self {
212			base: selector.scale_weight(values[mid]),
213			slopes: Vec::new(),
214			names: Vec::new(),
215			value_dists: None,
216			errors: None,
217			minimum: selector.get_minimum(r),
218			selector,
219		})
220	}
221
222	pub fn median_slopes(
223		r: &Vec<BenchmarkResult>,
224		selector: BenchmarkSelector,
225	) -> Result<Self, anyhow::Error> {
226		anyhow::ensure!(!r.is_empty(), "benchmark results cannot be empty");
227
228		if r[0].components.is_empty() {
229			return Self::median_value(r, selector);
230		}
231
232		let results = r[0]
233			.components
234			.iter()
235			.enumerate()
236			.map(|(i, &(param, _))| {
237				let mut counted = BTreeMap::<Vec<u32>, usize>::new();
238				for result in r.iter() {
239					let mut p = result.components.iter().map(|x| x.1).collect::<Vec<_>>();
240					p[i] = 0;
241					*counted.entry(p).or_default() += 1;
242				}
243				let others: Vec<u32> =
244					counted.iter().max_by_key(|i| i.1).expect("r is not empty; qed").0.clone();
245				let values = r
246					.iter()
247					.filter(|v| {
248						v.components
249							.iter()
250							.map(|x| x.1)
251							.zip(others.iter())
252							.enumerate()
253							.all(|(j, (v1, v2))| j == i || v1 == *v2)
254					})
255					.map(|result| (result.components[i].1, selector.get_value(result)))
256					.collect::<Vec<_>>();
257				(format!("{:?}", param), i, others, values)
258			})
259			.collect::<Vec<_>>();
260
261		let models = results
262			.iter()
263			.map(|(param_name, _, _, ref values)| {
264				let mut slopes = vec![];
265				for (i, &(x1, y1)) in values.iter().enumerate() {
266					for &(x2, y2) in values.iter().skip(i + 1) {
267						if x1 != x2 {
268							slopes.push((y1 as f64 - y2 as f64) / (x1 as f64 - x2 as f64));
269						}
270					}
271				}
272				if slopes.is_empty() {
273					let unique_values = values
274						.iter()
275						.map(|(x, _)| x)
276						.collect::<std::collections::BTreeSet<_>>()
277						.len();
278					return Err(anyhow::anyhow!(
279						"Parameter `{param_name}` only has \
280						{unique_values} unique value(s) but needs at least 2 to compute a slope. \
281						This can happen when too many benchmark samples are skipped. \
282						Try increasing the number of steps for this parameter or fix the benchmark.",
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				Ok((offset, slope))
296			})
297			.collect::<Result<Vec<_>, anyhow::Error>>()?;
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		Ok(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(
331		r: &Vec<BenchmarkResult>,
332		selector: BenchmarkSelector,
333	) -> Result<Self, anyhow::Error> {
334		anyhow::ensure!(!r.is_empty(), "benchmark results cannot be empty");
335
336		if r[0].components.is_empty() {
337			return Self::median_value(r, selector);
338		}
339
340		// The OLS fit below requires more than two samples. Fall back to
341		// `median_slopes` because two samples at distinct x-values still uniquely
342		// determine a slope.
343		if r.len() <= 2 {
344			return Self::median_slopes(r, selector);
345		}
346
347		let mut results = BTreeMap::<Vec<u32>, Vec<u128>>::new();
348		for result in r.iter() {
349			let p = result.components.iter().map(|x| x.1).collect::<Vec<_>>();
350			results.entry(p).or_default().push(selector.get_value(result));
351		}
352
353		for (_, rs) in results.iter_mut() {
354			rs.sort();
355			let ql = rs.len() / 4;
356			*rs = rs[ql..rs.len() - ql].to_vec();
357		}
358
359		let names = r[0].components.iter().map(|x| format!("{:?}", x.0)).collect::<Vec<_>>();
360		let value_dists = results
361			.iter()
362			.map(|(p, vs)| {
363				// Avoid divide by zero
364				if vs.is_empty() {
365					return (p.clone(), 0, 0);
366				}
367				let total = vs.iter().fold(0u128, |acc, v| acc + *v);
368				let mean = total / vs.len() as u128;
369				let sum_sq_diff = vs.iter().fold(0u128, |acc, v| {
370					let d = mean.max(*v) - mean.min(*v);
371					acc + d * d
372				});
373				let stddev = (sum_sq_diff as f64 / vs.len() as f64).sqrt() as u128;
374				(p.clone(), mean, stddev)
375			})
376			.collect::<Vec<_>>();
377
378		let mut ys: Vec<f64> = Vec::new();
379		let mut xs: Vec<f64> = Vec::new();
380		for result in results {
381			let x: Vec<f64> = result.0.iter().map(|value| *value as f64).collect();
382			for y in result.1 {
383				xs.extend(x.iter().copied());
384				ys.push(y as f64);
385			}
386		}
387
388		let (intercept, slopes, errors) = linear_regression(xs, ys, r[0].components.len())
389			.ok_or_else(|| {
390				anyhow::anyhow!("linear regression failed for min_squares_iqr analysis")
391			})?;
392
393		Ok(Self {
394			base: selector.scale_and_cast_weight(intercept, true),
395			slopes: slopes
396				.into_iter()
397				.map(|value| selector.scale_and_cast_weight(value, true))
398				.collect(),
399			names,
400			value_dists: Some(value_dists),
401			errors: Some(
402				errors
403					.into_iter()
404					.map(|value| selector.scale_and_cast_weight(value, false))
405					.collect(),
406			),
407			minimum: selector.get_minimum(r),
408			selector,
409		})
410	}
411
412	pub fn max(
413		r: &Vec<BenchmarkResult>,
414		selector: BenchmarkSelector,
415	) -> Result<Self, anyhow::Error> {
416		let median_slopes = Self::median_slopes(r, selector)?;
417		let min_squares = Self::min_squares_iqr(r, selector)?;
418
419		let base = median_slopes.base.max(min_squares.base);
420		let slopes = median_slopes
421			.slopes
422			.into_iter()
423			.zip(min_squares.slopes.into_iter())
424			.map(|(a, b): (u128, u128)| a.max(b))
425			.collect::<Vec<u128>>();
426		// components should always be in the same order
427		median_slopes
428			.names
429			.iter()
430			.zip(min_squares.names.iter())
431			.for_each(|(a, b)| assert!(a == b, "benchmark results not in the same order"));
432		let names = median_slopes.names;
433		let value_dists = min_squares.value_dists;
434		let errors = min_squares.errors;
435		let minimum = selector.get_minimum(r);
436
437		Ok(Self { base, slopes, names, value_dists, errors, selector, minimum })
438	}
439}
440
441fn ms(mut nanos: u128) -> String {
442	let mut x = 100_000u128;
443	while x > 1 {
444		if nanos > x * 1_000 {
445			nanos = nanos / x * x;
446			break;
447		}
448		x /= 10;
449	}
450	format!("{}", nanos as f64 / 1_000f64)
451}
452
453impl std::fmt::Display for Analysis {
454	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
455		if let Some(ref value_dists) = self.value_dists {
456			writeln!(f, "\nData points distribution:")?;
457			writeln!(
458				f,
459				"{}   mean µs  sigma µs       %",
460				self.names.iter().map(|p| format!("{:>5}", p)).collect::<Vec<_>>().join(" ")
461			)?;
462			for (param_values, mean, sigma) in value_dists.iter() {
463				if *mean == 0 {
464					writeln!(
465						f,
466						"{}  {:>8}  {:>8}  {:>3}.{}%",
467						param_values
468							.iter()
469							.map(|v| format!("{:>5}", v))
470							.collect::<Vec<_>>()
471							.join(" "),
472						ms(*mean),
473						ms(*sigma),
474						"?",
475						"?"
476					)?;
477				} else {
478					writeln!(
479						f,
480						"{}  {:>8}  {:>8}  {:>3}.{}%",
481						param_values
482							.iter()
483							.map(|v| format!("{:>5}", v))
484							.collect::<Vec<_>>()
485							.join(" "),
486						ms(*mean),
487						ms(*sigma),
488						(sigma * 100 / mean),
489						(sigma * 1000 / mean % 10)
490					)?;
491				}
492			}
493		}
494
495		if let Some(ref errors) = self.errors {
496			writeln!(f, "\nQuality and confidence:")?;
497			writeln!(f, "param     error")?;
498			for (p, se) in self.names.iter().zip(errors.iter()) {
499				writeln!(f, "{}      {:>8}", p, ms(self.selector.nanos_from_weight(*se)))?;
500			}
501		}
502
503		writeln!(f, "\nModel:")?;
504		writeln!(f, "Time ~= {:>8}", ms(self.selector.nanos_from_weight(self.base)))?;
505		for (&t, n) in self.slopes.iter().zip(self.names.iter()) {
506			writeln!(f, "    + {} {:>8}", n, ms(self.selector.nanos_from_weight(t)))?;
507		}
508		writeln!(f, "              µs")
509	}
510}
511
512impl std::fmt::Debug for Analysis {
513	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
514		write!(f, "{}", self.base)?;
515		for (&m, n) in self.slopes.iter().zip(self.names.iter()) {
516			write!(f, " + ({} * {})", m, n)?;
517		}
518		write!(f, "")
519	}
520}
521
522#[cfg(test)]
523mod tests {
524	use super::*;
525	use crate::BenchmarkParameter;
526
527	fn benchmark_result(
528		components: Vec<(BenchmarkParameter, u32)>,
529		extrinsic_time: u128,
530		storage_root_time: u128,
531		reads: u32,
532		writes: u32,
533	) -> BenchmarkResult {
534		BenchmarkResult {
535			components,
536			extrinsic_time,
537			storage_root_time,
538			reads,
539			repeat_reads: 0,
540			writes,
541			repeat_writes: 0,
542			proof_size: 0,
543			keys: vec![],
544		}
545	}
546
547	#[test]
548	fn test_linear_regression() {
549		let ys = vec![
550			3797981.0,
551			37857779.0,
552			70569402.0,
553			104004114.0,
554			137233924.0,
555			169826237.0,
556			203521133.0,
557			237552333.0,
558			271082065.0,
559			305554637.0,
560			335218347.0,
561			371759065.0,
562			405086197.0,
563			438353555.0,
564			472891417.0,
565			505339532.0,
566			527784778.0,
567			562590596.0,
568			635291991.0,
569			673027090.0,
570			708119408.0,
571		];
572		let xs = vec![
573			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,
574			16.0, 17.0, 18.0, 19.0, 20.0,
575		];
576
577		let (intercept, params, errors) = raw_linear_regression(&xs, &ys, 1, true).unwrap();
578		assert_eq!(intercept as i64, -2712997);
579		assert_eq!(params.len(), 1);
580		assert_eq!(params[0] as i64, 34444926);
581		assert_eq!(errors.len(), 2);
582		assert_eq!(errors[0] as i64, 4805766);
583		assert_eq!(errors[1] as i64, 411084);
584
585		let (intercept, params, errors) = linear_regression(xs, ys, 1).unwrap();
586		assert_eq!(intercept as i64, 3797981);
587		assert_eq!(params.len(), 1);
588		assert_eq!(params[0] as i64, 33968513);
589		assert_eq!(errors.len(), 1);
590		assert_eq!(errors[0] as i64, 217331);
591	}
592
593	// Regression: `min_squares_iqr` used to short-circuit to `median_value` (which
594	// has no slopes) whenever `r.len() <= 2`, so benchmarks whose valid sample set
595	// is narrowed to two distinct x-values — for example by `BenchmarkError::Skip`
596	// filtering all but two values of a `Linear<lo, hi>` parameter — lost their
597	// linear component entirely. Two distinct-x samples uniquely determine a
598	// slope; the fallback now goes through `median_slopes`, which handles that
599	// case exactly.
600	#[test]
601	fn min_squares_iqr_fits_slope_with_two_distinct_x_samples() {
602		// y = 4 + 1·n at n=4 (reads=8) and n=8 (reads=12)
603		let data = vec![
604			benchmark_result(vec![(BenchmarkParameter::n, 4)], 0, 0, 8, 0),
605			benchmark_result(vec![(BenchmarkParameter::n, 8)], 0, 0, 12, 0),
606		];
607		let analysis = Analysis::min_squares_iqr(&data, BenchmarkSelector::Reads).unwrap();
608		assert_eq!(analysis.slopes, vec![1]);
609		assert_eq!(analysis.base, 4);
610	}
611
612	// All samples at the same x cannot determine a slope. The previous
613	// `median_value` fallback silently produced a constant; the new fallback
614	// surfaces an explicit error from `median_slopes`.
615	#[test]
616	fn min_squares_iqr_two_samples_same_x_errors() {
617		let data = vec![
618			benchmark_result(vec![(BenchmarkParameter::n, 4)], 0, 0, 8, 0),
619			benchmark_result(vec![(BenchmarkParameter::n, 4)], 0, 0, 10, 0),
620		];
621		let err = Analysis::min_squares_iqr(&data, BenchmarkSelector::Reads).unwrap_err();
622		assert!(
623			err.to_string().contains("only has 1 unique value"),
624			"expected 'only has 1 unique value' diagnostic, got: {err}",
625		);
626	}
627
628	#[test]
629	fn analysis_median_slopes_should_work() {
630		let data = vec![
631			benchmark_result(
632				vec![(BenchmarkParameter::n, 1), (BenchmarkParameter::m, 5)],
633				11_500_000,
634				0,
635				3,
636				10,
637			),
638			benchmark_result(
639				vec![(BenchmarkParameter::n, 2), (BenchmarkParameter::m, 5)],
640				12_500_000,
641				0,
642				4,
643				10,
644			),
645			benchmark_result(
646				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 5)],
647				13_500_000,
648				0,
649				5,
650				10,
651			),
652			benchmark_result(
653				vec![(BenchmarkParameter::n, 4), (BenchmarkParameter::m, 5)],
654				14_500_000,
655				0,
656				6,
657				10,
658			),
659			benchmark_result(
660				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 1)],
661				13_100_000,
662				0,
663				5,
664				2,
665			),
666			benchmark_result(
667				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 3)],
668				13_300_000,
669				0,
670				5,
671				6,
672			),
673			benchmark_result(
674				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 7)],
675				13_700_000,
676				0,
677				5,
678				14,
679			),
680			benchmark_result(
681				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 10)],
682				14_000_000,
683				0,
684				5,
685				20,
686			),
687		];
688
689		let extrinsic_time =
690			Analysis::median_slopes(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
691		assert_eq!(extrinsic_time.base, 10_000_000_000);
692		assert_eq!(extrinsic_time.slopes, vec![1_000_000_000, 100_000_000]);
693
694		let reads = Analysis::median_slopes(&data, BenchmarkSelector::Reads).unwrap();
695		assert_eq!(reads.base, 2);
696		assert_eq!(reads.slopes, vec![1, 0]);
697
698		let writes = Analysis::median_slopes(&data, BenchmarkSelector::Writes).unwrap();
699		assert_eq!(writes.base, 0);
700		assert_eq!(writes.slopes, vec![0, 2]);
701	}
702
703	#[test]
704	fn analysis_median_min_squares_should_work() {
705		let data = vec![
706			benchmark_result(
707				vec![(BenchmarkParameter::n, 1), (BenchmarkParameter::m, 5)],
708				11_500_000,
709				0,
710				3,
711				10,
712			),
713			benchmark_result(
714				vec![(BenchmarkParameter::n, 2), (BenchmarkParameter::m, 5)],
715				12_500_000,
716				0,
717				4,
718				10,
719			),
720			benchmark_result(
721				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 5)],
722				13_500_000,
723				0,
724				5,
725				10,
726			),
727			benchmark_result(
728				vec![(BenchmarkParameter::n, 4), (BenchmarkParameter::m, 5)],
729				14_500_000,
730				0,
731				6,
732				10,
733			),
734			benchmark_result(
735				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 1)],
736				13_100_000,
737				0,
738				5,
739				2,
740			),
741			benchmark_result(
742				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 3)],
743				13_300_000,
744				0,
745				5,
746				6,
747			),
748			benchmark_result(
749				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 7)],
750				13_700_000,
751				0,
752				5,
753				14,
754			),
755			benchmark_result(
756				vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 10)],
757				14_000_000,
758				0,
759				5,
760				20,
761			),
762		];
763
764		let extrinsic_time =
765			Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
766		assert_eq!(extrinsic_time.base, 10_000_000_000);
767		assert_eq!(extrinsic_time.slopes, vec![1000000000, 100000000]);
768
769		let reads = Analysis::min_squares_iqr(&data, BenchmarkSelector::Reads).unwrap();
770		assert_eq!(reads.base, 2);
771		assert_eq!(reads.slopes, vec![1, 0]);
772
773		let writes = Analysis::min_squares_iqr(&data, BenchmarkSelector::Writes).unwrap();
774		assert_eq!(writes.base, 0);
775		assert_eq!(writes.slopes, vec![0, 2]);
776	}
777
778	#[test]
779	fn analysis_min_squares_iqr_uses_multiple_samples_for_same_parameters() {
780		let data = vec![
781			benchmark_result(vec![(BenchmarkParameter::n, 0)], 2_000_000, 0, 0, 0),
782			benchmark_result(vec![(BenchmarkParameter::n, 0)], 4_000_000, 0, 0, 0),
783			benchmark_result(vec![(BenchmarkParameter::n, 1)], 4_000_000, 0, 0, 0),
784			benchmark_result(vec![(BenchmarkParameter::n, 1)], 8_000_000, 0, 0, 0),
785		];
786
787		let extrinsic_time =
788			Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
789		assert_eq!(extrinsic_time.base, 3_000_000_000);
790		assert_eq!(extrinsic_time.slopes, vec![3_000_000_000]);
791	}
792
793	#[test]
794	fn intercept_of_a_little_under_zero_is_rounded_up_to_zero() {
795		// Analytically this should result in an intercept of 0, but
796		// due to numerical imprecision this will generate an intercept
797		// equal to roughly -0.0000000000000004440892098500626
798		let data = vec![
799			benchmark_result(vec![(BenchmarkParameter::n, 1)], 2, 0, 0, 0),
800			benchmark_result(vec![(BenchmarkParameter::n, 2)], 4, 0, 0, 0),
801			benchmark_result(vec![(BenchmarkParameter::n, 3)], 6, 0, 0, 0),
802		];
803
804		let extrinsic_time =
805			Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
806		assert_eq!(extrinsic_time.base, 0);
807		assert_eq!(extrinsic_time.slopes, vec![2000]);
808	}
809}