Skip to main content

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