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
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 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 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 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 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}