Browse Source

add percentiles to summary output

pull/59/head
JackKCWong 2 years ago
parent
commit
a62271ba02
  1. 60
      Cargo.lock
  2. 1
      Cargo.toml
  3. 2
      src/lib.rs
  4. 4
      src/main.rs
  5. 18
      src/plot/histogram.rs
  6. 9
      src/plot/xy.rs
  7. 57
      src/stats/mod.rs

60
Cargo.lock generated

@ -428,6 +428,17 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@ -521,9 +532,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.147" version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]] [[package]]
name = "link-cplusplus" name = "link-cplusplus"
@ -569,6 +580,7 @@ dependencies = [
"humantime", "humantime",
"log", "log",
"predicates", "predicates",
"rand",
"regex", "regex",
"serial_test", "serial_test",
"simplelog", "simplelog",
@ -653,6 +665,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "predicates" name = "predicates"
version = "3.0.3" version = "3.0.3"
@ -702,6 +720,36 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.16" version = "0.2.16"
@ -906,7 +954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi 0.10.0+wasi-snapshot-preview1",
"winapi", "winapi",
] ]
@ -966,6 +1014,12 @@ version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.83" version = "0.2.83"

1
Cargo.toml

@ -34,6 +34,7 @@ chrono = "^0.4.28"
humantime = "^2" humantime = "^2"
simplelog = "^0" simplelog = "^0"
log = "^0" log = "^0"
rand = "0.8.5"
[dev-dependencies] [dev-dependencies]
float_eq = "^1" float_eq = "^1"

2
src/lib.rs

@ -8,7 +8,7 @@
//! ```rust,no_run //! ```rust,no_run
//! use lowcharts::plot; //! use lowcharts::plot;
//! //!
//! let vec = &[-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99]; //! let vec = &mut [-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99];
//! // Plot a histogram of the above vector, with 4 buckets and a precision //! // Plot a histogram of the above vector, with 4 buckets and a precision
//! // chosen by library //! // chosen by library
//! let options = plot::HistogramOptions { intervals: 4, ..Default::default() }; //! let options = plot::HistogramOptions { intervals: 4, ..Default::default() };

4
src/main.rs

@ -105,7 +105,7 @@ fn histogram(matches: &ArgMatches) -> i32 {
Ok(r) => r, Ok(r) => r,
_ => return 2, _ => return 2,
}; };
let vec = reader.read(matches.value_of("input").unwrap()); let mut vec = reader.read(matches.value_of("input").unwrap());
if !assert_data(&vec, 1) { if !assert_data(&vec, 1) {
return 1; return 1;
} }
@ -117,7 +117,7 @@ fn histogram(matches: &ArgMatches) -> i32 {
options.log_scale = matches.is_present("log-scale"); options.log_scale = matches.is_present("log-scale");
options.intervals = matches.value_of_t("intervals").unwrap(); options.intervals = matches.value_of_t("intervals").unwrap();
let width = matches.value_of_t("width").unwrap(); let width = matches.value_of_t("width").unwrap();
let histogram = plot::Histogram::new(&vec, options); let histogram = plot::Histogram::new(&mut vec, options);
print!("{histogram:width$}"); print!("{histogram:width$}");
0 0
} }

18
src/plot/histogram.rs

@ -54,7 +54,7 @@ impl Histogram {
/// ///
/// `options` is a `HistogramOptions` struct with the preferences to create /// `options` is a `HistogramOptions` struct with the preferences to create
/// histogram. /// histogram.
pub fn new(vec: &[f64], mut options: HistogramOptions) -> Self { pub fn new(vec: &mut [f64], mut options: HistogramOptions) -> Self {
let mut stats = Stats::new(vec, options.precision); let mut stats = Stats::new(vec, options.precision);
if options.log_scale { if options.log_scale {
stats.min = 0.0; // We will silently discard negative values stats.min = 0.0; // We will silently discard negative values
@ -222,7 +222,7 @@ mod tests {
#[test] #[test]
fn test_buckets() { fn test_buckets() {
let stats = Stats::new(&[-2.0, 14.0], None); let stats = Stats::new(&mut [-2.0, 14.0], None);
let options = HistogramOptions { let options = HistogramOptions {
intervals: 8, intervals: 8,
..Default::default() ..Default::default()
@ -247,14 +247,14 @@ mod tests {
intervals: 6, intervals: 6,
..Default::default() ..Default::default()
}; };
let mut hist = Histogram::new_with_stats(Stats::new(&[-2.0, 4.0], None), &options); let mut hist = Histogram::new_with_stats(Stats::new(&mut [-2.0, 4.0], None), &options);
hist.load(&[-1.0, 2.0, -1.0, 2.0, 10.0, 10.0, 10.0, -10.0]); hist.load(&[-1.0, 2.0, -1.0, 2.0, 10.0, 10.0, 10.0, -10.0]);
assert_eq!(hist.top, 2); assert_eq!(hist.top, 2);
} }
#[test] #[test]
fn display_test() { fn display_test() {
let stats = Stats::new(&[-2.0, 14.0], None); let stats = Stats::new(&mut [-2.0, 14.0], None);
let options = HistogramOptions { let options = HistogramOptions {
intervals: 8, intervals: 8,
precision: Some(3), precision: Some(3),
@ -280,7 +280,7 @@ mod tests {
precision: Some(3), precision: Some(3),
..Default::default() ..Default::default()
}; };
let mut hist = Histogram::new_with_stats(Stats::new(&[-2.0, 14.0], None), &options); let mut hist = Histogram::new_with_stats(Stats::new(&mut [-2.0, 14.0], None), &options);
hist.load(&[ hist.load(&[
-1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96, -1.0, -1.1, 2.0, 2.0, 2.1, -0.9, 11.0, 11.2, 1.9, 1.99, 1.98, 1.97, 1.96,
]); ]);
@ -291,7 +291,7 @@ mod tests {
#[test] #[test]
fn display_test_human_units() { fn display_test_human_units() {
let vector = &[ let vector = &mut [
-1.0, -1.0,
-12000000.0, -12000000.0,
-12000001.0, -12000001.0,
@ -320,7 +320,7 @@ mod tests {
#[test] #[test]
fn display_test_log_scale() { fn display_test_log_scale() {
let hist = Histogram::new( let hist = Histogram::new(
&[0.4, 0.4, 0.4, 0.4, 255.0, 0.2, 1.2, 128.0, 126.0, -7.0], &mut [0.4, 0.4, 0.4, 0.4, 255.0, 0.2, 1.2, 128.0, 126.0, -7.0],
HistogramOptions { HistogramOptions {
intervals: 8, intervals: 8,
log_scale: true, log_scale: true,
@ -402,7 +402,7 @@ mod tests {
intervals: 8, intervals: 8,
..Default::default() ..Default::default()
}; };
let hist = Histogram::new_with_stats(Stats::new(&[-12.0, 4.0], None), &options); let hist = Histogram::new_with_stats(Stats::new(&mut [-12.0, 4.0], None), &options);
assert!(hist.find_slot(-13.0) == None); assert!(hist.find_slot(-13.0) == None);
assert!(hist.find_slot(13.0) == None); assert!(hist.find_slot(13.0) == None);
assert!(hist.find_slot(-12.0) == Some(0)); assert!(hist.find_slot(-12.0) == Some(0));
@ -416,7 +416,7 @@ mod tests {
fn find_slot_logarithmic() { fn find_slot_logarithmic() {
let hist = Histogram::new( let hist = Histogram::new(
// More than 8 values to avoid interval truncation // More than 8 values to avoid interval truncation
&[255.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -2000.0], &mut [255.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -2000.0],
HistogramOptions { HistogramOptions {
intervals: 8, intervals: 8,
log_scale: true, log_scale: true,

9
src/plot/xy.rs

@ -1,3 +1,4 @@
use std::borrow::BorrowMut;
use std::fmt; use std::fmt;
use std::ops::Range; use std::ops::Range;
@ -32,7 +33,7 @@ impl XyPlot {
/// "None" is used, human units will be used, with an heuristic based on the /// "None" is used, human units will be used, with an heuristic based on the
/// input data for deciding the units and the decimal places. /// input data for deciding the units and the decimal places.
pub fn new(vec: &[f64], width: usize, height: usize, precision: Option<usize>) -> Self { pub fn new(vec: &[f64], width: usize, height: usize, precision: Option<usize>) -> Self {
let mut plot = Self::new_with_stats(width, height, Stats::new(vec, precision), precision); let mut plot = Self::new_with_stats(width, height, Stats::new(vec.to_vec().borrow_mut(), precision), precision);
plot.load(vec); plot.load(vec);
plot plot
} }
@ -132,7 +133,7 @@ mod tests {
#[test] #[test]
fn basic_test() { fn basic_test() {
let stats = Stats::new(&[-1.0, 4.0], None); let stats = Stats::new(&mut [-1.0, 4.0], None);
let mut plot = XyPlot::new_with_stats(3, 5, stats, Some(3)); let mut plot = XyPlot::new_with_stats(3, 5, stats, Some(3));
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]); plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]);
assert_float_eq!(plot.x_axis[0], -0.5, rmax <= f64::EPSILON); assert_float_eq!(plot.x_axis[0], -0.5, rmax <= f64::EPSILON);
@ -146,7 +147,7 @@ mod tests {
#[test] #[test]
fn display_test() { fn display_test() {
let stats = Stats::new(&[-1.0, 4.0], None); let stats = Stats::new(&mut [-1.0, 4.0], None);
let mut plot = XyPlot::new_with_stats(3, 5, stats, Some(3)); let mut plot = XyPlot::new_with_stats(3, 5, stats, Some(3));
plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]); plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]);
Paint::disable(); Paint::disable();
@ -159,7 +160,7 @@ mod tests {
#[test] #[test]
fn display_test_human_units() { fn display_test_human_units() {
let vector = &[1000000.0, -1000000.0, -2000000.0, -4000000.0]; let vector = &mut [1000000.0, -1000000.0, -2000000.0, -4000000.0];
let plot = XyPlot::new(vector, 3, 5, None); let plot = XyPlot::new(vector, 3, 5, None);
Paint::disable(); Paint::disable();
let display = format!("{plot}"); let display = format!("{plot}");

57
src/stats/mod.rs

@ -21,6 +21,27 @@ pub struct Stats {
/// Number of samples of the input values. /// Number of samples of the input values.
pub samples: usize, pub samples: usize,
precision: Option<usize>, // If None, then human friendly display will be used precision: Option<usize>, // If None, then human friendly display will be used
/// 50 percentile
pub p50: f64,
/// 90 percentile
pub p90: f64,
/// 95 percentile
pub p95: f64,
/// 99 percentile
pub p99: f64,
}
fn percentiles(vec: &mut [f64]) -> (f64, f64, f64, f64) {
vec.sort_by(|a, b| a.partial_cmp(b).unwrap());
let len = vec.len();
let p50 = vec[len / 2];
let p90 = vec[(len * 9) / 10];
let p95 = vec[(len * 95) / 100];
let p99 = vec[(len * 99) / 100];
(p50, p90, p95, p99)
} }
impl Stats { impl Stats {
@ -29,7 +50,7 @@ impl Stats {
/// `precision` is an Option with the number of decimals to display. If /// `precision` is an Option with the number of decimals to display. If
/// "None" is used, human units will be used, with an heuristic based on the /// "None" is used, human units will be used, with an heuristic based on the
/// input data for deciding the units and the decimal places. /// input data for deciding the units and the decimal places.
pub fn new(vec: &[f64], precision: Option<usize>) -> Self { pub fn new(vec: &mut [f64], precision: Option<usize>) -> Self {
let mut max = vec[0]; let mut max = vec[0];
let mut min = max; let mut min = max;
let mut temp: f64 = 0.0; let mut temp: f64 = 0.0;
@ -42,6 +63,7 @@ impl Stats {
} }
let var = temp / vec.len() as f64; let var = temp / vec.len() as f64;
let std = var.sqrt(); let std = var.sqrt();
let (p50, p90, p95, p99) = percentiles(vec);
Self { Self {
min, min,
max, max,
@ -50,6 +72,10 @@ impl Stats {
var, var,
samples: vec.len(), samples: vec.len(),
precision, precision,
p50,
p90,
p95,
p99,
} }
} }
} }
@ -73,6 +99,14 @@ impl fmt::Display for Stats {
avg = Blue.paint(formatter.format(self.avg)), avg = Blue.paint(formatter.format(self.avg)),
var = Blue.paint(format!("{:.3}", self.var)), var = Blue.paint(format!("{:.3}", self.var)),
std = Blue.paint(format!("{:.3}", self.std)), std = Blue.paint(format!("{:.3}", self.std)),
)?;
writeln!(
f,
"p50 = {p50}; p90 = {p90}; p95 = {p95}; p99 = {p99}",
p50 = Blue.paint(formatter.format(self.p50)),
p90 = Blue.paint(formatter.format(self.p90)),
p95 = Blue.paint(formatter.format(self.p95)),
p99 = Blue.paint(formatter.format(self.p99)),
) )
} }
} }
@ -82,10 +116,11 @@ mod tests {
use super::*; use super::*;
use float_eq::assert_float_eq; use float_eq::assert_float_eq;
use yansi::Paint; use yansi::Paint;
use rand::{seq::SliceRandom, thread_rng};
#[test] #[test]
fn basic_test() { fn basic_test() {
let stats = Stats::new(&[1.1, 3.3, 2.2], Some(3)); let stats = Stats::new(&mut [1.1, 3.3, 2.2], Some(3));
assert_eq!(3_usize, stats.samples); assert_eq!(3_usize, stats.samples);
assert_float_eq!(stats.avg, 2.2, rmax <= f64::EPSILON); assert_float_eq!(stats.avg, 2.2, rmax <= f64::EPSILON);
assert_float_eq!(stats.min, 1.1, rmax <= f64::EPSILON); assert_float_eq!(stats.min, 1.1, rmax <= f64::EPSILON);
@ -96,7 +131,7 @@ mod tests {
#[test] #[test]
fn test_display() { fn test_display() {
let stats = Stats::new(&[1.1, 3.3, 2.2], Some(3)); let stats = Stats::new(&mut [1.1, 3.3, 2.2], Some(3));
Paint::disable(); Paint::disable();
let display = format!("{stats}"); let display = format!("{stats}");
assert!(display.contains("Samples = 3")); assert!(display.contains("Samples = 3"));
@ -107,11 +142,25 @@ mod tests {
#[test] #[test]
fn test_big_num() { fn test_big_num() {
let stats = Stats::new(&[123456789.1234, 123456788.1234], None); let stats = Stats::new(&mut [123456789.1234, 123456788.1234], None);
Paint::disable(); Paint::disable();
let display = format!("{stats}"); let display = format!("{stats}");
assert!(display.contains("Samples = 2")); assert!(display.contains("Samples = 2"));
assert!(display.contains("Min = 123456788.123")); assert!(display.contains("Min = 123456788.123"));
assert!(display.contains("Max = 123456789.123")); assert!(display.contains("Max = 123456789.123"));
} }
#[test]
fn test_percentile() {
let mut vec: Vec<f64> = (0..100).map(|i| i as f64).collect();
vec.shuffle(&mut thread_rng());
let stats = Stats::new(&mut vec, Some(1));
Paint::disable();
let display = format!("{stats}");
println!("{}", display);
assert!(display.contains("p50 = 50.0"));
assert!(display.contains("p90 = 90.0"));
assert!(display.contains("p95 = 95.0"));
assert!(display.contains("p99 = 99.0"));
}
} }

Loading…
Cancel
Save