feature: Adaptive network widget (#206)

Allows the network widget graph to grow/shrink with current data, rather than using a static size.
This commit is contained in:
Clement Tsang 2020-08-28 16:30:24 -04:00 committed by GitHub
parent 81ec7c311b
commit 9a11e77aa0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 189 additions and 34 deletions

View file

@ -5,6 +5,12 @@
"EINVAL",
"EPERM",
"ESRCH",
"GIBI",
"GIBIBYTE",
"GIGA",
"KIBI",
"MEBI",
"MEBIBYTE",
"MSRV",
"Mahmoud",
"Marcin",
@ -12,6 +18,8 @@
"PKGBUILD",
"Qudsi",
"SIGTERM",
"TEBI",
"TERA",
"Tebibytes",
"Toolset",
"Ungrouped",

View file

@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.5.0] - Unreleased
### Features
- [#206](https://github.com/ClementTsang/bottom/pull/206): Adaptive network graphs --- prior to this update, graphs were stuck at a range from 0B to 1GiB. Now, they adjust to your current usage and time span, so if you're using, say, less than a MiB, it will cap at a MiB. If you're using 10GiB, then the graph will reflect that and span to a bit greater than 10GiB.
### Changes
### Bug Fixes
## [0.4.7] - 2020-08-26
### Bug Fixes

View file

@ -42,7 +42,6 @@ unicode-segmentation = "1.6.0"
unicode-width = "0.1"
libc = "0.2"
ctrlc = {version = "3.1", features = ["termination"]}
# tui = {version = "0.10.0", features = ["crossterm"], default-features = false, git = "https://github.com/fdehau/tui-rs.git"}
tui = {version = "0.9.5", features = ["crossterm"], default-features = false }
# For debugging only...

View file

@ -335,7 +335,11 @@ Note that the `and` operator takes precedence over the `or` operator.
As yet _another_ process/system visualization and management application, bottom supports the typical features:
- CPU, memory, and network usage visualization
- CPU usage visualization, on an average and per-core basis
- RAM and swap usage visualization
- Network visualization for receiving and transmitting, on a log-graph scale
- Display information about disk capacity and I/O per second
@ -527,7 +531,7 @@ Each component of the layout accepts a `ratio` value. If this is not set, it def
For an example, look at the [default config](./sample_configs/default_config.toml), which contains the default layout.
And if your heart desires, you can have duplicate widgets. This means you could do something like:
Furthermore, you can have duplicate widgets. This means you could do something like:
```toml
[[row]]

View file

@ -945,9 +945,6 @@ impl App {
}
self.handle_char(caught_char);
} else if self.help_dialog_state.is_showing_help {
// TODO: Seems weird that we have it like this; it would be better to make this
// more obvious that we are separating dialog logic and normal logic IMO.
// This is even more so as most logic already checks for dialog state.
match caught_char {
'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
let potential_index = caught_char.to_digit(10);

View file

@ -185,21 +185,20 @@ impl DataCollection {
}
fn eat_network(&mut self, network: &network::NetworkHarvest, new_entry: &mut TimedData) {
// FIXME [NETWORKING]: Support bits, support switching between decimal and binary units (move the log part to conversion and switch on the fly)
// RX
let logged_rx_val = if network.rx as f64 > 0.0 {
(network.rx as f64).log(2.0)
new_entry.rx_data = if network.rx > 0 {
(network.rx as f64).log2()
} else {
0.0
};
new_entry.rx_data = logged_rx_val;
// TX
let logged_tx_val = if network.tx as f64 > 0.0 {
(network.tx as f64).log(2.0)
new_entry.tx_data = if network.tx > 0 {
(network.tx as f64).log2()
} else {
0.0
};
new_entry.tx_data = logged_tx_val;
// In addition copy over latest data for easy reference
self.network_harvest = network.clone();

View file

@ -301,6 +301,7 @@ impl ProcColumn {
.sum()
}
/// ALWAYS call this when opening the sorted window.
pub fn set_to_sorted_index(&mut self, proc_sorting_type: &ProcessSorting) {
// TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT!
let mut true_index = 0;

View file

@ -28,14 +28,17 @@ mod dialogs;
mod drawing_utils;
mod widgets;
/// Point is of time, data
type Point = (f64, f64);
#[derive(Default)]
pub struct DisplayableData {
pub rx_display: String,
pub tx_display: String,
pub total_rx_display: String,
pub total_tx_display: String,
pub network_data_rx: Vec<(f64, f64)>,
pub network_data_tx: Vec<(f64, f64)>,
pub network_data_rx: Vec<Point>,
pub network_data_tx: Vec<Point>,
pub disk_data: Vec<Vec<String>>,
pub temp_sensor_data: Vec<Vec<String>>,
pub single_process_data: Vec<ConvertedProcessData>, // Contains single process data
@ -45,8 +48,8 @@ pub struct DisplayableData {
pub swap_label_percent: String,
pub mem_label_frac: String,
pub swap_label_frac: String,
pub mem_data: Vec<(f64, f64)>,
pub swap_data: Vec<(f64, f64)>,
pub mem_data: Vec<Point>,
pub swap_data: Vec<Point>,
pub cpu_data: Vec<ConvertedCpuData>,
pub battery_data: Vec<ConvertedBatteryData>,
}

View file

@ -144,10 +144,9 @@ impl CpuGraphWidget for Painter {
.labels_style(self.colours.graph_style)
};
// Note this is offset as otherwise the 0 value is not drawn!
let y_axis = Axis::default()
.style(self.colours.graph_style)
.bounds([-0.5, 100.5])
.bounds([0.0, 100.5])
.labels_style(self.colours.graph_style)
.labels(&["0%", "100%"]);

View file

@ -54,10 +54,9 @@ impl MemGraphWidget for Painter {
.labels_style(self.colours.graph_style)
};
// Offset as the zero value isn't drawn otherwise...
let y_axis = Axis::default()
.style(self.colours.graph_style)
.bounds([-0.5, 100.5])
.bounds([0.0, 100.5])
.labels(&["0%", "100%"])
.labels_style(self.colours.graph_style);

View file

@ -5,6 +5,7 @@ use crate::{
app::App,
canvas::{drawing_utils::get_variable_intrinsic_widths, Painter},
constants::*,
utils::gen_util::*,
};
use tui::{
@ -67,10 +68,109 @@ impl NetworkGraphWidget for Painter {
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
hide_legend: bool,
) {
/// Point is of time, data
type Point = (f64, f64);
/// Returns the required max data point and labels.
fn adjust_network_data_point(
rx: &[Point], tx: &[Point], time_start: f64, time_end: f64,
) -> (f64, Vec<String>) {
// First, filter and find the maximal rx or tx so we know how to scale
let mut max_val_bytes = 0.0;
let filtered_rx = rx
.iter()
.cloned()
.filter(|(time, _data)| *time >= time_start && *time <= time_end);
let filtered_tx = tx
.iter()
.cloned()
.filter(|(time, _data)| *time >= time_start && *time <= time_end);
for (_time, data) in filtered_rx.clone().chain(filtered_tx.clone()) {
if data > max_val_bytes {
max_val_bytes = data;
}
}
// Main idea is that we have some "limits" --- if we're, say, under a logged kibibyte,
// then we are just gonna set the cap at a kibibyte.
// For gibi/giga and beyond, we instead start going up by 1 rather than jumping to a tera/tebi.
// So, it would look a bit like:
// - < Kibi => Kibi => Mebi => Gibi => 2 Gibi => ... => 999 Gibi => Tebi => 2 Tebi => ...
let true_max_val: f64;
let mut labels = vec![];
if max_val_bytes < LOG_KIBI_LIMIT {
true_max_val = LOG_KIBI_LIMIT;
labels = vec!["0B".to_string(), "1KiB".to_string()];
} else if max_val_bytes < LOG_MEBI_LIMIT {
true_max_val = LOG_MEBI_LIMIT;
labels = vec!["0B".to_string(), "1KiB".to_string(), "1MiB".to_string()];
} else if max_val_bytes < LOG_GIBI_LIMIT {
true_max_val = LOG_GIBI_LIMIT;
labels = vec![
"0B".to_string(),
"1KiB".to_string(),
"1MiB".to_string(),
"1GiB".to_string(),
];
} else if max_val_bytes < LOG_TEBI_LIMIT {
true_max_val = max_val_bytes.ceil() + 1.0;
let cap_u32 = true_max_val as u32;
for i in 0..=cap_u32 {
match i {
0 => labels.push("0B".to_string()),
LOG_KIBI_LIMIT_U32 => labels.push("1KiB".to_string()),
LOG_MEBI_LIMIT_U32 => labels.push("1MiB".to_string()),
LOG_GIBI_LIMIT_U32 => labels.push("1GiB".to_string()),
_ if i == cap_u32 => {
labels.push(format!("{}GiB", 2_u64.pow(cap_u32 - LOG_GIBI_LIMIT_U32)))
}
_ if i == (LOG_GIBI_LIMIT_U32 + cap_u32) / 2 => labels.push(format!(
"{}GiB",
2_u64.pow(cap_u32 - ((LOG_GIBI_LIMIT_U32 + cap_u32) / 2))
)), // ~Halfway point
_ => labels.push(String::default()),
}
}
} else {
true_max_val = max_val_bytes.ceil() + 1.0;
let cap_u32 = true_max_val as u32;
for i in 0..=cap_u32 {
match i {
0 => labels.push("0B".to_string()),
LOG_KIBI_LIMIT_U32 => labels.push("1KiB".to_string()),
LOG_MEBI_LIMIT_U32 => labels.push("1MiB".to_string()),
LOG_GIBI_LIMIT_U32 => labels.push("1GiB".to_string()),
LOG_TEBI_LIMIT_U32 => labels.push("1TiB".to_string()),
_ if i == cap_u32 => {
labels.push(format!("{}GiB", 2_u64.pow(cap_u32 - LOG_TEBI_LIMIT_U32)))
}
_ if i == (LOG_TEBI_LIMIT_U32 + cap_u32) / 2 => labels.push(format!(
"{}TiB",
2_u64.pow(cap_u32 - ((LOG_TEBI_LIMIT_U32 + cap_u32) / 2))
)), // ~Halfway point
_ => labels.push(String::default()),
}
}
}
(true_max_val, labels)
}
if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) {
let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx;
let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx;
let (max_range, labels) = adjust_network_data_point(
network_data_rx,
network_data_tx,
-(network_widget_state.current_display_time as f64),
0.0,
);
let display_time_labels = [
format!("{}s", network_widget_state.current_display_time / 1000),
"0s".to_string(),
@ -104,11 +204,10 @@ impl NetworkGraphWidget for Painter {
.labels_style(self.colours.graph_style)
};
// 0 is offset.
let y_axis_labels = ["0B", "1KiB", "1MiB", "1GiB"];
let y_axis_labels = labels;
let y_axis = Axis::default()
.style(self.colours.graph_style)
.bounds([-0.5, 30_f64])
.bounds([0.0, max_range])
.labels(&y_axis_labels)
.labels_style(self.colours.graph_style);

View file

@ -5,9 +5,10 @@ use std::collections::HashMap;
use crate::{
app::{data_farmer, data_harvester, App},
utils::gen_util::{get_exact_byte_values, get_simple_byte_values},
utils::gen_util::*,
};
/// Point is of time, data
type Point = (f64, f64);
#[derive(Default, Debug)]
@ -28,6 +29,13 @@ pub struct ConvertedNetworkData {
pub tx_display: String,
pub total_rx_display: Option<String>,
pub total_tx_display: Option<String>,
// TODO: [NETWORKING] add min/max/mean of each
// min_rx : f64,
// max_rx : f64,
// mean_rx: f64,
// min_tx: f64,
// max_tx: f64,
// mean_tx: f64,
}
#[derive(Clone, Default, Debug)]
@ -335,7 +343,7 @@ pub fn convert_network_data_points(
}
} else {
let rx_display = format!(
"RX: {:<9} All: {:<9}",
"RX: {:<9} Total: {:<9}",
format!("{:.1}{:3}", rx_converted_result.0, rx_converted_result.1),
format!(
"{:.1}{:3}",
@ -343,7 +351,7 @@ pub fn convert_network_data_points(
)
);
let tx_display = format!(
"TX: {:<9} All: {:<9}",
"TX: {:<9} Total: {:<9}",
format!("{:.1}{:3}", tx_converted_result.0, tx_converted_result.1),
format!(
"{:.1}{:3}",

View file

@ -420,6 +420,8 @@ pub fn handle_force_redraws(app: &mut App) {
#[allow(clippy::needless_collect)]
pub fn update_all_process_lists(app: &mut App) {
// According to clippy, I can avoid a collect... but if I follow it,
// I end up conflicting with the borrow checker since app is used within the closure... hm.
if !app.is_frozen {
let widget_ids = app
.proc_state

View file

@ -1,5 +1,34 @@
use std::cmp::Ordering;
pub const KILO_LIMIT: u64 = 1000;
pub const MEGA_LIMIT: u64 = 1_000_000;
pub const GIGA_LIMIT: u64 = 1_000_000_000;
pub const TERA_LIMIT: u64 = 1_000_000_000_000;
pub const KIBI_LIMIT: u64 = 1024;
pub const MEBI_LIMIT: u64 = 1_048_576;
pub const GIBI_LIMIT: u64 = 1_073_741_824;
pub const TEBI_LIMIT: u64 = 1_099_511_627_776;
pub const LOG_KILO_LIMIT: f64 = 3.0;
pub const LOG_MEGA_LIMIT: f64 = 6.0;
pub const LOG_GIGA_LIMIT: f64 = 9.0;
pub const LOG_TERA_LIMIT: f64 = 12.0;
pub const LOG_KIBI_LIMIT: f64 = 10.0;
pub const LOG_MEBI_LIMIT: f64 = 20.0;
pub const LOG_GIBI_LIMIT: f64 = 30.0;
pub const LOG_TEBI_LIMIT: f64 = 40.0;
pub const LOG_KILO_LIMIT_U32: u32 = 3;
pub const LOG_MEGA_LIMIT_U32: u32 = 6;
pub const LOG_GIGA_LIMIT_U32: u32 = 9;
pub const LOG_TERA_LIMIT_U32: u32 = 12;
pub const LOG_KIBI_LIMIT_U32: u32 = 10;
pub const LOG_MEBI_LIMIT_U32: u32 = 20;
pub const LOG_GIBI_LIMIT_U32: u32 = 30;
pub const LOG_TEBI_LIMIT_U32: u32 = 40;
pub fn float_min(a: f32, b: f32) -> f32 {
match a.partial_cmp(&b) {
Some(x) => match x {
@ -26,7 +55,7 @@ pub fn float_max(a: f32, b: f32) -> f32 {
/// This only supports up to a tebibyte.
pub fn get_exact_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
match bytes {
b if b < 1024 => (
b if b < KIBI_LIMIT => (
bytes as f64,
if spacing {
" B".to_string()
@ -34,9 +63,9 @@ pub fn get_exact_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
"B".to_string()
},
),
b if b < 1_048_576 => (bytes as f64 / 1024.0, "KiB".to_string()),
b if b < 1_073_741_824 => (bytes as f64 / 1_048_576.0, "MiB".to_string()),
b if b < 1_099_511_627_776 => (bytes as f64 / 1_073_741_824.0, "GiB".to_string()),
b if b < MEBI_LIMIT => (bytes as f64 / 1024.0, "KiB".to_string()),
b if b < GIBI_LIMIT => (bytes as f64 / 1_048_576.0, "MiB".to_string()),
b if b < TERA_LIMIT => (bytes as f64 / 1_073_741_824.0, "GiB".to_string()),
_ => (bytes as f64 / 1_099_511_627_776.0, "TiB".to_string()),
}
}
@ -45,7 +74,7 @@ pub fn get_exact_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
/// This only supports up to a terabyte. Note the "byte" unit will have a space appended to match the others.
pub fn get_simple_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
match bytes {
b if b < 1000 => (
b if b < KILO_LIMIT => (
bytes as f64,
if spacing {
" B".to_string()
@ -53,9 +82,9 @@ pub fn get_simple_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
"B".to_string()
},
),
b if b < 1_000_000 => (bytes as f64 / 1000.0, "KB".to_string()),
b if b < 1_000_000_000 => (bytes as f64 / 1_000_000.0, "MB".to_string()),
b if b < 1_000_000_000_000 => (bytes as f64 / 1_000_000_000.0, "GB".to_string()),
b if b < MEGA_LIMIT => (bytes as f64 / 1000.0, "KB".to_string()),
b if b < GIGA_LIMIT => (bytes as f64 / 1_000_000.0, "MB".to_string()),
b if b < TERA_LIMIT => (bytes as f64 / 1_000_000_000.0, "GB".to_string()),
_ => (bytes as f64 / 1_000_000_000_000.0, "TB".to_string()),
}
}