Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions gping/src/histogram.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use core::time::Duration;

Check warning on line 2 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

Check warning on line 2 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
use tui::{
buffer::Buffer,
layout::{Layout, Constraint, Flex, Rect},
symbols,
style::{Color, Style},
text::Line,
widgets::{Axis, Block, Chart, Dataset, GraphType, Padding, Paragraph, Widget},
};

Check warning on line 11 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

Check warning on line 11 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
/// defines the x-axis extent, effectively a zoom parameter
const OVERFLOW_SIZE : usize = 15;

const DEFAULT_WINDOW_SIZE : usize = 500;

#[derive(Debug)]
pub struct HistogramState {
pub enabled: bool,

Check warning on line 19 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

Check warning on line 19 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
/// the raw data used to compute the histogram
/// the length of this cannot exceed window_size, if set
pub samples : Vec<u64>,
/// how many samples to use when generating the historgram
/// if None, all samples will be used without limit.
pub window_size : Option<usize>,
pub bin_buckets : Vec<u64>,
pub bin_counts : Vec<u64>,
plot_data: Vec<(f64, f64)>,
max_count: u64,
max_bin: u64,
overflow_bin: u64

Check warning on line 31 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

Check warning on line 31 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
}

impl Default for HistogramState {
fn default() -> Self {

Check warning on line 35 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

Check warning on line 35 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
let bin_buckets: Vec<u64> = [
( 1 .. 50 ).step_by(1).collect::<Vec<i64>>(),
( 50 .. 250 ).step_by(5).collect::<Vec<i64>>(),
( 250 .. 1000 ).step_by(100).collect::<Vec<i64>>(),
].concat().iter().map(|x| *x as u64).collect();

HistogramState {
samples: Vec::new(),
window_size: Some(DEFAULT_WINDOW_SIZE),
bin_counts: vec![0; bin_buckets.len()],
plot_data: Vec::new(),
overflow_bin: bin_buckets[bin_buckets.len() - 1],
max_bin: 0,
max_count: 0,
enabled: false,
bin_buckets,
}
}

Check warning on line 53 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

Check warning on line 53 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
}


/// helper function to create a top-right rect using a count of lines of the available rect `r`
/// modified from https://ratatui.rs/examples/apps/popup/
fn popup_area(area: Rect, consume_y: u16) -> Rect {
let vertical = Layout::vertical([Constraint::Length(consume_y)]).flex(Flex::Start);
let horizontal = Layout::horizontal([Constraint::Fill(1), Constraint::Min(20)]);
let [area] = vertical.areas(area);
let [_, area] = horizontal.areas(area);
area
}

impl HistogramState {
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
}

pub fn add_sample(&mut self, x: Option<Duration>) {

Check warning on line 72 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

Check warning on line 72 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
let x = match x {
None => u64::MAX,
Some(d ) => {
let millis = d.as_millis();
if millis >= u64::MAX as u128 {
u64::MAX
} else {

Check warning on line 79 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
millis as u64
}
}
};

self.samples.push(x);

Check warning on line 86 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
// roll window
if let Some(window) = self.window_size {
while self.samples.len() > window {
self.samples.remove(0);
}
}

// we collect data when disabled, but we don't do anything else.
if self.enabled {
self.update();
}
}

Check warning on line 98 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

Check warning on line 98 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs

fn _bin_index(&self, x: &u64) -> usize {
for i in 0 .. self.bin_buckets.len() {
if *x <= self.bin_buckets[i] {
return i
}
}

self.bin_buckets.len() - 1
}

// FIXME: not efficient, recalculates from scratch
fn update_bins(&mut self) {
// initialize
let n = self.bin_counts.len();

self.bin_counts = Vec::with_capacity(n);
self.bin_counts.resize(n, 0);

// count
for i in self.samples.iter() {
let idx = self._bin_index(i);
self.bin_counts[idx] += 1
}
}

fn update(&mut self) {
self.update_bins();

Check warning on line 127 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
self.max_count = *self.bin_counts.iter().max().unwrap_or(&0);
let (max_bin, overflow_bin_idx) = {
let max_bin_idx: usize = self.bin_counts.iter().position(|&x| x == self.max_count).unwrap_or(self.bin_buckets.len() - 1);

let next_max_bin = match max_bin_idx {
x if x + OVERFLOW_SIZE >= self.bin_buckets.len() - 1 => self.bin_buckets.len() - 1,
x => x + OVERFLOW_SIZE

Check warning on line 134 in gping/src/histogram.rs

View workflow job for this annotation

GitHub Actions / Checks

Diff in /home/runner/work/gping/gping/gping/src/histogram.rs
};

(self.bin_buckets[max_bin_idx], next_max_bin)
};

self.max_bin = max_bin;
self.overflow_bin = self.bin_buckets[overflow_bin_idx];

let overflow: u64 = self.bin_counts[overflow_bin_idx..].iter().sum();

let mut plot_data: Vec<(f64, f64)> = self.bin_buckets.iter().map(|x| *x as f64).zip(self.bin_counts.iter().map(|x| *x as f64)).collect();

// add the overflow to the last visible bin
if overflow > 0 {
plot_data.get_mut(overflow_bin_idx).unwrap().1 += overflow as f64
}

self.plot_data = plot_data;
}

fn dataset(&self) -> Dataset<'_> {
Dataset::default()
.marker(symbols::Marker::HalfBlock)
.style(Style::new().fg(Color::White))
.graph_type(GraphType::Bar)
.data(&self.plot_data)
}

pub fn render_histogram(&self, area: &Rect, buffer: &mut Buffer) {
let dataset = self.dataset();

Chart::new(vec![dataset])
.block(Block::new().padding(Padding{ left: 2, right: 2, top: 2, bottom: 2}))
.x_axis(
Axis::default()
.bounds([0.0, self.overflow_bin as f64])
)
.y_axis(
Axis::default()
.bounds([0.0, self.max_count as f64])
)
.render(*area, buffer);

let stats_area = popup_area(*area, 3 + 2);
let stats_text = vec![
Line::from(vec![
format!("Samples: {}", self.samples.len()).into(),
]),
Line::from(vec![
format!("Mode: {} ms", self.max_bin).into(),
]),
Line::from(vec![
format!("Overflow >= {} ms", self.overflow_bin).into(),
])
];
Paragraph::new(stats_text).block(Block::bordered().title("Stats")).render(stats_area, buffer);
}
}
36 changes: 34 additions & 2 deletions gping/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,19 @@ use std::thread::{sleep, JoinHandle};
use std::time::{Duration, Instant};
use tui::backend::{Backend, CrosstermBackend};
use tui::layout::{Constraint, Direction, Flex, Layout};
use tui::prelude::Rect;
use tui::style::{Color, Style};
use tui::text::Span;
use tui::widgets::{Axis, Block, Borders, Chart, Dataset};
use tui::Terminal;

mod colors;
mod histogram;
mod plot_data;
mod region_map;

use colors::Colors;
use histogram::HistogramState;
use shadow_rs::{formatcp, shadow};
use tui::prelude::Position;

Expand Down Expand Up @@ -127,6 +130,7 @@ following color names: 'black', 'red', 'green', 'yellow', 'blue', 'magenta',

struct App {
data: Vec<PlotData>,
histogram: HistogramState,
display_interval: chrono::Duration,
started: chrono::DateTime<Local>,
}
Expand All @@ -136,13 +140,16 @@ impl App {
App {
data,
display_interval: chrono::Duration::from_std(Duration::from_secs(buffer)).unwrap(),
histogram: HistogramState::default(),
started: Local::now(),
}
}

/// receiver of results from the ping thread
fn update(&mut self, host_idx: usize, item: Option<Duration>) {
let host = &mut self.data[host_idx];
host.update(item);
self.histogram.add_sample(item);
}

fn y_axis_bounds(&self) -> [f64; 2] {
Expand Down Expand Up @@ -240,6 +247,7 @@ impl From<PingResult> for Update {
enum Event {
Update(usize, Update),
Terminate,
ToggleHistogram,
Render,
}

Expand Down Expand Up @@ -491,6 +499,9 @@ fn main() -> Result<()> {
key_tx.send(Event::Terminate)?;
break;
}
KeyCode::Char('h') => {
key_tx.send(Event::ToggleHistogram)?;
}
_ => {}
}
}
Expand All @@ -517,6 +528,18 @@ fn main() -> Result<()> {
}
Event::Render => {
terminal.draw(|f| {
let (chunk_area, histogram) = match app.histogram.enabled {
true => {
let output: [Rect; 2] = Layout::horizontal([
Constraint::Percentage(75),
Constraint::Fill(1),
])
.areas(f.area());
(output[0], Some(output[1]))
}
false => (f.area(), None),
};

let chunks = Layout::default()
.flex(Flex::Legacy)
.direction(Direction::Vertical)
Expand All @@ -528,7 +551,7 @@ fn main() -> Result<()> {
.chain(iter::once(Constraint::Percentage(10)))
.collect::<Vec<_>>(),
)
.split(f.area());
.split(chunk_area);

let total_chunks = chunks.len();

Expand Down Expand Up @@ -579,13 +602,22 @@ fn main() -> Result<()> {
.labels(app.y_axis_labels(y_axis_bounds)),
);

f.render_widget(chart, *chart_chunk)
f.render_widget(chart, *chart_chunk);

if app.histogram.enabled {
let histogram_area = histogram.expect("Histogram area wasn't created.");
app.histogram
.render_histogram(&histogram_area, f.buffer_mut());
}
})?;
}
Event::Terminate => {
killed.store(true, Ordering::Release);
break;
}
Event::ToggleHistogram => {
app.histogram.toggle();
}
}
}
killed.store(true, Ordering::Relaxed);
Expand Down
Loading