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