frame_benchmarking_cli/shared/
stats.rs1use sc_cli::Result;
22
23use serde::Serialize;
24use std::{fmt, result, str::FromStr};
25
26#[derive(Serialize, Default, Clone)]
29pub struct Stats {
30 pub sum: u64,
32 pub min: u64,
34 pub max: u64,
36
37 pub avg: u64,
39 pub median: u64,
41 pub stddev: f64,
43
44 pub p99: u64,
46 pub p95: u64,
48 pub p75: u64,
50}
51
52#[derive(Debug, Clone, Copy, Serialize, PartialEq)]
55pub enum StatSelect {
56 Maximum,
58 Average,
60 Median,
62 P99Percentile,
64 P95Percentile,
66 P75Percentile,
68}
69
70impl Stats {
71 pub fn new(xs: &Vec<u64>) -> Result<Self> {
73 if xs.is_empty() {
74 return Err("Empty input is invalid".into())
75 }
76 let (avg, stddev) = Self::avg_and_stddev(xs);
77
78 Ok(Self {
79 sum: xs.iter().sum(),
80 min: *xs.iter().min().expect("Checked for non-empty above"),
81 max: *xs.iter().max().expect("Checked for non-empty above"),
82
83 avg: avg as u64,
84 median: Self::percentile(xs.clone(), 0.50),
85 stddev: (stddev * 100.0).round() / 100.0, p99: Self::percentile(xs.clone(), 0.99),
88 p95: Self::percentile(xs.clone(), 0.95),
89 p75: Self::percentile(xs.clone(), 0.75),
90 })
91 }
92
93 pub fn select(&self, s: StatSelect) -> u64 {
95 match s {
96 StatSelect::Maximum => self.max,
97 StatSelect::Average => self.avg,
98 StatSelect::Median => self.median,
99 StatSelect::P99Percentile => self.p99,
100 StatSelect::P95Percentile => self.p95,
101 StatSelect::P75Percentile => self.p75,
102 }
103 }
104
105 fn avg_and_stddev(xs: &Vec<u64>) -> (f64, f64) {
107 let avg = xs.iter().map(|x| *x as f64).sum::<f64>() / xs.len() as f64;
108 let variance = xs.iter().map(|x| (*x as f64 - avg).powi(2)).sum::<f64>() / xs.len() as f64;
109 (avg, variance.sqrt())
110 }
111
112 fn percentile(mut xs: Vec<u64>, p: f64) -> u64 {
115 xs.sort();
116 let index = (xs.len() as f64 * p).ceil() as usize - 1;
117 xs[index.clamp(0, xs.len() - 1)]
118 }
119}
120
121impl fmt::Debug for Stats {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 writeln!(f, "Total: {}", self.sum)?;
124 writeln!(f, "Min: {}, Max: {}", self.min, self.max)?;
125 writeln!(f, "Average: {}, Median: {}, Stddev: {}", self.avg, self.median, self.stddev)?;
126 write!(f, "Percentiles 99th, 95th, 75th: {}, {}, {}", self.p99, self.p95, self.p75)
127 }
128}
129
130impl Default for StatSelect {
131 fn default() -> Self {
133 Self::Average
134 }
135}
136
137impl FromStr for StatSelect {
138 type Err = &'static str;
139
140 fn from_str(day: &str) -> result::Result<Self, Self::Err> {
141 match day.to_lowercase().as_str() {
142 "max" => Ok(Self::Maximum),
143 "average" => Ok(Self::Average),
144 "median" => Ok(Self::Median),
145 "p99" => Ok(Self::P99Percentile),
146 "p95" => Ok(Self::P95Percentile),
147 "p75" => Ok(Self::P75Percentile),
148 _ => Err("String was not a StatSelect"),
149 }
150 }
151}
152
153#[cfg(test)]
154mod test_stats {
155 use super::Stats;
156 use rand::{seq::SliceRandom, thread_rng};
157
158 #[test]
159 fn stats_correct() {
160 let mut data: Vec<u64> = (1..=100).collect();
161 data.shuffle(&mut thread_rng());
162 let stats = Stats::new(&data).unwrap();
163
164 assert_eq!(stats.sum, 5050);
165 assert_eq!(stats.min, 1);
166 assert_eq!(stats.max, 100);
167
168 assert_eq!(stats.avg, 50);
169 assert_eq!(stats.median, 50); assert_eq!(stats.stddev, 28.87); assert_eq!(stats.p99, 99);
173 assert_eq!(stats.p95, 95);
174 assert_eq!(stats.p75, 75);
175 }
176
177 #[test]
178 fn no_panic_short_lengths() {
179 assert!(Stats::new(&vec![]).is_err());
181
182 for l in 1..10 {
184 let data = (0..=l).collect();
185 assert!(Stats::new(&data).is_ok());
186 }
187 }
188}