Skip to main content

netget/tui/
metrics.rs

1//! Per-instance throughput: one sample a second, a rate, a sparkline.
2//!
3//! The snapshot carries byte totals; this keeps the deltas. Sampled on the
4//! 1s stats tick, so a sample is bytes per second and thirty samples are the
5//! last half minute — enough to see whether an instance is alive at a glance.
6
7use std::collections::VecDeque;
8
9/// Samples kept per instance.
10pub const SAMPLES: usize = 30;
11
12const BARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
13
14#[derive(Debug, Clone, Default)]
15pub struct Throughput {
16    last: Option<(u64, u64)>,
17    /// `(received, sent)` per second, oldest first.
18    samples: VecDeque<(u64, u64)>,
19}
20
21impl Throughput {
22    /// Record the current totals. The first call only establishes a baseline.
23    pub fn sample(&mut self, received_total: u64, sent_total: u64) {
24        if let Some((rx, tx)) = self.last {
25            self.samples.push_back((
26                received_total.saturating_sub(rx),
27                sent_total.saturating_sub(tx),
28            ));
29            while self.samples.len() > SAMPLES {
30                self.samples.pop_front();
31            }
32        }
33        self.last = Some((received_total, sent_total));
34    }
35
36    /// The most recent `(received, sent)` per-second sample.
37    pub fn rate(&self) -> (u64, u64) {
38        self.samples.back().copied().unwrap_or((0, 0))
39    }
40
41    pub fn samples(&self) -> impl Iterator<Item = u64> + '_ {
42        self.samples.iter().map(|(rx, tx)| rx + tx)
43    }
44
45    /// Whether anything moved in the sampled window.
46    pub fn is_idle(&self) -> bool {
47        self.samples().all(|s| s == 0)
48    }
49
50    /// The last `width` samples as bar glyphs, scaled to the window's peak.
51    /// Missing samples (a young instance) render as spaces so the line
52    /// fills in from the right as history accrues.
53    pub fn sparkline(&self, width: usize) -> String {
54        if width == 0 {
55            return String::new();
56        }
57        let values: Vec<u64> = self.samples().collect();
58        let window: Vec<u64> = values.iter().rev().take(width).rev().copied().collect();
59        let peak = window.iter().copied().max().unwrap_or(0);
60        let mut out = String::with_capacity(width * 3);
61        for _ in window.len()..width {
62            out.push(' ');
63        }
64        for value in window {
65            let glyph = if peak == 0 || value == 0 {
66                BARS[0]
67            } else {
68                // 1..=7 for anything non-zero, so a trickle is visible.
69                let level = ((value * 7) / peak).clamp(1, 7) as usize;
70                BARS[level]
71            };
72            out.push(glyph);
73        }
74        out
75    }
76}
77
78/// `340`, `1.2K`, `4.5M` — compact bytes for a 40-column pane.
79pub fn human_bytes(n: u64) -> String {
80    const UNITS: [&str; 5] = ["", "K", "M", "G", "T"];
81    if n < 1000 {
82        return n.to_string();
83    }
84    let mut value = n as f64;
85    let mut unit = 0;
86    while value >= 1000.0 && unit < UNITS.len() - 1 {
87        value /= 1000.0;
88        unit += 1;
89    }
90    if value >= 100.0 {
91        format!("{value:.0}{}", UNITS[unit])
92    } else {
93        format!("{value:.1}{}", UNITS[unit])
94    }
95}
96
97/// `1.2K/s`.
98pub fn human_rate(n: u64) -> String {
99    format!("{}/s", human_bytes(n))
100}
101
102/// `2m13s`, `1h04m`, `12s`.
103pub fn human_duration(secs: u64) -> String {
104    if secs < 60 {
105        format!("{secs}s")
106    } else if secs < 3600 {
107        format!("{}m{:02}s", secs / 60, secs % 60)
108    } else if secs < 86_400 {
109        format!("{}h{:02}m", secs / 3600, (secs % 3600) / 60)
110    } else {
111        format!("{}d{:02}h", secs / 86_400, (secs % 86_400) / 3600)
112    }
113}
114
115/// `HH:MM:SS` local time for a unix-millisecond stamp.
116pub fn clock(unix_ms: u64) -> String {
117    use chrono::TimeZone;
118    chrono::Local
119        .timestamp_millis_opt(unix_ms as i64)
120        .single()
121        .map(|t| t.format("%H:%M:%S").to_string())
122        .unwrap_or_else(|| "--:--:--".to_string())
123}
124
125/// `HH:MM:SS` now.
126pub fn clock_now() -> String {
127    chrono::Local::now().format("%H:%M:%S").to_string()
128}