From a62271ba0259a2d58dca2da5c079d417010f4670 Mon Sep 17 00:00:00 2001 From: JackKCWong Date: Fri, 9 Feb 2024 16:14:48 +0800 Subject: [PATCH 1/3] add percentiles to summary output --- Cargo.lock | 60 ++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + src/lib.rs | 2 +- src/main.rs | 4 +-- src/plot/histogram.rs | 18 ++++++------- src/plot/xy.rs | 9 ++++--- src/stats/mod.rs | 57 +++++++++++++++++++++++++++++++++++++--- 7 files changed, 128 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 088ec94..93a51c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,17 @@ dependencies = [ "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]] name = "hashbrown" version = "0.12.3" @@ -521,9 +532,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "link-cplusplus" @@ -569,6 +580,7 @@ dependencies = [ "humantime", "log", "predicates", + "rand", "regex", "serial_test", "simplelog", @@ -653,6 +665,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "predicates" version = "3.0.3" @@ -702,6 +720,36 @@ dependencies = [ "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]] name = "redox_syscall" version = "0.2.16" @@ -906,7 +954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -966,6 +1014,12 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "wasm-bindgen" version = "0.2.83" diff --git a/Cargo.toml b/Cargo.toml index f1c9f67..421a06a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ chrono = "^0.4.28" humantime = "^2" simplelog = "^0" log = "^0" +rand = "0.8.5" [dev-dependencies] float_eq = "^1" diff --git a/src/lib.rs b/src/lib.rs index e5c5c09..0fc4233 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ //! ```rust,no_run //! 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 //! // chosen by library //! let options = plot::HistogramOptions { intervals: 4, ..Default::default() }; diff --git a/src/main.rs b/src/main.rs index ecd1d34..44e476d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -105,7 +105,7 @@ fn histogram(matches: &ArgMatches) -> i32 { Ok(r) => r, _ => 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) { return 1; } @@ -117,7 +117,7 @@ fn histogram(matches: &ArgMatches) -> i32 { options.log_scale = matches.is_present("log-scale"); options.intervals = matches.value_of_t("intervals").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$}"); 0 } diff --git a/src/plot/histogram.rs b/src/plot/histogram.rs index f0ad228..ac9d15e 100644 --- a/src/plot/histogram.rs +++ b/src/plot/histogram.rs @@ -54,7 +54,7 @@ impl Histogram { /// /// `options` is a `HistogramOptions` struct with the preferences to create /// 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); if options.log_scale { stats.min = 0.0; // We will silently discard negative values @@ -222,7 +222,7 @@ mod tests { #[test] 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 { intervals: 8, ..Default::default() @@ -247,14 +247,14 @@ mod tests { intervals: 6, ..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]); assert_eq!(hist.top, 2); } #[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 { intervals: 8, precision: Some(3), @@ -280,7 +280,7 @@ mod tests { precision: Some(3), ..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(&[ -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] fn display_test_human_units() { - let vector = &[ + let vector = &mut [ -1.0, -12000000.0, -12000001.0, @@ -320,7 +320,7 @@ mod tests { #[test] fn display_test_log_scale() { 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 { intervals: 8, log_scale: true, @@ -402,7 +402,7 @@ mod tests { intervals: 8, ..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(-12.0) == Some(0)); @@ -416,7 +416,7 @@ mod tests { fn find_slot_logarithmic() { let hist = Histogram::new( // 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 { intervals: 8, log_scale: true, diff --git a/src/plot/xy.rs b/src/plot/xy.rs index c10ff4a..e6f56b5 100644 --- a/src/plot/xy.rs +++ b/src/plot/xy.rs @@ -1,3 +1,4 @@ +use std::borrow::BorrowMut; use std::fmt; use std::ops::Range; @@ -32,7 +33,7 @@ impl XyPlot { /// "None" is used, human units will be used, with an heuristic based on the /// input data for deciding the units and the decimal places. pub fn new(vec: &[f64], width: usize, height: usize, precision: Option) -> 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 } @@ -132,7 +133,7 @@ mod tests { #[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)); 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); @@ -146,7 +147,7 @@ mod tests { #[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)); plot.load(&[-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, -1.0]); Paint::disable(); @@ -159,7 +160,7 @@ mod tests { #[test] 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); Paint::disable(); let display = format!("{plot}"); diff --git a/src/stats/mod.rs b/src/stats/mod.rs index fea34b2..495503d 100644 --- a/src/stats/mod.rs +++ b/src/stats/mod.rs @@ -21,6 +21,27 @@ pub struct Stats { /// Number of samples of the input values. pub samples: usize, precision: Option, // 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 { @@ -29,7 +50,7 @@ impl Stats { /// `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 /// input data for deciding the units and the decimal places. - pub fn new(vec: &[f64], precision: Option) -> Self { + pub fn new(vec: &mut [f64], precision: Option) -> Self { let mut max = vec[0]; let mut min = max; let mut temp: f64 = 0.0; @@ -42,6 +63,7 @@ impl Stats { } let var = temp / vec.len() as f64; let std = var.sqrt(); + let (p50, p90, p95, p99) = percentiles(vec); Self { min, max, @@ -50,6 +72,10 @@ impl Stats { var, samples: vec.len(), precision, + p50, + p90, + p95, + p99, } } } @@ -73,6 +99,14 @@ impl fmt::Display for Stats { avg = Blue.paint(formatter.format(self.avg)), var = Blue.paint(format!("{:.3}", self.var)), 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 float_eq::assert_float_eq; use yansi::Paint; + use rand::{seq::SliceRandom, thread_rng}; #[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_float_eq!(stats.avg, 2.2, rmax <= f64::EPSILON); assert_float_eq!(stats.min, 1.1, rmax <= f64::EPSILON); @@ -96,7 +131,7 @@ mod tests { #[test] 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(); let display = format!("{stats}"); assert!(display.contains("Samples = 3")); @@ -107,11 +142,25 @@ mod tests { #[test] 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(); let display = format!("{stats}"); assert!(display.contains("Samples = 2")); assert!(display.contains("Min = 123456788.123")); assert!(display.contains("Max = 123456789.123")); } + + #[test] + fn test_percentile() { + let mut vec: Vec = (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")); + } } From d97c8ce39551fb8dfd7ab30e67438b0dcf252c7b Mon Sep 17 00:00:00 2001 From: JackKCWong Date: Fri, 9 Feb 2024 21:39:56 +0800 Subject: [PATCH 2/3] fix test dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 421a06a..be63974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ chrono = "^0.4.28" humantime = "^2" simplelog = "^0" log = "^0" -rand = "0.8.5" [dev-dependencies] float_eq = "^1" @@ -42,3 +41,4 @@ tempfile = "3" assert_cmd = "^2" predicates = "^3" serial_test = "2" +rand = "0.8.5" From efddcc9a1d377f85e334a6be841d74e73b462cf7 Mon Sep 17 00:00:00 2001 From: JackKCWong Date: Sat, 10 Feb 2024 08:47:27 +0800 Subject: [PATCH 3/3] fix format --- src/plot/xy.rs | 7 ++++++- src/stats/mod.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/plot/xy.rs b/src/plot/xy.rs index e6f56b5..6e1a6e3 100644 --- a/src/plot/xy.rs +++ b/src/plot/xy.rs @@ -33,7 +33,12 @@ impl XyPlot { /// "None" is used, human units will be used, with an heuristic based on the /// input data for deciding the units and the decimal places. pub fn new(vec: &[f64], width: usize, height: usize, precision: Option) -> Self { - let mut plot = Self::new_with_stats(width, height, Stats::new(vec.to_vec().borrow_mut(), precision), precision); + let mut plot = Self::new_with_stats( + width, + height, + Stats::new(vec.to_vec().borrow_mut(), precision), + precision, + ); plot.load(vec); plot } diff --git a/src/stats/mod.rs b/src/stats/mod.rs index 495503d..78e7f16 100644 --- a/src/stats/mod.rs +++ b/src/stats/mod.rs @@ -115,8 +115,8 @@ impl fmt::Display for Stats { mod tests { use super::*; use float_eq::assert_float_eq; - use yansi::Paint; use rand::{seq::SliceRandom, thread_rng}; + use yansi::Paint; #[test] fn basic_test() {