From 42ee4c79afdfb5c96fd58e8b19c12efc1b79fe6c Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:41:07 -0400 Subject: [PATCH] feat: add packet size & packet rate (#1980) * feat: add packet size & packet rate * feat: add packet size & packet rate for network basic panel * update wording * some text shifting fixes * fix most things --------- Co-authored-by: WqyJh <781345688@qq.com> --- src/app.rs | 1 + src/canvas.rs | 20 ++- src/canvas/widgets/mod.rs | 48 +++++ src/canvas/widgets/network_basic.rs | 268 ++++++++++++++++++++++++---- src/canvas/widgets/network_graph.rs | 163 +++++++++++------ src/collection.rs | 8 + src/collection/network.rs | 4 + src/collection/network/sysinfo.rs | 19 +- src/options.rs | 14 ++ src/options/args.rs | 9 + src/options/config/flags.rs | 1 + src/options/config/network.rs | 2 + src/utils/data_units.rs | 6 +- 13 files changed, 468 insertions(+), 95 deletions(-) diff --git a/src/app.rs b/src/app.rs index 242045b3..86103a6b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -68,6 +68,7 @@ pub struct AppConfigFields { pub network_legend_position: Option, pub network_scale_type: AxisScaling, pub network_use_binary_prefix: bool, + pub network_show_packets: bool, pub retention_ms: u64, pub dedicated_average_row: bool, pub default_tree_collapse: bool, diff --git a/src/canvas.rs b/src/canvas.rs index 41dd2490..62964b8e 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -238,13 +238,9 @@ impl Painter { rect[0], app_state.current_widget.widget_id, ), - Net => self.draw_network_graph( - f, - app_state, - rect[0], - app_state.current_widget.widget_id, - false, - ), + Net => { + self.draw_network(f, app_state, rect[0], app_state.current_widget.widget_id) + } Proc | ProcSearch | ProcSort => { let widget_id = app_state.current_widget.widget_id - match &app_state.current_widget.widget_type { @@ -311,8 +307,14 @@ impl Painter { mem_rows += data.gpu_harvest.len() as u16; // add row(s) for gpu } - if mem_rows == 1 { - mem_rows += 1; // need at least 2 rows for RX and TX + let network_rows = if app_state.app_config_fields.network_show_packets { + 4 // 4 rows for RX/TX and Packet Rates (Avg sizes moved to right side) + } else { + 2 // 2 rows for RX and TX + }; + + if mem_rows < network_rows { + mem_rows += network_rows - mem_rows; // min rows } let vertical_chunks = Layout::default() diff --git a/src/canvas/widgets/mod.rs b/src/canvas/widgets/mod.rs index 25ec595b..9b087a00 100644 --- a/src/canvas/widgets/mod.rs +++ b/src/canvas/widgets/mod.rs @@ -1,3 +1,5 @@ +use crate::{collection::network::NetworkHarvest, utils::data_units::convert_bytes}; + pub mod cpu_basic; pub mod cpu_graph; pub mod disk_table; @@ -10,3 +12,49 @@ pub mod temperature_table; #[cfg(feature = "battery")] pub mod battery_display; + +/// Helper struct to hold packet-related data +pub(super) struct PacketInfo { + /// Current received packet rate. + pub(super) rx_packet_rate: u64, + + /// Current transmitted packet rate. + pub(super) tx_packet_rate: u64, + + /// Average received packet size in bytes, converted to the nearest unit. + pub(super) avg_rx_packet_size: (f64, &'static str), + + /// Average transmitted packet size in bytes, converted to the nearest unit. + pub(super) avg_tx_packet_size: (f64, &'static str), +} + +/// Calculate packet information from network data. +pub(super) fn calculate_packet_info( + network_latest_data: &NetworkHarvest, use_binary_prefix: bool, +) -> PacketInfo { + let rx_packet_rate = network_latest_data.rx_packets; + let tx_packet_rate = network_latest_data.tx_packets; + + // Calculate average packet size (bytes per packet) + let avg_rx_packet_size = if network_latest_data.rx_packets > 0 { + (network_latest_data.rx as f64 / 8.0) / network_latest_data.rx_packets as f64 // Convert bits to bytes + } else { + 0.0 + }; + + let avg_tx_packet_size = if network_latest_data.tx_packets > 0 { + (network_latest_data.tx as f64 / 8.0) / network_latest_data.tx_packets as f64 // Convert bits to bytes + } else { + 0.0 + }; + + let avg_rx_packet_size = convert_bytes(avg_rx_packet_size.round() as u64, use_binary_prefix); + let avg_tx_packet_size = convert_bytes(avg_tx_packet_size.round() as u64, use_binary_prefix); + + PacketInfo { + rx_packet_rate, + tx_packet_rate, + avg_rx_packet_size, + avg_tx_packet_size, + } +} diff --git a/src/canvas/widgets/network_basic.rs b/src/canvas/widgets/network_basic.rs index caf82903..b9ca4849 100644 --- a/src/canvas/widgets/network_basic.rs +++ b/src/canvas/widgets/network_basic.rs @@ -7,7 +7,11 @@ use tui::{ use crate::{ app::App, - canvas::{Painter, drawing_utils::widget_block}, + canvas::{ + Painter, + drawing_utils::widget_block, + widgets::{PacketInfo, calculate_packet_info}, + }, utils::data_units::{convert_bits, get_unit_prefix}, }; @@ -15,22 +19,7 @@ impl Painter { pub fn draw_basic_network( &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { - let divided_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(draw_loc); - - let net_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(1) - .split(divided_loc[0]); - - let total_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(100)]) - .horizontal_margin(1) - .split(divided_loc[1]); + let show_packets = app_state.app_config_fields.network_show_packets; if app_state.current_widget.widget_id == widget_id { f.render_widget( @@ -47,27 +36,242 @@ impl Painter { let total_rx = convert_bits(network_data.total_rx, use_binary_prefix); let total_tx = convert_bits(network_data.total_tx, use_binary_prefix); - let rx_label = format!("RX: {:.1}{}", rx.0, rx.1); - let tx_label = format!("TX: {:.1}{}", tx.0, tx.1); + let rx_label = format!("RX: {:.1}{}/s", rx.0, rx.1); + let tx_label = format!("TX: {:.1}{}/s", tx.0, tx.1); let total_rx_label = format!("Total RX: {:.1}{}", total_rx.0, total_rx.1); let total_tx_label = format!("Total TX: {:.1}{}", total_tx.0, total_tx.1); - let net_text = vec![ - Line::from(Span::styled(rx_label, self.styles.rx_style)), - Line::from(Span::styled(tx_label, self.styles.tx_style)), - ]; + // Determine if we need grid layout based on available width + // Assume we need at least ~15 chars per column for horizontal layout + // With 4 columns, that's ~60 chars minimum + // If width is less than 60, use grid layout (4 rows x 2 columns) + if show_packets { + let PacketInfo { + rx_packet_rate, + tx_packet_rate, + avg_rx_packet_size, + avg_tx_packet_size, + } = calculate_packet_info(network_data, use_binary_prefix); - let total_net_text = vec![ - Line::from(Span::styled(total_rx_label, self.styles.total_rx_style)), - Line::from(Span::styled(total_tx_label, self.styles.total_tx_style)), - ]; + // TODO: Stylize packet stuff later with something else? Or maybe make it so total is now (by default) just bolded RX/TX? I doubt anyone cares... + let rx_packet_rate_label = format!("RX Pkt: {}pkt/s", rx_packet_rate); + let tx_packet_rate_label = format!("TX Pkt: {}pkt/s", tx_packet_rate); + let avg_rx_packet_size_label = format!( + "Avg RX Pkt: {:.1}{}", + avg_rx_packet_size.0, avg_rx_packet_size.1 + ); + let avg_tx_packet_size_label = format!( + "Avg TX Pkt: {:.1}{}", + avg_tx_packet_size.0, avg_tx_packet_size.1 + ); - f.render_widget(Paragraph::new(net_text).block(Block::default()), net_loc[0]); + if draw_loc.width < 60 { + // 4 rows x 2 columns layout + // Column 1: RX, TX, Total RX, Total TX (top to bottom) + // Column 2: RX Packets, TX Packets, AVG RX, AVG TX (top to bottom) + let grid_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(draw_loc); - f.render_widget( - Paragraph::new(total_net_text).block(Block::default()), - total_loc[0], - ); + // Column 1: RX, TX, Total RX, Total TX + let col1_loc = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]) + .split(grid_loc[0]); + f.render_widget( + Paragraph::new(Line::from(Span::styled(rx_label, self.styles.rx_style))) + .block(Block::default()), + col1_loc[0], + ); + f.render_widget( + Paragraph::new(Line::from(Span::styled(tx_label, self.styles.tx_style))) + .block(Block::default()), + col1_loc[1], + ); + f.render_widget( + Paragraph::new(Line::from(Span::styled( + total_rx_label, + self.styles.total_rx_style, + ))) + .block(Block::default()), + col1_loc[2], + ); + f.render_widget( + Paragraph::new(Line::from(Span::styled( + total_tx_label, + self.styles.total_tx_style, + ))) + .block(Block::default()), + col1_loc[3], + ); + + // Column 2: RX Packets, TX Packets, AVG RX, AVG TX + let col2_loc = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]) + .split(grid_loc[1]); + f.render_widget( + Paragraph::new(Line::from(Span::styled( + rx_packet_rate_label, + self.styles.rx_style, + ))) + .block(Block::default()), + col2_loc[0], + ); + f.render_widget( + Paragraph::new(Line::from(Span::styled( + tx_packet_rate_label, + self.styles.tx_style, + ))) + .block(Block::default()), + col2_loc[1], + ); + f.render_widget( + Paragraph::new(Line::from(Span::styled( + avg_rx_packet_size_label, + self.styles.total_rx_style, + ))) + .block(Block::default()), + col2_loc[2], + ); + f.render_widget( + Paragraph::new(Line::from(Span::styled( + avg_tx_packet_size_label, + self.styles.total_tx_style, + ))) + .block(Block::default()), + col2_loc[3], + ); + } else { + // Horizontal 4-column layout + let constraints = [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]; + + let divided_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints) + .split(draw_loc); + + // Column 1: RX/TX + let col1_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(divided_loc[0]); + let col1_text = vec![ + Line::from(Span::styled(rx_label, self.styles.rx_style)), + Line::from(Span::styled(tx_label, self.styles.tx_style)), + ]; + f.render_widget( + Paragraph::new(col1_text).block(Block::default()), + col1_loc[0], + ); + + // Column 2: Total RX/TX + let col2_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(divided_loc[1]); + let col2_text = vec![ + Line::from(Span::styled(total_rx_label, self.styles.total_rx_style)), + Line::from(Span::styled(total_tx_label, self.styles.total_tx_style)), + ]; + f.render_widget( + Paragraph::new(col2_text).block(Block::default()), + col2_loc[0], + ); + + // Column 3: RX/TX packets + let col3_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(divided_loc[2]); + let col3_text = vec![ + Line::from(Span::styled(rx_packet_rate_label, self.styles.rx_style)), + Line::from(Span::styled(tx_packet_rate_label, self.styles.tx_style)), + ]; + f.render_widget( + Paragraph::new(col3_text).block(Block::default()), + col3_loc[0], + ); + + // Column 4: AVG RX/TX packets + let col4_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(divided_loc[3]); + let col4_text = vec![ + Line::from(Span::styled( + avg_rx_packet_size_label, + self.styles.total_rx_style, + )), + Line::from(Span::styled( + avg_tx_packet_size_label, + self.styles.total_tx_style, + )), + ]; + f.render_widget( + Paragraph::new(col4_text).block(Block::default()), + col4_loc[0], + ); + } + } else { + // No packets, 2-column layout + let constraints = [Constraint::Percentage(50), Constraint::Percentage(50)]; + + let divided_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints) + .split(draw_loc); + + // Column 1: RX/TX + let col1_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(divided_loc[0]); + let col1_text = vec![ + Line::from(Span::styled(rx_label, self.styles.rx_style)), + Line::from(Span::styled(tx_label, self.styles.tx_style)), + ]; + f.render_widget( + Paragraph::new(col1_text).block(Block::default()), + col1_loc[0], + ); + + // Column 2: Total RX/TX + let col2_loc = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(1) + .split(divided_loc[1]); + let col2_text = vec![ + Line::from(Span::styled(total_rx_label, self.styles.total_rx_style)), + Line::from(Span::styled(total_tx_label, self.styles.total_tx_style)), + ]; + f.render_widget( + Paragraph::new(col2_text).block(Block::default()), + col2_loc[0], + ); + } // Update draw loc in widget map if app_state.should_get_widget_bounds() { diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index 87472c05..875400c5 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -14,6 +14,7 @@ use crate::{ Painter, components::time_graph::{AxisBound, ChartScaling, GraphData, TimeGraph}, drawing_utils::should_hide_x_label, + widgets::{PacketInfo, calculate_packet_info}, }, utils::{ data_units::*, @@ -37,10 +38,10 @@ impl Painter { ]) .split(draw_loc); - self.draw_network_graph(f, app_state, network_chunk[0], widget_id, true); - self.draw_network_labels(f, app_state, network_chunk[1], widget_id); + self.draw_network_graph(f, app_state, network_chunk[0], widget_id); + self.draw_old_network_labels(f, app_state, network_chunk[1], widget_id); } else { - self.draw_network_graph(f, app_state, draw_loc, widget_id, false); + self.draw_network_graph(f, app_state, draw_loc, widget_id); } if app_state.should_get_widget_bounds() { @@ -57,7 +58,6 @@ impl Painter { pub fn draw_network_graph( &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64, - full_screen: bool, ) { if let Some(network_widget_state) = app_state.states.net_state.widget_states.get_mut(&widget_id) @@ -147,14 +147,17 @@ impl Painter { adjust_network_data_point(y_max, &app_state.app_config_fields); let y_bounds = AxisBound::Max(adjusted_y_max); - let legend_constraints = if full_screen { - (Constraint::Ratio(0, 1), Constraint::Ratio(0, 1)) + let use_old_network_legend = app_state.app_config_fields.use_old_network_legend; + let legend_constraints = if use_old_network_legend { + // Always hide it. Note that I could pass in `None` to the position as well but eh this works. + (Constraint::Length(0), Constraint::Length(0)) } else { - (Constraint::Ratio(1, 1), Constraint::Ratio(3, 4)) + // Hide the legend if the width is 75% of the total widget width + // or the height is greater than 75% of the total widget hight. + (Constraint::Ratio(3, 4), Constraint::Ratio(3, 4)) }; // TODO: Add support for clicking on legend to only show that value on chart. - let use_binary_prefix = app_state.app_config_fields.network_use_binary_prefix; let unit_type = app_state.app_config_fields.network_unit_type; let unit = match unit_type { @@ -167,50 +170,77 @@ impl Painter { let total_rx = convert_bits(network_latest_data.total_rx, use_binary_prefix); let total_tx = convert_bits(network_latest_data.total_tx, use_binary_prefix); - // TODO: This behaviour is pretty weird, we should probably just make it so if you use old network legend - // you don't do whatever this is... - let graph_data = if app_state.app_config_fields.use_old_network_legend && !full_screen { - let rx_label = format!("RX: {:.1}{}{}", rx.0, rx.1, unit); - let tx_label = format!("TX: {:.1}{}{}", tx.0, tx.1, unit); - let total_rx_label = format!("Total RX: {:.1}{}", total_rx.0, total_rx.1); - let total_tx_label = format!("Total TX: {:.1}{}", total_tx.0, total_tx.1); - - vec![ + let graph_data = if use_old_network_legend { + let mut graph_data = vec![ GraphData::default() - .name(rx_label.into()) .time(times) .values(rx_points) .style(self.styles.rx_style), GraphData::default() - .name(tx_label.into()) .time(times) .values(tx_points) .style(self.styles.tx_style), - GraphData::default() - .style(self.styles.total_rx_style) - .name(total_rx_label.into()), - GraphData::default() - .style(self.styles.total_tx_style) - .name(total_tx_label.into()), - ] + ]; + + graph_data.extend(vec![ + GraphData::default().style(self.styles.total_rx_style), + GraphData::default().style(self.styles.total_tx_style), + ]); + + graph_data } else { let rx_label = format!("{:.1}{}{}", rx.0, rx.1, unit); let tx_label = format!("{:.1}{}{}", tx.0, tx.1, unit); let total_rx_label = format!("{:.1}{}", total_rx.0, total_rx.1); let total_tx_label = format!("{:.1}{}", total_tx.0, total_tx.1); - vec![ - GraphData::default() - .name(format!("RX: {rx_label:<10} All: {total_rx_label}").into()) - .time(times) - .values(rx_points) - .style(self.styles.rx_style), - GraphData::default() - .name(format!("TX: {tx_label:<10} All: {total_tx_label}").into()) - .time(times) - .values(tx_points) - .style(self.styles.tx_style), - ] + // Add packets information if enabled and there's enough room. + const MAX_LEGEND_WIDTH: u16 = 70; + let approx_legend_width = draw_loc.width * 3 / 4; + + // FIXME: I'm not really a huge fan of this - I think it may be better to just not support this and + // allow for more easily spawning a separate legend table (basically old legend). + if app_state.app_config_fields.network_show_packets + && approx_legend_width > MAX_LEGEND_WIDTH + { + let PacketInfo { + rx_packet_rate, + tx_packet_rate, + avg_rx_packet_size, + avg_tx_packet_size, + } = calculate_packet_info(network_latest_data, use_binary_prefix); + + let avg_rx_packet_size_label = + format!("{:.1}{}", avg_rx_packet_size.0, avg_rx_packet_size.1); + let avg_tx_packet_size_label = + format!("{:.1}{}", avg_tx_packet_size.0, avg_tx_packet_size.1); + + vec![ + GraphData::default() + .name(format!("RX: {rx_label:<10} All: {total_rx_label:<8} Packets: {rx_packet_rate:>8}pkt/s Avg: {avg_rx_packet_size_label}").into()) + .time(times) + .values(rx_points) + .style(self.styles.rx_style), + GraphData::default() + .name(format!("TX: {tx_label:<10} All: {total_tx_label:<8} Packets: {tx_packet_rate:>8}pkt/s Avg: {avg_tx_packet_size_label}").into()) + .time(times) + .values(tx_points) + .style(self.styles.tx_style), + ] + } else { + vec![ + GraphData::default() + .name(format!("RX: {rx_label:<10} All: {total_rx_label}").into()) + .time(times) + .values(rx_points) + .style(self.styles.rx_style), + GraphData::default() + .name(format!("TX: {tx_label:<10} All: {total_tx_label}").into()) + .time(times) + .values(tx_points) + .style(self.styles.tx_style), + ] + } }; let marker = if app_state.app_config_fields.use_dot { @@ -252,11 +282,9 @@ impl Painter { } } - fn draw_network_labels( + fn draw_old_network_labels( &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { - const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"]; - let network_latest_data = &(app_state.data_store.get_data().network_harvest); let use_binary_prefix = app_state.app_config_fields.network_use_binary_prefix; let unit_type = app_state.app_config_fields.network_unit_type; @@ -276,23 +304,58 @@ impl Painter { let total_rx_label = format!("{:.1}{}", total_rx.0, total_rx.1); let total_tx_label = format!("{:.1}{}", total_tx.0, total_tx.1); - // Gross but I need it to work... - let total_network = vec![Row::new([ - Text::styled(rx_label, self.styles.rx_style), - Text::styled(tx_label, self.styles.tx_style), - Text::styled(total_rx_label, self.styles.total_rx_style), - Text::styled(total_tx_label, self.styles.total_tx_style), - ])]; + let total_network = if app_state.app_config_fields.network_show_packets { + let PacketInfo { + rx_packet_rate, + tx_packet_rate, + avg_rx_packet_size, + avg_tx_packet_size, + } = calculate_packet_info(network_latest_data, use_binary_prefix); + + let avg_rx_packet_size_label = + format!("{:.1}{}", avg_rx_packet_size.0, avg_rx_packet_size.1); + let avg_tx_packet_size_label = + format!("{:.1}{}", avg_tx_packet_size.0, avg_tx_packet_size.1); + + vec![Row::new([ + Text::styled(rx_label, self.styles.rx_style), + Text::styled(tx_label, self.styles.tx_style), + Text::styled(total_rx_label, self.styles.total_rx_style), + Text::styled(total_tx_label, self.styles.total_tx_style), + Text::styled(format!("{rx_packet_rate}pkt/s"), self.styles.rx_style), + Text::styled(format!("{tx_packet_rate}pkt/s"), self.styles.tx_style), + Text::styled(avg_rx_packet_size_label, self.styles.rx_style), + Text::styled(avg_tx_packet_size_label, self.styles.tx_style), + ])] + } else { + vec![Row::new([ + Text::styled(rx_label, self.styles.rx_style), + Text::styled(tx_label, self.styles.tx_style), + Text::styled(total_rx_label, self.styles.total_rx_style), + Text::styled(total_tx_label, self.styles.total_tx_style), + ])] + }; + + let headers = if app_state.app_config_fields.network_show_packets { + vec![ + "RX", "TX", "Total RX", "Total TX", "RX Pkts", "TX Pkts", "Avg RX", "Avg TX", + ] + } else { + vec!["RX", "TX", "Total RX", "Total TX"] + }; + let num_columns = headers.len(); + + let column_width = draw_loc.width.saturating_sub(2) / num_columns as u16; // Draw f.render_widget( Table::new( total_network, - &((std::iter::repeat_n(draw_loc.width.saturating_sub(2) / 4, 4)) + &((std::iter::repeat_n(column_width, num_columns)) .map(Constraint::Length) .collect::>()), ) - .header(Row::new(NETWORK_HEADERS).style(self.styles.table_header_style)) + .header(Row::new(headers).style(self.styles.table_header_style)) .block(Block::default().borders(Borders::ALL).border_style( if app_state.current_widget.widget_id == widget_id { self.styles.highlighted_border_style diff --git a/src/collection.rs b/src/collection.rs index 07b0e60b..7f4b76b4 100644 --- a/src/collection.rs +++ b/src/collection.rs @@ -156,6 +156,8 @@ pub struct DataCollector { total_rx: u64, total_tx: u64, + total_rx_packets: u64, + total_tx_packets: u64, unnormalized_cpu: bool, use_current_cpu_total: bool, @@ -211,6 +213,8 @@ impl DataCollector { last_collection_time, total_rx: 0, total_tx: 0, + total_rx_packets: 0, + total_tx_packets: 0, show_average_cpu: false, widgets_to_harvest: UsedWidgets::default(), #[cfg(feature = "battery")] @@ -522,12 +526,16 @@ impl DataCollector { self.last_collection_time, &mut self.total_rx, &mut self.total_tx, + &mut self.total_rx_packets, + &mut self.total_tx_packets, self.data.collection_time, &self.filters.net_filter, ); self.total_rx = net_data.total_rx; self.total_tx = net_data.total_tx; + self.total_rx_packets = net_data.total_rx_packets; + self.total_tx_packets = net_data.total_tx_packets; self.data.network = Some(net_data); } } diff --git a/src/collection/network.rs b/src/collection/network.rs index b2faebca..2c31f38f 100644 --- a/src/collection/network.rs +++ b/src/collection/network.rs @@ -10,6 +10,10 @@ pub struct NetworkHarvest { pub tx: u64, pub total_rx: u64, pub total_tx: u64, + pub rx_packets: u64, + pub tx_packets: u64, + pub total_rx_packets: u64, + pub total_tx_packets: u64, } impl NetworkHarvest { diff --git a/src/collection/network/sysinfo.rs b/src/collection/network/sysinfo.rs index 2975fe66..75952692 100644 --- a/src/collection/network/sysinfo.rs +++ b/src/collection/network/sysinfo.rs @@ -11,10 +11,13 @@ use crate::app::filter::Filter; // account, so we can show per-interface! pub fn get_network_data( networks: &Networks, prev_net_access_time: Instant, prev_net_rx: &mut u64, - prev_net_tx: &mut u64, curr_time: Instant, filter: &Option, + prev_net_tx: &mut u64, prev_net_rx_packets: &mut u64, prev_net_tx_packets: &mut u64, + curr_time: Instant, filter: &Option, ) -> NetworkHarvest { let mut total_rx: u64 = 0; let mut total_tx: u64 = 0; + let mut total_rx_packets: u64 = 0; + let mut total_tx_packets: u64 = 0; for (name, network) in networks { let to_keep = if let Some(filter) = filter { @@ -26,26 +29,36 @@ pub fn get_network_data( if to_keep { total_rx += network.total_received() * 8; total_tx += network.total_transmitted() * 8; + total_rx_packets += network.total_packets_received(); + total_tx_packets += network.total_packets_transmitted(); } } let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64(); - let (rx, tx) = if elapsed_time == 0.0 { - (0, 0) + let (rx, tx, rx_packets, tx_packets) = if elapsed_time == 0.0 { + (0, 0, 0, 0) } else { ( ((total_rx.saturating_sub(*prev_net_rx)) as f64 / elapsed_time) as u64, ((total_tx.saturating_sub(*prev_net_tx)) as f64 / elapsed_time) as u64, + ((total_rx_packets.saturating_sub(*prev_net_rx_packets)) as f64 / elapsed_time) as u64, + ((total_tx_packets.saturating_sub(*prev_net_tx_packets)) as f64 / elapsed_time) as u64, ) }; *prev_net_rx = total_rx; *prev_net_tx = total_tx; + *prev_net_rx_packets = total_rx_packets; + *prev_net_tx_packets = total_tx_packets; NetworkHarvest { rx, tx, total_rx, total_tx, + rx_packets, + tx_packets, + total_rx_packets, + total_tx_packets, } } diff --git a/src/options.rs b/src/options.rs index f899caca..b85073f7 100644 --- a/src/options.rs +++ b/src/options.rs @@ -276,6 +276,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL let network_scale_type = get_network_scale_type(args, config); let network_use_binary_prefix = is_flag_enabled!(network_use_binary_prefix, args.network, config); + let network_show_packets = get_network_show_packets(args, config); let proc_columns: Option> = { config.processes.as_ref().and_then(|cfg| { @@ -330,6 +331,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL network_scale_type, network_unit_type, network_use_binary_prefix, + network_show_packets, retention_ms, dedicated_average_row: get_dedicated_avg_row(config), default_tree_collapse: is_default_tree_collapsed, @@ -977,6 +979,18 @@ fn get_network_scale_type(args: &BottomArgs, config: &Config) -> AxisScaling { AxisScaling::Linear } +fn get_network_show_packets(args: &BottomArgs, config: &Config) -> bool { + if args.network.show_packets { + return true; + } else if let Some(network_config) = &config.network { + if let Some(show_packets) = network_config.show_packets { + return show_packets; + } + } + + false +} + fn get_retention(args: &BottomArgs, config: &Config) -> OptionResult { const DEFAULT_RETENTION_MS: u64 = 600 * 1000; // Keep 10 minutes of data. diff --git a/src/options/args.rs b/src/options/args.rs index 4c849292..b617a6e6 100644 --- a/src/options/args.rs +++ b/src/options/args.rs @@ -590,6 +590,15 @@ pub struct NetworkArgs { alias = "use-old-network-legend" )] pub use_old_network_legend: bool, + + #[arg( + long, + action = ArgAction::SetTrue, + help = "Displays packets information (packet rate and average packet size) in the network widget.", + long_help = "Displays packets information including packet rate (packets per second) and average packet size in the network widget.", + alias = "show-packets" + )] + pub show_packets: bool, } /// Battery arguments/config options. diff --git a/src/options/config/flags.rs b/src/options/config/flags.rs index e2fb4354..aff78a36 100644 --- a/src/options/config/flags.rs +++ b/src/options/config/flags.rs @@ -48,6 +48,7 @@ pub(crate) struct GeneralConfig { pub(crate) network_use_bytes: Option, pub(crate) network_use_log: Option, pub(crate) network_use_binary_prefix: Option, + pub(crate) show_packets: Option, pub(crate) disable_gpu: Option, pub(crate) enable_cache_memory: Option, pub(crate) retention: Option, diff --git a/src/options/config/network.rs b/src/options/config/network.rs index da2bf18e..cf80056a 100644 --- a/src/options/config/network.rs +++ b/src/options/config/network.rs @@ -9,4 +9,6 @@ use super::IgnoreList; pub(crate) struct NetworkConfig { /// A filter over the network interface names. pub(crate) interface_filter: Option, + /// Whether to show packets information (packet rate and average packet size). + pub(crate) show_packets: Option, } diff --git a/src/utils/data_units.rs b/src/utils/data_units.rs index 39c3a1e7..d108d85b 100644 --- a/src/utils/data_units.rs +++ b/src/utils/data_units.rs @@ -64,8 +64,12 @@ pub fn get_decimal_bytes(bytes: u64) -> (f64, &'static str) { /// Given a value in _bits_, turn a tuple containing the value and a unit. #[inline] pub fn convert_bits(bits: u64, base_two: bool) -> (f64, &'static str) { - let bytes = bits / 8; + convert_bytes(bits / 8, base_two) +} +/// Given a value in _bytes_, turn a tuple containing the value and a unit. +#[inline] +pub fn convert_bytes(bytes: u64, base_two: bool) -> (f64, &'static str) { if base_two { get_binary_bytes(bytes) } else {