diff --git a/src/main.rs b/src/main.rs index 68709026..810b78ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use hyper::http::{ use printer::PrintMode; use rand::prelude::*; use rand_regex::Regex; +use result_data::ResultData; use std::{io::Read, str::FromStr}; use url::Url; use url_generator::UrlGenerator; @@ -18,6 +19,7 @@ mod client; mod histogram; mod monitor; mod printer; +mod result_data; mod timescale; mod url_generator; @@ -25,8 +27,6 @@ mod url_generator; #[global_allocator] static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; -use client::{ClientError, RequestResult}; - #[derive(Parser)] #[clap(author, about, version, override_usage = "oha [FLAGS] [OPTIONS] ")] #[command(arg_required_else_help(true))] @@ -427,7 +427,7 @@ async fn main() -> anyhow::Result<()> { } }); - let mut all: Vec> = Vec::new(); + let mut all: ResultData = Default::default(); loop { tokio::select! { report = result_rx.recv_async() => { @@ -610,7 +610,7 @@ async fn main() -> anyhow::Result<()> { let duration = start.elapsed(); - let res: Vec> = data_collector.await??; + let res: ResultData = data_collector.await??; printer::print_result( &mut std::io::stdout(), diff --git a/src/monitor.rs b/src/monitor.rs index f13d68e2..dc3e6a9a 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -18,6 +18,7 @@ use std::{collections::BTreeMap, io}; use crate::{ client::{ClientError, RequestResult}, printer::PrintMode, + result_data::ResultData, timescale::{TimeLabel, TimeScale}, }; @@ -65,7 +66,7 @@ pub struct Monitor { } impl Monitor { - pub async fn monitor(self) -> Result>, std::io::Error> { + pub async fn monitor(self) -> Result { crossterm::terminal::enable_raw_mode()?; io::stdout().execute(crossterm::terminal::EnterAlternateScreen)?; io::stdout().execute(crossterm::cursor::Hide)?; @@ -77,11 +78,9 @@ impl Monitor { // Return this when ends to application print summary // We must not read all data from this due to computational cost. - let mut all: Vec> = Vec::new(); + let mut all: ResultData = Default::default(); // stats for HTTP status let mut status_dist: BTreeMap = Default::default(); - // stats for Error - let mut error_dist: BTreeMap = Default::default(); #[cfg(unix)] // Limit for number open files. eg. ulimit -n @@ -100,9 +99,8 @@ impl Monitor { loop { match self.report_receiver.try_recv() { Ok(report) => { - match report.as_ref() { - Ok(report) => *status_dist.entry(report.status).or_default() += 1, - Err(e) => *error_dist.entry(e.to_string()).or_default() += 1, + if let Ok(report) = report.as_ref() { + *status_dist.entry(report.status).or_default() += 1; } all.push(report); } @@ -136,19 +134,17 @@ impl Monitor { let mut bar_num_req = vec![0u64; count]; let short_bin = (now - self.start).as_secs_f64() % bin; - for r in all.iter().rev() { - if let Ok(r) = r.as_ref() { - let past = (now - r.end).as_secs_f64(); - let i = if past <= short_bin { - 0 - } else { - 1 + ((past - short_bin) / bin) as usize - }; - if i >= bar_num_req.len() { - break; - } - bar_num_req[i] += 1; + for r in all.success.iter().rev() { + let past = (now - r.end).as_secs_f64(); + let i = if past <= short_bin { + 0 + } else { + 1 + ((past - short_bin) / bin) as usize + }; + if i >= bar_num_req.len() { + break; } + bar_num_req[i] += 1; } let cols = bar_num_req @@ -189,7 +185,7 @@ impl Monitor { [ Constraint::Length(3), Constraint::Length(8), - Constraint::Length(error_dist.len() as u16 + 2), + Constraint::Length(all.error.len() as u16 + 2), Constraint::Fill(1), ] .as_ref(), @@ -224,9 +220,9 @@ impl Monitor { f.render_widget(gauge, row4[0]); let last_1_timescale = all + .success .iter() .rev() - .filter_map(|r| r.as_ref().ok()) .take_while(|r| (now - r.end).as_secs_f64() <= timescale.as_secs_f64()) .collect::>(); @@ -316,7 +312,7 @@ impl Monitor { ); f.render_widget(stats2, mid[1]); - let mut error_v: Vec<(String, usize)> = error_dist.clone().into_iter().collect(); + let mut error_v: Vec<(String, usize)> = all.error.clone().into_iter().collect(); error_v.sort_by_key(|t| std::cmp::Reverse(t.1)); let errors_text = error_v .into_iter() @@ -370,9 +366,9 @@ impl Monitor { } .max(2); let values = all + .success .iter() .rev() - .filter_map(|r| r.as_ref().ok()) .take_while(|r| (now - r.end).as_secs_f64() < timescale.as_secs_f64()) .map(|r| r.duration().as_secs_f64()) .collect::>(); diff --git a/src/printer.rs b/src/printer.rs index 2567c931..6e86076b 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,6 +1,7 @@ use crate::{ - client::{ClientError, ConnectionTime, RequestResult}, + client::{ClientError, ConnectionTime}, histogram::histogram, + result_data::ResultData, }; use average::{Max, Variance}; use byte_unit::Byte; @@ -99,7 +100,7 @@ pub fn print_result( w: &mut W, mode: PrintMode, start: Instant, - res: &[Result], + res: &ResultData, total_duration: Duration, disable_color: bool, stats_success_breakdown: bool, @@ -121,7 +122,7 @@ pub fn print_result( fn print_json( w: &mut W, start: Instant, - res: &[Result], + res: &ResultData, total_duration: Duration, stats_success_breakdown: bool, ) -> serde_json::Result<()> { @@ -254,8 +255,8 @@ fn print_json( } let mut ends = res + .success .iter() - .filter_map(|r| r.as_ref().ok()) .map(|r| (r.end - start).as_secs_f64()) .collect::>(); ends.push(0.0); @@ -298,15 +299,10 @@ fn print_json( let mut status_code_distribution: BTreeMap = Default::default(); - for s in res.iter().filter_map(|r| r.as_ref().ok()).map(|r| r.status) { + for s in res.success.iter().map(|r| r.status) { *status_code_distribution.entry(s).or_default() += 1; } - let mut error_distribution: BTreeMap = Default::default(); - for e in res.iter().filter_map(|r| r.as_ref().err()) { - *error_distribution.entry(e.to_string()).or_default() += 1; - } - let connection_times: Vec<(std::time::Instant, ConnectionTime)> = calculate_connection_times_base(res); let details = Details { @@ -338,7 +334,7 @@ fn print_json( .into_iter() .map(|(k, v)| (k.as_u16().to_string(), v)) .collect(), - error_distribution, + error_distribution: res.error.clone(), }, ) } @@ -346,7 +342,7 @@ fn print_json( /// Print all summary as Text fn print_summary( w: &mut W, - res: &[Result], + res: &ResultData, total_duration: Duration, disable_color: bool, stats_success_breakdown: bool, @@ -492,7 +488,7 @@ fn print_summary( let mut status_dist: BTreeMap = Default::default(); - for s in res.iter().filter_map(|r| r.as_ref().ok()).map(|r| r.status) { + for s in res.success.iter().map(|r| r.status) { *status_dist.entry(s).or_default() += 1; } @@ -512,12 +508,8 @@ fn print_summary( )?; } - let mut error_dist: BTreeMap = Default::default(); - for e in res.iter().filter_map(|r| r.as_ref().err()) { - *error_dist.entry(e.to_string()).or_default() += 1; - } - - let mut error_v: Vec<(String, usize)> = error_dist.into_iter().collect(); + let mut error_v: Vec<(String, usize)> = + res.error.iter().map(|(k, v)| (k.clone(), *v)).collect(); error_v.sort_by_key(|t| std::cmp::Reverse(t.1)); if !error_v.is_empty() { @@ -617,38 +609,40 @@ fn percentiles(values: &mut [f64]) -> BTreeMap { .collect() } -fn calculate_success_rate(res: &[Result]) -> f64 { +fn calculate_success_rate(res: &ResultData) -> f64 { + let dead_line = ClientError::Deadline.to_string(); // We ignore deadline errors which are because of `-z` option, not because of the server - let iter = res - .iter() - .filter(|r| !matches!(r, Err(ClientError::Deadline))); - - let denominator = iter.clone().count(); - let numerator = iter.filter(|r| r.is_ok()).count(); + let denominator = res.success.len() + + res + .error + .iter() + .filter_map(|(k, v)| if k == &dead_line { None } else { Some(v) }) + .sum::(); + let numerator = res.success.len(); numerator as f64 / denominator as f64 } -fn calculate_slowest_request(res: &[Result]) -> f64 { - res.iter() - .filter_map(|r| r.as_ref().ok()) +fn calculate_slowest_request(res: &ResultData) -> f64 { + res.success + .iter() .map(|r| r.duration().as_secs_f64()) .collect::() .max() } -fn calculate_fastest_request(res: &[Result]) -> f64 { - res.iter() - .filter_map(|r| r.as_ref().ok()) +fn calculate_fastest_request(res: &ResultData) -> f64 { + res.success + .iter() .map(|r| r.duration().as_secs_f64()) .collect::() .min() } -fn calculate_average_request(res: &[Result]) -> f64 { +fn calculate_average_request(res: &ResultData) -> f64 { let mean = res + .success .iter() - .filter_map(|r| r.as_ref().ok()) .map(|r| r.duration().as_secs_f64()) .collect::(); if mean.is_empty() { @@ -658,41 +652,33 @@ fn calculate_average_request(res: &[Result]) -> f64 { } } -fn calculate_requests_per_sec( - res: &[Result], - total_duration: Duration, -) -> f64 { +fn calculate_requests_per_sec(res: &ResultData, total_duration: Duration) -> f64 { res.len() as f64 / total_duration.as_secs_f64() } -fn calculate_total_data(res: &[Result]) -> u64 { - res.iter() - .filter_map(|r| r.as_ref().ok()) - .map(|r| r.len_bytes as u64) - .sum::() +fn calculate_total_data(res: &ResultData) -> u64 { + res.success.iter().map(|r| r.len_bytes as u64).sum::() } -fn calculate_size_per_request(res: &[Result]) -> Option { - res.iter() - .filter_map(|r| r.as_ref().ok()) +fn calculate_size_per_request(res: &ResultData) -> Option { + res.success + .iter() .map(|r| r.len_bytes as u64) .sum::() - .checked_div(res.iter().filter(|r| r.is_ok()).count() as u64) + .checked_div(res.success.len() as u64) } -fn calculate_size_per_sec(res: &[Result], total_duration: Duration) -> f64 { - res.iter() - .filter_map(|r| r.as_ref().ok()) +fn calculate_size_per_sec(res: &ResultData, total_duration: Duration) -> f64 { + res.success + .iter() .map(|r| r.len_bytes as u128) .sum::() as f64 / total_duration.as_secs_f64() } -fn calculate_connection_times_base( - res: &[Result], -) -> Vec<(Instant, ConnectionTime)> { - res.iter() - .filter_map(|r| r.as_ref().ok()) +fn calculate_connection_times_base(res: &ResultData) -> Vec<(Instant, ConnectionTime)> { + res.success + .iter() .filter_map(|r| r.connection_time.map(|c| (r.start, c))) .collect() } @@ -757,24 +743,24 @@ fn calculate_connection_times_dns_lookup_slowest( .max() } -fn get_durations_all(res: &[Result]) -> Vec { - res.iter() - .filter_map(|r: &Result| r.as_ref().ok()) +fn get_durations_all(res: &ResultData) -> Vec { + res.success + .iter() .map(|r| r.duration().as_secs_f64()) .collect::>() } -fn get_durations_successful(res: &[Result]) -> Vec { - res.iter() - .filter_map(|r: &Result| r.as_ref().ok()) +fn get_durations_successful(res: &ResultData) -> Vec { + res.success + .iter() .filter(|r| r.status.is_success()) .map(|r| r.duration().as_secs_f64()) .collect::>() } -fn get_durations_not_successful(res: &[Result]) -> Vec { - res.iter() - .filter_map(|r: &Result| r.as_ref().ok()) +fn get_durations_not_successful(res: &ResultData) -> Vec { + res.success + .iter() .filter(|r| r.status.is_client_error() || r.status.is_server_error()) .map(|r| r.duration().as_secs_f64()) .collect::>() @@ -813,12 +799,31 @@ mod tests { }) } - fn build_mock_request_result_vec() -> Vec> { - vec![ - build_mock_request_result(StatusCode::OK, 1000, 200, 50, 100), - build_mock_request_result(StatusCode::BAD_REQUEST, 100000, 250, 100, 200), - build_mock_request_result(StatusCode::INTERNAL_SERVER_ERROR, 1000000, 300, 150, 300), - ] + fn build_mock_request_results() -> ResultData { + let mut results = ResultData::default(); + + results.push(build_mock_request_result( + StatusCode::OK, + 1000, + 200, + 50, + 100, + )); + results.push(build_mock_request_result( + StatusCode::BAD_REQUEST, + 100000, + 250, + 100, + 200, + )); + results.push(build_mock_request_result( + StatusCode::INTERNAL_SERVER_ERROR, + 1000000, + 300, + 150, + 300, + )); + results } fn fp_round(value: f64, places: f64) -> f64 { @@ -848,7 +853,7 @@ mod tests { #[test] fn test_calculate_success_rate() { - let res = build_mock_request_result_vec(); + let res = build_mock_request_results(); assert_eq!(calculate_success_rate(&res), 1.0); } @@ -857,7 +862,7 @@ mod tests { assert_eq!( // Round the calculation to 4 decimal places to remove imprecision fp_round( - calculate_slowest_request(&build_mock_request_result_vec()), + calculate_slowest_request(&build_mock_request_results()), 4.0 ), 1000_f64 @@ -869,7 +874,7 @@ mod tests { assert_eq!( // Round the calculation to 4 decimal places to remove imprecision fp_round( - calculate_fastest_request(&build_mock_request_result_vec()), + calculate_fastest_request(&build_mock_request_results()), 4.0 ), 1_f64 @@ -881,7 +886,7 @@ mod tests { assert_eq!( // Round the calculation to 4 decimal places to remove imprecision fp_round( - calculate_average_request(&build_mock_request_result_vec()), + calculate_average_request(&build_mock_request_results()), 4.0 ), 367_f64 @@ -891,20 +896,20 @@ mod tests { #[test] fn test_calculate_requests_per_sec() { assert_eq!( - calculate_requests_per_sec(&build_mock_request_result_vec(), Duration::from_secs(1)), + calculate_requests_per_sec(&build_mock_request_results(), Duration::from_secs(1)), 3.0 ); } #[test] fn test_calculate_total_data() { - assert_eq!(calculate_total_data(&build_mock_request_result_vec()), 600); + assert_eq!(calculate_total_data(&build_mock_request_results()), 600); } #[test] fn test_calculate_size_per_request() { assert_eq!( - calculate_size_per_request(&build_mock_request_result_vec()).unwrap(), + calculate_size_per_request(&build_mock_request_results()).unwrap(), 200 ); } @@ -912,7 +917,7 @@ mod tests { #[test] fn test_calculate_size_per_sec() { assert_eq!( - (calculate_size_per_sec(&build_mock_request_result_vec(), Duration::from_secs(1))), + (calculate_size_per_sec(&build_mock_request_results(), Duration::from_secs(1))), 600.0 ); } @@ -920,7 +925,7 @@ mod tests { #[test] fn test_calcuate_connection_times_base() { assert_eq!( - calculate_connection_times_base(&build_mock_request_result_vec()).len(), + calculate_connection_times_base(&build_mock_request_results()).len(), 3 ); } @@ -931,7 +936,7 @@ mod tests { // Round the calculation to 4 decimal places to remove imprecision fp_round( calculate_connection_times_dns_dialup_average(&calculate_connection_times_base( - &build_mock_request_result_vec() + &build_mock_request_results() )), 4.0 ), @@ -945,7 +950,7 @@ mod tests { // Round the calculation to 4 decimal places to remove imprecision fp_round( calculate_connection_times_dns_dialup_fastest(&calculate_connection_times_base( - &build_mock_request_result_vec() + &build_mock_request_results() )), 4.0 ), @@ -959,7 +964,7 @@ mod tests { // Round the calculation to 4 decimal places to remove imprecision fp_round( calculate_connection_times_dns_dialup_slowest(&calculate_connection_times_base( - &build_mock_request_result_vec() + &build_mock_request_results() )), 4.0 ), @@ -973,7 +978,7 @@ mod tests { // Round the calculation to 4 decimal places to remove imprecision fp_round( calculate_connection_times_dns_lookup_average(&calculate_connection_times_base( - &build_mock_request_result_vec() + &build_mock_request_results() )), 4.0 ), @@ -987,7 +992,7 @@ mod tests { // Round the calculation to 4 decimal places to remove imprecision fp_round( calculate_connection_times_dns_lookup_fastest(&calculate_connection_times_base( - &build_mock_request_result_vec() + &build_mock_request_results() )), 4.0 ), @@ -1001,7 +1006,7 @@ mod tests { // Round the calculation to 4 decimal places to remove imprecision fp_round( calculate_connection_times_dns_lookup_slowest(&calculate_connection_times_base( - &build_mock_request_result_vec() + &build_mock_request_results() )), 4.0 ), @@ -1011,7 +1016,7 @@ mod tests { #[test] fn test_get_durations_all() { - let durations = get_durations_all(&build_mock_request_result_vec()); + let durations = get_durations_all(&build_mock_request_results()); // Round the calculations to 4 decimal places to remove imprecision assert_eq!(fp_round(durations[0], 4.0), 1.0); assert_eq!(fp_round(durations[1], 4.0), 100.0); @@ -1020,7 +1025,7 @@ mod tests { #[test] fn test_get_durations_successful() { - let durations = get_durations_successful(&build_mock_request_result_vec()); + let durations = get_durations_successful(&build_mock_request_results()); // Round the calculations to 4 decimal places to remove imprecision assert_eq!(fp_round(durations[0], 4.0), 1.0); assert_eq!(durations.get(1), None); @@ -1028,7 +1033,7 @@ mod tests { #[test] fn test_get_durations_not_successful() { - let durations = get_durations_not_successful(&build_mock_request_result_vec()); + let durations = get_durations_not_successful(&build_mock_request_results()); // Round the calculations to 4 decimal places to remove imprecision assert_eq!(fp_round(durations[0], 4.0), 100.0); assert_eq!(fp_round(durations[1], 4.0), 1000.0); diff --git a/src/result_data.rs b/src/result_data.rs new file mode 100644 index 00000000..13217b20 --- /dev/null +++ b/src/result_data.rs @@ -0,0 +1,28 @@ +use std::collections::BTreeMap; + +use crate::client::{ClientError, RequestResult}; + +/// Data container for the results of the all requests +/// When a request is successful, the result is pushed to the `success` vector and the memory consumption will not be a problem because the number of successful requests is limited by network overhead. +/// When a request fails, the error message is pushed to the `error` map because the number of error messages may huge. +#[derive(Debug, Default)] +pub struct ResultData { + pub success: Vec, + pub error: BTreeMap, +} + +impl ResultData { + pub fn push(&mut self, result: Result) { + match result { + Ok(result) => self.success.push(result), + Err(err) => { + let count = self.error.entry(err.to_string()).or_insert(0); + *count += 1; + } + } + } + + pub fn len(&self) -> usize { + self.success.len() + self.error.values().sum::() + } +}