use crate::BenchmarkResult;
use std::collections::BTreeMap;
pub struct Analysis {
pub base: u128,
pub slopes: Vec<u128>,
pub names: Vec<String>,
pub value_dists: Option<Vec<(Vec<u32>, u128, u128)>>,
pub errors: Option<Vec<u128>>,
pub minimum: u128,
selector: BenchmarkSelector,
}
#[derive(Clone, Copy)]
pub enum BenchmarkSelector {
ExtrinsicTime,
StorageRootTime,
Reads,
Writes,
ProofSize,
}
fn mul_1000_into_u128(value: f64) -> u128 {
(value as u128)
.saturating_mul(1000)
.saturating_add((value.fract() * 1000.0) as u128)
}
impl BenchmarkSelector {
fn scale_and_cast_weight(self, value: f64, round_up: bool) -> u128 {
if let BenchmarkSelector::ExtrinsicTime = self {
mul_1000_into_u128(value + 0.000_000_005)
} else {
if round_up {
(value + 0.5) as u128
} else {
value as u128
}
}
}
fn scale_weight(self, value: u128) -> u128 {
if let BenchmarkSelector::ExtrinsicTime = self {
value.saturating_mul(1000)
} else {
value
}
}
fn nanos_from_weight(self, value: u128) -> u128 {
if let BenchmarkSelector::ExtrinsicTime = self {
value / 1000
} else {
value
}
}
fn get_value(self, result: &BenchmarkResult) -> u128 {
match self {
BenchmarkSelector::ExtrinsicTime => result.extrinsic_time,
BenchmarkSelector::StorageRootTime => result.storage_root_time,
BenchmarkSelector::Reads => result.reads.into(),
BenchmarkSelector::Writes => result.writes.into(),
BenchmarkSelector::ProofSize => result.proof_size.into(),
}
}
fn get_minimum(self, results: &[BenchmarkResult]) -> u128 {
results
.iter()
.map(|result| self.get_value(result))
.min()
.expect("results cannot be empty")
}
}
#[derive(Debug)]
pub enum AnalysisChoice {
MinSquares,
MedianSlopes,
Max,
}
impl Default for AnalysisChoice {
fn default() -> Self {
AnalysisChoice::MinSquares
}
}
impl TryFrom<Option<String>> for AnalysisChoice {
type Error = &'static str;
fn try_from(s: Option<String>) -> Result<Self, Self::Error> {
match s {
None => Ok(AnalysisChoice::default()),
Some(i) => match &i[..] {
"min-squares" | "min_squares" => Ok(AnalysisChoice::MinSquares),
"median-slopes" | "median_slopes" => Ok(AnalysisChoice::MedianSlopes),
"max" => Ok(AnalysisChoice::Max),
_ => Err("invalid analysis string"),
},
}
}
}
fn raw_linear_regression(
xs: &[f64],
ys: &[f64],
x_vars: usize,
with_intercept: bool,
) -> Option<(f64, Vec<f64>, Vec<f64>)> {
let mut data: Vec<f64> = Vec::new();
for (&y, xs) in ys.iter().zip(xs.chunks_exact(x_vars)) {
data.push(y);
if with_intercept {
data.push(1.0);
} else {
data.push(0.0);
}
data.extend(xs);
}
let model = linregress::fit_low_level_regression_model(&data, ys.len(), x_vars + 2).ok()?;
Some((model.parameters()[0], model.parameters()[1..].to_vec(), model.se().to_vec()))
}
fn linear_regression(
xs: Vec<f64>,
mut ys: Vec<f64>,
x_vars: usize,
) -> Option<(f64, Vec<f64>, Vec<f64>)> {
let (intercept, params, errors) = raw_linear_regression(&xs, &ys, x_vars, true)?;
if intercept >= -0.0001 {
return Some((intercept, params, errors[1..].to_vec()))
}
let mut min = ys[0];
for &value in &ys {
if value < min {
min = value;
}
}
for value in &mut ys {
*value -= min;
}
let (intercept, params, errors) = raw_linear_regression(&xs, &ys, x_vars, false)?;
assert!(intercept.abs() <= 0.0001);
Some((min, params, errors[1..].to_vec()))
}
impl Analysis {
fn median_value(r: &Vec<BenchmarkResult>, selector: BenchmarkSelector) -> Option<Self> {
if r.is_empty() {
return None
}
let mut values: Vec<u128> = r
.iter()
.map(|result| match selector {
BenchmarkSelector::ExtrinsicTime => result.extrinsic_time,
BenchmarkSelector::StorageRootTime => result.storage_root_time,
BenchmarkSelector::Reads => result.reads.into(),
BenchmarkSelector::Writes => result.writes.into(),
BenchmarkSelector::ProofSize => result.proof_size.into(),
})
.collect();
values.sort();
let mid = values.len() / 2;
Some(Self {
base: selector.scale_weight(values[mid]),
slopes: Vec::new(),
names: Vec::new(),
value_dists: None,
errors: None,
minimum: selector.get_minimum(&r),
selector,
})
}
pub fn median_slopes(r: &Vec<BenchmarkResult>, selector: BenchmarkSelector) -> Option<Self> {
if r[0].components.is_empty() {
return Self::median_value(r, selector)
}
let results = r[0]
.components
.iter()
.enumerate()
.map(|(i, &(param, _))| {
let mut counted = BTreeMap::<Vec<u32>, usize>::new();
for result in r.iter() {
let mut p = result.components.iter().map(|x| x.1).collect::<Vec<_>>();
p[i] = 0;
*counted.entry(p).or_default() += 1;
}
let others: Vec<u32> =
counted.iter().max_by_key(|i| i.1).expect("r is not empty; qed").0.clone();
let values = r
.iter()
.filter(|v| {
v.components
.iter()
.map(|x| x.1)
.zip(others.iter())
.enumerate()
.all(|(j, (v1, v2))| j == i || v1 == *v2)
})
.map(|result| {
let data = match selector {
BenchmarkSelector::ExtrinsicTime => result.extrinsic_time,
BenchmarkSelector::StorageRootTime => result.storage_root_time,
BenchmarkSelector::Reads => result.reads.into(),
BenchmarkSelector::Writes => result.writes.into(),
BenchmarkSelector::ProofSize => result.proof_size.into(),
};
(result.components[i].1, data)
})
.collect::<Vec<_>>();
(format!("{:?}", param), i, others, values)
})
.collect::<Vec<_>>();
let models = results
.iter()
.map(|(_, _, _, ref values)| {
let mut slopes = vec![];
for (i, &(x1, y1)) in values.iter().enumerate() {
for &(x2, y2) in values.iter().skip(i + 1) {
if x1 != x2 {
slopes.push((y1 as f64 - y2 as f64) / (x1 as f64 - x2 as f64));
}
}
}
slopes.sort_by(|a, b| a.partial_cmp(b).expect("values well defined; qed"));
let slope = slopes[slopes.len() / 2];
let mut offsets = vec![];
for &(x, y) in values.iter() {
offsets.push(y as f64 - slope * x as f64);
}
offsets.sort_by(|a, b| a.partial_cmp(b).expect("values well defined; qed"));
let offset = offsets[offsets.len() / 2];
(offset, slope)
})
.collect::<Vec<_>>();
let models = models
.iter()
.zip(results.iter())
.map(|((offset, slope), (_, i, others, _))| {
let over = others
.iter()
.enumerate()
.filter(|(j, _)| j != i)
.map(|(j, v)| models[j].1 * *v as f64)
.fold(0f64, |acc, i| acc + i);
(*offset - over, *slope)
})
.collect::<Vec<_>>();
let base = selector.scale_and_cast_weight(models[0].0.max(0f64), false);
let slopes = models
.iter()
.map(|x| selector.scale_and_cast_weight(x.1.max(0f64), false))
.collect::<Vec<_>>();
Some(Self {
base,
slopes,
names: results.into_iter().map(|x| x.0).collect::<Vec<_>>(),
value_dists: None,
errors: None,
minimum: selector.get_minimum(&r),
selector,
})
}
pub fn min_squares_iqr(r: &Vec<BenchmarkResult>, selector: BenchmarkSelector) -> Option<Self> {
if r[0].components.is_empty() || r.len() <= 2 {
return Self::median_value(r, selector)
}
let mut results = BTreeMap::<Vec<u32>, Vec<u128>>::new();
for result in r.iter() {
let p = result.components.iter().map(|x| x.1).collect::<Vec<_>>();
results.entry(p).or_default().push(match selector {
BenchmarkSelector::ExtrinsicTime => result.extrinsic_time,
BenchmarkSelector::StorageRootTime => result.storage_root_time,
BenchmarkSelector::Reads => result.reads.into(),
BenchmarkSelector::Writes => result.writes.into(),
BenchmarkSelector::ProofSize => result.proof_size.into(),
})
}
for (_, rs) in results.iter_mut() {
rs.sort();
let ql = rs.len() / 4;
*rs = rs[ql..rs.len() - ql].to_vec();
}
let names = r[0].components.iter().map(|x| format!("{:?}", x.0)).collect::<Vec<_>>();
let value_dists = results
.iter()
.map(|(p, vs)| {
if vs.is_empty() {
return (p.clone(), 0, 0)
}
let total = vs.iter().fold(0u128, |acc, v| acc + *v);
let mean = total / vs.len() as u128;
let sum_sq_diff = vs.iter().fold(0u128, |acc, v| {
let d = mean.max(*v) - mean.min(*v);
acc + d * d
});
let stddev = (sum_sq_diff as f64 / vs.len() as f64).sqrt() as u128;
(p.clone(), mean, stddev)
})
.collect::<Vec<_>>();
let mut ys: Vec<f64> = Vec::new();
let mut xs: Vec<f64> = Vec::new();
for result in results {
let x: Vec<f64> = result.0.iter().map(|value| *value as f64).collect();
for y in result.1 {
xs.extend(x.iter().copied());
ys.push(y as f64);
}
}
let (intercept, slopes, errors) = linear_regression(xs, ys, r[0].components.len())?;
Some(Self {
base: selector.scale_and_cast_weight(intercept, true),
slopes: slopes
.into_iter()
.map(|value| selector.scale_and_cast_weight(value, true))
.collect(),
names,
value_dists: Some(value_dists),
errors: Some(
errors
.into_iter()
.map(|value| selector.scale_and_cast_weight(value, false))
.collect(),
),
minimum: selector.get_minimum(&r),
selector,
})
}
pub fn max(r: &Vec<BenchmarkResult>, selector: BenchmarkSelector) -> Option<Self> {
let median_slopes = Self::median_slopes(r, selector);
let min_squares = Self::min_squares_iqr(r, selector);
if median_slopes.is_none() || min_squares.is_none() {
return None
}
let median_slopes = median_slopes.unwrap();
let min_squares = min_squares.unwrap();
let base = median_slopes.base.max(min_squares.base);
let slopes = median_slopes
.slopes
.into_iter()
.zip(min_squares.slopes.into_iter())
.map(|(a, b): (u128, u128)| a.max(b))
.collect::<Vec<u128>>();
median_slopes
.names
.iter()
.zip(min_squares.names.iter())
.for_each(|(a, b)| assert!(a == b, "benchmark results not in the same order"));
let names = median_slopes.names;
let value_dists = min_squares.value_dists;
let errors = min_squares.errors;
let minimum = selector.get_minimum(&r);
Some(Self { base, slopes, names, value_dists, errors, selector, minimum })
}
}
fn ms(mut nanos: u128) -> String {
let mut x = 100_000u128;
while x > 1 {
if nanos > x * 1_000 {
nanos = nanos / x * x;
break
}
x /= 10;
}
format!("{}", nanos as f64 / 1_000f64)
}
impl std::fmt::Display for Analysis {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(ref value_dists) = self.value_dists {
writeln!(f, "\nData points distribution:")?;
writeln!(
f,
"{} mean µs sigma µs %",
self.names.iter().map(|p| format!("{:>5}", p)).collect::<Vec<_>>().join(" ")
)?;
for (param_values, mean, sigma) in value_dists.iter() {
if *mean == 0 {
writeln!(
f,
"{} {:>8} {:>8} {:>3}.{}%",
param_values
.iter()
.map(|v| format!("{:>5}", v))
.collect::<Vec<_>>()
.join(" "),
ms(*mean),
ms(*sigma),
"?",
"?"
)?;
} else {
writeln!(
f,
"{} {:>8} {:>8} {:>3}.{}%",
param_values
.iter()
.map(|v| format!("{:>5}", v))
.collect::<Vec<_>>()
.join(" "),
ms(*mean),
ms(*sigma),
(sigma * 100 / mean),
(sigma * 1000 / mean % 10)
)?;
}
}
}
if let Some(ref errors) = self.errors {
writeln!(f, "\nQuality and confidence:")?;
writeln!(f, "param error")?;
for (p, se) in self.names.iter().zip(errors.iter()) {
writeln!(f, "{} {:>8}", p, ms(self.selector.nanos_from_weight(*se)))?;
}
}
writeln!(f, "\nModel:")?;
writeln!(f, "Time ~= {:>8}", ms(self.selector.nanos_from_weight(self.base)))?;
for (&t, n) in self.slopes.iter().zip(self.names.iter()) {
writeln!(f, " + {} {:>8}", n, ms(self.selector.nanos_from_weight(t)))?;
}
writeln!(f, " µs")
}
}
impl std::fmt::Debug for Analysis {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.base)?;
for (&m, n) in self.slopes.iter().zip(self.names.iter()) {
write!(f, " + ({} * {})", m, n)?;
}
write!(f, "")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BenchmarkParameter;
fn benchmark_result(
components: Vec<(BenchmarkParameter, u32)>,
extrinsic_time: u128,
storage_root_time: u128,
reads: u32,
writes: u32,
) -> BenchmarkResult {
BenchmarkResult {
components,
extrinsic_time,
storage_root_time,
reads,
repeat_reads: 0,
writes,
repeat_writes: 0,
proof_size: 0,
keys: vec![],
}
}
#[test]
fn test_linear_regression() {
let ys = vec![
3797981.0,
37857779.0,
70569402.0,
104004114.0,
137233924.0,
169826237.0,
203521133.0,
237552333.0,
271082065.0,
305554637.0,
335218347.0,
371759065.0,
405086197.0,
438353555.0,
472891417.0,
505339532.0,
527784778.0,
562590596.0,
635291991.0,
673027090.0,
708119408.0,
];
let xs = vec![
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,
16.0, 17.0, 18.0, 19.0, 20.0,
];
let (intercept, params, errors) = raw_linear_regression(&xs, &ys, 1, true).unwrap();
assert_eq!(intercept as i64, -2712997);
assert_eq!(params.len(), 1);
assert_eq!(params[0] as i64, 34444926);
assert_eq!(errors.len(), 2);
assert_eq!(errors[0] as i64, 4805766);
assert_eq!(errors[1] as i64, 411084);
let (intercept, params, errors) = linear_regression(xs, ys, 1).unwrap();
assert_eq!(intercept as i64, 3797981);
assert_eq!(params.len(), 1);
assert_eq!(params[0] as i64, 33968513);
assert_eq!(errors.len(), 1);
assert_eq!(errors[0] as i64, 217331);
}
#[test]
fn analysis_median_slopes_should_work() {
let data = vec![
benchmark_result(
vec![(BenchmarkParameter::n, 1), (BenchmarkParameter::m, 5)],
11_500_000,
0,
3,
10,
),
benchmark_result(
vec![(BenchmarkParameter::n, 2), (BenchmarkParameter::m, 5)],
12_500_000,
0,
4,
10,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 5)],
13_500_000,
0,
5,
10,
),
benchmark_result(
vec![(BenchmarkParameter::n, 4), (BenchmarkParameter::m, 5)],
14_500_000,
0,
6,
10,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 1)],
13_100_000,
0,
5,
2,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 3)],
13_300_000,
0,
5,
6,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 7)],
13_700_000,
0,
5,
14,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 10)],
14_000_000,
0,
5,
20,
),
];
let extrinsic_time =
Analysis::median_slopes(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
assert_eq!(extrinsic_time.base, 10_000_000_000);
assert_eq!(extrinsic_time.slopes, vec![1_000_000_000, 100_000_000]);
let reads = Analysis::median_slopes(&data, BenchmarkSelector::Reads).unwrap();
assert_eq!(reads.base, 2);
assert_eq!(reads.slopes, vec![1, 0]);
let writes = Analysis::median_slopes(&data, BenchmarkSelector::Writes).unwrap();
assert_eq!(writes.base, 0);
assert_eq!(writes.slopes, vec![0, 2]);
}
#[test]
fn analysis_median_min_squares_should_work() {
let data = vec![
benchmark_result(
vec![(BenchmarkParameter::n, 1), (BenchmarkParameter::m, 5)],
11_500_000,
0,
3,
10,
),
benchmark_result(
vec![(BenchmarkParameter::n, 2), (BenchmarkParameter::m, 5)],
12_500_000,
0,
4,
10,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 5)],
13_500_000,
0,
5,
10,
),
benchmark_result(
vec![(BenchmarkParameter::n, 4), (BenchmarkParameter::m, 5)],
14_500_000,
0,
6,
10,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 1)],
13_100_000,
0,
5,
2,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 3)],
13_300_000,
0,
5,
6,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 7)],
13_700_000,
0,
5,
14,
),
benchmark_result(
vec![(BenchmarkParameter::n, 3), (BenchmarkParameter::m, 10)],
14_000_000,
0,
5,
20,
),
];
let extrinsic_time =
Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
assert_eq!(extrinsic_time.base, 10_000_000_000);
assert_eq!(extrinsic_time.slopes, vec![1000000000, 100000000]);
let reads = Analysis::min_squares_iqr(&data, BenchmarkSelector::Reads).unwrap();
assert_eq!(reads.base, 2);
assert_eq!(reads.slopes, vec![1, 0]);
let writes = Analysis::min_squares_iqr(&data, BenchmarkSelector::Writes).unwrap();
assert_eq!(writes.base, 0);
assert_eq!(writes.slopes, vec![0, 2]);
}
#[test]
fn analysis_min_squares_iqr_uses_multiple_samples_for_same_parameters() {
let data = vec![
benchmark_result(vec![(BenchmarkParameter::n, 0)], 2_000_000, 0, 0, 0),
benchmark_result(vec![(BenchmarkParameter::n, 0)], 4_000_000, 0, 0, 0),
benchmark_result(vec![(BenchmarkParameter::n, 1)], 4_000_000, 0, 0, 0),
benchmark_result(vec![(BenchmarkParameter::n, 1)], 8_000_000, 0, 0, 0),
];
let extrinsic_time =
Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
assert_eq!(extrinsic_time.base, 3_000_000_000);
assert_eq!(extrinsic_time.slopes, vec![3_000_000_000]);
}
#[test]
fn intercept_of_a_little_under_zero_is_rounded_up_to_zero() {
let data = vec![
benchmark_result(vec![(BenchmarkParameter::n, 1)], 2, 0, 0, 0),
benchmark_result(vec![(BenchmarkParameter::n, 2)], 4, 0, 0, 0),
benchmark_result(vec![(BenchmarkParameter::n, 3)], 6, 0, 0, 0),
];
let extrinsic_time =
Analysis::min_squares_iqr(&data, BenchmarkSelector::ExtrinsicTime).unwrap();
assert_eq!(extrinsic_time.base, 0);
assert_eq!(extrinsic_time.slopes, vec![2000]);
}
}