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