From c13085e381b71177f4b1a05be7c628a04e3b6991 Mon Sep 17 00:00:00 2001 From: Hayden Stainsby Date: Thu, 9 Mar 2023 00:01:50 +0100 Subject: [PATCH] feat(console): reduce decimal digits in UI (#402) The durations in the tokio-console UI are shown with a unit, so the digits after the decimal separator are generally not relevant. Since space is at a premium in a terminal UI, it makes sense to cut down where possible. This change removes all digits after the decimal separator in the tasks table view. In the task detail view, 2 decimal places are kept. Additionally, 4 new duration formats are added to represent minutes-with-secondsi, hours-with-minutes, days-with-hours and days. These are only applied once the leading unit is greater than zero. For example, 59 seconds will be shown as seconds: `99s` and then 60 seconds will be shown as minutes-with-seconds: `1m00s`. New colors have been chosen for each format. NOTE: I had to make a small unrelated change in `task::Details::make_percentiles_widget` to make clippy happy. Fixes: #224 --- tokio-console/src/view/async_ops.rs | 9 +- tokio-console/src/view/mod.rs | 5 +- tokio-console/src/view/resources.rs | 11 +-- tokio-console/src/view/styles.rs | 141 +++++++++++++++++++++++----- tokio-console/src/view/task.rs | 31 +++--- tokio-console/src/view/tasks.rs | 9 +- 6 files changed, 143 insertions(+), 63 deletions(-) diff --git a/tokio-console/src/view/async_ops.rs b/tokio-console/src/view/async_ops.rs index 7e804cfeb..0cdf02162 100644 --- a/tokio-console/src/view/async_ops.rs +++ b/tokio-console/src/view/async_ops.rs @@ -7,7 +7,7 @@ use crate::{ view::{ self, bold, table::{self, TableList, TableListState}, - DUR_LEN, DUR_PRECISION, + DUR_LEN, DUR_TABLE_PRECISION, }, }; @@ -106,12 +106,7 @@ impl TableList<9> for AsyncOpsTable { let mut polls_width = view::Width::new(Self::WIDTHS[7] as u16); let dur_cell = |dur: std::time::Duration| -> Cell<'static> { - Cell::from(styles.time_units(format!( - "{:>width$.prec$?}", - dur, - width = DUR_LEN, - prec = DUR_PRECISION, - ))) + Cell::from(styles.time_units(dur, DUR_TABLE_PRECISION, Some(DUR_LEN))) }; let rows = { diff --git a/tokio-console/src/view/mod.rs b/tokio-console/src/view/mod.rs index 8f1d5429d..9f50d5da7 100644 --- a/tokio-console/src/view/mod.rs +++ b/tokio-console/src/view/mod.rs @@ -18,11 +18,12 @@ mod tasks; pub(crate) use self::styles::{Palette, Styles}; pub(crate) use self::table::SortBy; -const DUR_LEN: usize = 10; +const DUR_LEN: usize = 6; // This data is only updated every second, so it doesn't make a ton of // sense to have a lot of precision in timestamps (and this makes sure // there's room for the unit!) -const DUR_PRECISION: usize = 4; +const DUR_LIST_PRECISION: usize = 2; +const DUR_TABLE_PRECISION: usize = 0; const TABLE_HIGHLIGHT_SYMBOL: &str = ">> "; pub struct View { diff --git a/tokio-console/src/view/resources.rs b/tokio-console/src/view/resources.rs index 4b65b00e8..afe9fc1f6 100644 --- a/tokio-console/src/view/resources.rs +++ b/tokio-console/src/view/resources.rs @@ -6,7 +6,7 @@ use crate::{ view::{ self, bold, table::{self, TableList, TableListState}, - DUR_LEN, DUR_PRECISION, + DUR_LEN, DUR_TABLE_PRECISION, }, }; @@ -104,12 +104,11 @@ impl TableList<9> for ResourcesTable { ))), Cell::from(parent_width.update_str(resource.parent_id()).to_owned()), Cell::from(kind_width.update_str(resource.kind()).to_owned()), - Cell::from(styles.time_units(format!( - "{:>width$.prec$?}", + Cell::from(styles.time_units( resource.total(now), - width = DUR_LEN, - prec = DUR_PRECISION, - ))), + DUR_TABLE_PRECISION, + Some(DUR_LEN), + )), Cell::from(target_width.update_str(resource.target()).to_owned()), Cell::from(type_width.update_str(resource.concrete_type()).to_owned()), Cell::from(resource.type_visibility().render(styles)), diff --git a/tokio-console/src/view/styles.rs b/tokio-console/src/view/styles.rs index b71587c1d..7834cca2b 100644 --- a/tokio-console/src/view/styles.rs +++ b/tokio-console/src/view/styles.rs @@ -1,6 +1,6 @@ use crate::config; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, str::FromStr}; +use std::{str::FromStr, time::Duration}; use tui::{ style::{Color, Modifier, Style}, text::Span, @@ -32,11 +32,41 @@ pub enum Palette { All, } +/// Represents formatted time spans. +/// +/// Distinguishing between different units allows appropriate colouring. +enum FormattedDuration { + /// Days (and no minor unit), e.g. `102d` + Days(String), + /// Days with hours, e.g. `12d03h` + DaysHours(String), + /// Hours with minutes, e.g. `14h32m` + HoursMinutes(String), + /// Minutes with seconds, e.g. `43m02s` + MinutesSeconds(String), + /// The `time::Duration` debug string which uses units ranging from + /// picoseconds (`ps`) to seconds (`s`). May contain decimal digits + /// (e.g. `628.76ms`) or not (e.g. `32ns`) + Debug(String), +} + +impl FormattedDuration { + fn into_inner(self) -> String { + match self { + Self::Days(inner) => inner, + Self::DaysHours(inner) => inner, + Self::HoursMinutes(inner) => inner, + Self::MinutesSeconds(inner) => inner, + Self::Debug(inner) => inner, + } + } +} + fn fg_style(color: Color) -> Style { Style::default().fg(color) } -// === impl Config === +// === impl Styles === impl Styles { pub fn from_config(config: config::ViewOptions) -> Self { @@ -126,39 +156,100 @@ impl Styles { } } - pub fn time_units<'a>(&self, text: impl Into>) -> Span<'a> { - let mut text = text.into(); - if !self.toggles.color_durations() { - return Span::raw(text); - } + /// Creates a span with a formatted duration inside. + /// + /// The formatted duration will be colored depending on the palette + /// defined for this `Styles` object. + /// + /// If the `width` parameter is `None` then no padding will be + /// added. Otherwise the text in the span will be left-padded to + /// the specified width (right aligned). Passing `Some(0)` is + /// equivalent to `None`. + pub fn time_units<'a>(&self, dur: Duration, prec: usize, width: Option) -> Span<'a> { + let formatted = self.duration_text(dur, width.unwrap_or(0), prec); - if !self.utf8 { - if let Some(mu_offset) = text.find("µs") { - text.to_mut().replace_range(mu_offset.., "us"); - } + if !self.toggles.color_durations() { + return Span::raw(formatted.into_inner()); } let style = match self.palette { - Palette::NoColors => return Span::raw(text), - Palette::Ansi8 | Palette::Ansi16 => match text.as_ref() { - s if s.ends_with("ps") => fg_style(Color::Blue), - s if s.ends_with("ns") => fg_style(Color::Green), - s if s.ends_with("µs") || s.ends_with("us") => fg_style(Color::Yellow), - s if s.ends_with("ms") => fg_style(Color::Red), - s if s.ends_with('s') => fg_style(Color::Magenta), + Palette::NoColors => return Span::raw(formatted.into_inner()), + Palette::Ansi8 | Palette::Ansi16 => match &formatted { + FormattedDuration::Days(_) => fg_style(Color::Blue), + FormattedDuration::DaysHours(_) => fg_style(Color::Blue), + FormattedDuration::HoursMinutes(_) => fg_style(Color::Cyan), + FormattedDuration::MinutesSeconds(_) => fg_style(Color::Green), + FormattedDuration::Debug(s) if s.ends_with("ps") => fg_style(Color::Gray), + FormattedDuration::Debug(s) if s.ends_with("ns") => fg_style(Color::Gray), + FormattedDuration::Debug(s) if s.ends_with("µs") || s.ends_with("us") => { + fg_style(Color::Magenta) + } + FormattedDuration::Debug(s) if s.ends_with("ms") => fg_style(Color::Red), + FormattedDuration::Debug(s) if s.ends_with('s') => fg_style(Color::Yellow), _ => Style::default(), }, - Palette::Ansi256 | Palette::All => match text.as_ref() { - s if s.ends_with("ps") => fg_style(Color::Indexed(40)), // green 3 - s if s.ends_with("ns") => fg_style(Color::Indexed(41)), // spring green 3 - s if s.ends_with("µs") || s.ends_with("us") => fg_style(Color::Indexed(42)), // spring green 2 - s if s.ends_with("ms") => fg_style(Color::Indexed(43)), // cyan 3 - s if s.ends_with('s') => fg_style(Color::Indexed(44)), // dark turquoise, + Palette::Ansi256 | Palette::All => match &formatted { + FormattedDuration::Days(_) => fg_style(Color::Indexed(33)), // dodger blue 1 + FormattedDuration::DaysHours(_) => fg_style(Color::Indexed(33)), // dodger blue 1 + FormattedDuration::HoursMinutes(_) => fg_style(Color::Indexed(39)), // deep sky blue 1 + FormattedDuration::MinutesSeconds(_) => fg_style(Color::Indexed(45)), // turquoise 2 + FormattedDuration::Debug(s) if s.ends_with("ps") => fg_style(Color::Indexed(40)), // green 3 + FormattedDuration::Debug(s) if s.ends_with("ns") => fg_style(Color::Indexed(41)), // spring green 3 + FormattedDuration::Debug(s) if s.ends_with("µs") || s.ends_with("us") => { + fg_style(Color::Indexed(42)) + } // spring green 2 + FormattedDuration::Debug(s) if s.ends_with("ms") => fg_style(Color::Indexed(43)), // cyan 3 + FormattedDuration::Debug(s) if s.ends_with('s') => fg_style(Color::Indexed(44)), // dark turquoise, _ => Style::default(), }, }; - Span::styled(text, style) + Span::styled(formatted.into_inner(), style) + } + + fn duration_text(&self, dur: Duration, width: usize, prec: usize) -> FormattedDuration { + let secs = dur.as_secs(); + + if secs >= 60 * 60 * 24 * 100 { + let days = secs / (60 * 60 * 24); + FormattedDuration::Days(format!("{days:>width$}d", days = days, width = width)) + } else if secs >= 60 * 60 * 24 { + let hours = secs / (60 * 60); + FormattedDuration::DaysHours(format!( + "{days:>leading_width$}d{hours:02.0}h", + days = hours / 24, + hours = hours % 24, + // Subtract the known 4 characters that trail the days value. + leading_width = width.saturating_sub(4), + )) + } else if secs >= 60 * 60 { + let mins = secs / 60; + FormattedDuration::HoursMinutes(format!( + "{hours:>leading_width$}h{minutes:02.0}m", + hours = mins / 60, + minutes = mins % 60, + // Subtract the known 4 characters that trail the hours value. + leading_width = width.saturating_sub(4), + )) + } else if secs >= 60 { + FormattedDuration::MinutesSeconds(format!( + "{minutes:>leading_width$}m{seconds:02.0}s", + minutes = secs / 60, + seconds = secs % 60, + // Subtract the known 4 characters that trail the minutes value. + leading_width = width.saturating_sub(4), + )) + } else { + let mut text = format!("{:>width$.prec$?}", dur, width = width, prec = prec); + + if !self.utf8 { + if let Some(mu_offset) = text.find("µs") { + text.replace_range(mu_offset.., "us"); + } + } + + FormattedDuration::Debug(text) + } } pub fn terminated(&self) -> Style { diff --git a/tokio-console/src/view/task.rs b/tokio-console/src/view/task.rs index 2708bfb92..b8febbcab 100644 --- a/tokio-console/src/view/task.rs +++ b/tokio-console/src/view/task.rs @@ -9,6 +9,7 @@ use crate::{ view::{ self, bold, mini_histogram::{HistogramMetadata, MiniHistogram}, + DUR_LIST_PRECISION, }, }; use std::{ @@ -329,29 +330,27 @@ impl Details { fn make_percentiles_widget(&self, styles: &view::Styles) -> Text<'static> { let mut text = Text::default(); let histogram = self.poll_times_histogram(); - let percentiles = - histogram - .iter() - .flat_map(|&DurationHistogram { ref histogram, .. }| { - let pairs = [10f64, 25f64, 50f64, 75f64, 90f64, 95f64, 99f64] - .iter() - .map(move |i| (*i, histogram.value_at_percentile(*i))); - pairs.map(|pair| { - Spans::from(vec![ - bold(format!("p{:>2}: ", pair.0)), - dur(styles, Duration::from_nanos(pair.1)), - ]) - }) - }); + let percentiles = histogram + .iter() + .flat_map(|&DurationHistogram { histogram, .. }| { + let pairs = [10f64, 25f64, 50f64, 75f64, 90f64, 95f64, 99f64] + .iter() + .map(move |i| (*i, histogram.value_at_percentile(*i))); + pairs.map(|pair| { + Spans::from(vec![ + bold(format!("p{:>2}: ", pair.0)), + dur(styles, Duration::from_nanos(pair.1)), + ]) + }) + }); text.extend(percentiles); text } } fn dur(styles: &view::Styles, dur: std::time::Duration) -> Span<'static> { - const DUR_PRECISION: usize = 4; // TODO(eliza): can we not have to use `format!` to make a string here? is // there a way to just give TUI a `fmt::Debug` implementation, or does it // have to be given a string in order to do layout stuff? - styles.time_units(format!("{:.prec$?}", dur, prec = DUR_PRECISION)) + styles.time_units(dur, DUR_LIST_PRECISION, None) } diff --git a/tokio-console/src/view/tasks.rs b/tokio-console/src/view/tasks.rs index eb6b2365a..780f4775c 100644 --- a/tokio-console/src/view/tasks.rs +++ b/tokio-console/src/view/tasks.rs @@ -6,7 +6,7 @@ use crate::{ view::{ self, bold, table::{self, TableList, TableListState}, - DUR_LEN, DUR_PRECISION, + DUR_LEN, DUR_TABLE_PRECISION, }, }; use tui::{ @@ -68,12 +68,7 @@ impl TableList<11> for TasksTable { .sort(now, &mut table_list_state.sorted_items); let dur_cell = |dur: std::time::Duration| -> Cell<'static> { - Cell::from(styles.time_units(format!( - "{:>width$.prec$?}", - dur, - width = DUR_LEN, - prec = DUR_PRECISION, - ))) + Cell::from(styles.time_units(dur, DUR_TABLE_PRECISION, Some(DUR_LEN))) }; // Start out wide enough to display the column headers...