1use 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
42fn mul_1000_into_u128(value: f64) -> u128 {
44 (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 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 MinSquares,
105 MedianSlopes,
107 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 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 return Some((intercept, params, errors[1..].to_vec()));
176 }
177
178 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 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 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 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 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 #[test]
601 fn min_squares_iqr_fits_slope_with_two_distinct_x_samples() {
602 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 #[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 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}