feat: Network chart, closes #79
This commit is contained in:
+163
-14
@@ -576,8 +576,116 @@ impl fmt::Display for ByteStats {
|
||||
}
|
||||
}
|
||||
|
||||
pub type MemTuple = (Vec<(f64, f64)>, ByteStats, State);
|
||||
pub type CpuTuple = (Vec<(f64, f64)>, CpuStats, State);
|
||||
#[derive(Debug, Default, Clone, Copy, Eq)]
|
||||
pub struct BandwidthStat(u64);
|
||||
|
||||
#[cfg(test)]
|
||||
impl BandwidthStat {
|
||||
pub fn new(x: u64) -> Self {
|
||||
Self(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for BandwidthStat {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for BandwidthStat {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for BandwidthStat {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
impl Stats for BandwidthStat {
|
||||
fn get_value(&self) -> f64 {
|
||||
self.0 as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// convert from bytes to per second, using 1000 instead of 1024
|
||||
impl fmt::Display for BandwidthStat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let as_f64 = self.get_value();
|
||||
let p = match as_f64 {
|
||||
x if x >= ONE_GB => format!("{y:.2} GB/s", y = as_f64 / ONE_GB),
|
||||
x if x >= ONE_MB => format!("{y:.2} Mb/s", y = as_f64 / ONE_MB),
|
||||
_ => format!("{y:.2} kb/s", y = as_f64 / ONE_KB),
|
||||
};
|
||||
write!(f, "{p:>x$}", x = f.width().unwrap_or(1))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NetworkBandwidth(VecDeque<BandwidthStat>);
|
||||
|
||||
impl NetworkBandwidth {
|
||||
pub fn new() -> Self {
|
||||
Self(VecDeque::with_capacity(60))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Find the highest speed recorded in the vecque
|
||||
pub fn max(&self) -> BandwidthStat {
|
||||
self.to_vec_f64()
|
||||
.iter()
|
||||
.map(|(_, speed)| *speed)
|
||||
.max_by(|a, b| a.total_cmp(b))
|
||||
.map(|m| BandwidthStat(m as u64))
|
||||
.unwrap_or(BandwidthStat(0))
|
||||
}
|
||||
|
||||
pub fn push(&mut self, x: u64) {
|
||||
if self.0.len() >= 60 {
|
||||
self.0.pop_front();
|
||||
}
|
||||
self.0.push_back(BandwidthStat(x));
|
||||
}
|
||||
|
||||
/// Get the current total amount of traffic on a given device
|
||||
pub fn current_total(&self) -> ByteStats {
|
||||
self.0
|
||||
.back()
|
||||
.map_or(ByteStats::default(), |i| ByteStats::new(i.0))
|
||||
}
|
||||
|
||||
/// Convert to f64 for use in the network graph
|
||||
pub fn to_vec_f64(&self) -> Vec<(f64, f64)> {
|
||||
self.0
|
||||
.iter()
|
||||
.zip(self.0.iter().skip(1))
|
||||
.enumerate()
|
||||
.map(|(i, (prev, current))| (i as f64, current.0.saturating_sub(prev.0) as f64))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ChartsData {
|
||||
pub memory: ChartSeries<ByteStats>,
|
||||
pub cpu: ChartSeries<CpuStats>,
|
||||
pub rx: ChartSeries<BandwidthStat>,
|
||||
pub tx: ChartSeries<BandwidthStat>,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ChartSeries<T: Stats> {
|
||||
pub dataset: Vec<(f64, f64)>,
|
||||
pub max: T,
|
||||
pub current: T,
|
||||
}
|
||||
|
||||
/// Used to make sure that each log entry, for each container, is unique,
|
||||
/// will only push a log entry into the logs vec if timestamp of said log entry isn't in the hashset
|
||||
@@ -992,10 +1100,10 @@ pub struct ContainerItem {
|
||||
pub mem_stats: VecDeque<ByteStats>,
|
||||
pub name: ContainerName,
|
||||
pub ports: Vec<ContainerPorts>,
|
||||
pub rx: ByteStats,
|
||||
pub rx: NetworkBandwidth,
|
||||
pub state: State,
|
||||
pub status: ContainerStatus,
|
||||
pub tx: ByteStats,
|
||||
pub tx: NetworkBandwidth,
|
||||
}
|
||||
|
||||
/// Basic display information, for when running in debug mode
|
||||
@@ -1042,10 +1150,10 @@ impl ContainerItem {
|
||||
mem_stats: VecDeque::with_capacity(60),
|
||||
name: name.into(),
|
||||
ports,
|
||||
rx: ByteStats::default(),
|
||||
rx: NetworkBandwidth::new(),
|
||||
state,
|
||||
status,
|
||||
tx: ByteStats::default(),
|
||||
tx: NetworkBandwidth::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,7 +1179,7 @@ impl ContainerItem {
|
||||
self.cpu_stats
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|i| (i.0 as f64, i.1.0))
|
||||
.map(|(i, v)| (i as f64, v.0))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
@@ -1081,24 +1189,65 @@ impl ContainerItem {
|
||||
self.mem_stats
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|i| (i.0 as f64, i.1.0 as f64))
|
||||
.map(|(i, v)| (i as f64, v.0 as f64))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Get all cpu chart data
|
||||
fn get_cpu_chart_data(&self) -> CpuTuple {
|
||||
(self.get_cpu_dataset(), self.max_cpu_stats(), self.state)
|
||||
fn get_cpu_chart_data(&self) -> ChartSeries<CpuStats> {
|
||||
ChartSeries {
|
||||
dataset: self.get_cpu_dataset(),
|
||||
max: self.max_cpu_stats(),
|
||||
current: self
|
||||
.cpu_stats
|
||||
.back()
|
||||
.map_or_else(|| CpuStats::new(0.0), |i| *i),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all mem chart data
|
||||
fn get_mem_chart_data(&self) -> MemTuple {
|
||||
(self.get_mem_dataset(), self.max_mem_stats(), self.state)
|
||||
fn get_mem_chart_data(&self) -> ChartSeries<ByteStats> {
|
||||
ChartSeries {
|
||||
dataset: self.get_mem_dataset(),
|
||||
max: self.max_mem_stats(),
|
||||
current: self
|
||||
.mem_stats
|
||||
.back()
|
||||
.map_or_else(|| ByteStats::new(0), |i| *i),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all mem chart data
|
||||
/// Don't understand what we are doing here
|
||||
fn get_bandwidth_chart_tx_data(&self) -> ChartSeries<BandwidthStat> {
|
||||
let data = self.tx.to_vec_f64();
|
||||
ChartSeries {
|
||||
current: BandwidthStat(data.last().map_or(0, |i| i.1 as u64)),
|
||||
dataset: data,
|
||||
max: self.tx.max(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all mem chart data
|
||||
fn get_bandwidth_chart_rx_data(&self) -> ChartSeries<BandwidthStat> {
|
||||
let data = self.rx.to_vec_f64();
|
||||
ChartSeries {
|
||||
current: BandwidthStat(data.last().map_or(0, |i| i.1 as u64)),
|
||||
dataset: data,
|
||||
max: self.rx.max(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get chart info for cpu & memory in one function
|
||||
/// So only need to call .lock() once
|
||||
pub fn get_chart_data(&self) -> (CpuTuple, MemTuple) {
|
||||
(self.get_cpu_chart_data(), self.get_mem_chart_data())
|
||||
pub fn get_chart_data(&self) -> ChartsData {
|
||||
ChartsData {
|
||||
memory: self.get_mem_chart_data(),
|
||||
cpu: self.get_cpu_chart_data(),
|
||||
rx: self.get_bandwidth_chart_rx_data(),
|
||||
tx: self.get_bandwidth_chart_tx_data(),
|
||||
state: self.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+90
-32
@@ -467,12 +467,14 @@ impl AppData {
|
||||
Header::Rx => item_ord
|
||||
.0
|
||||
.rx
|
||||
.cmp(&item_ord.1.rx)
|
||||
.current_total()
|
||||
.cmp(&item_ord.1.rx.current_total())
|
||||
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
|
||||
Header::Tx => item_ord
|
||||
.0
|
||||
.tx
|
||||
.cmp(&item_ord.1.tx)
|
||||
.current_total()
|
||||
.cmp(&item_ord.1.tx.current_total())
|
||||
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
|
||||
Header::Name => item_ord
|
||||
.0
|
||||
@@ -610,10 +612,17 @@ impl AppData {
|
||||
}
|
||||
|
||||
/// Get a mutable container by given id
|
||||
#[cfg(not(test))]
|
||||
fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
||||
self.containers.items.iter_mut().find(|i| &i.id == id)
|
||||
}
|
||||
|
||||
/// As above, but make it public to testing
|
||||
#[cfg(test)]
|
||||
pub fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
||||
self.containers.items.iter_mut().find(|i| &i.id == id)
|
||||
}
|
||||
|
||||
/// Get a mutable container by given id in the tmp_container vec
|
||||
fn get_hidden_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
||||
self.hidden_containers.iter_mut().find(|i| &i.id == id)
|
||||
@@ -790,7 +799,7 @@ impl AppData {
|
||||
|
||||
/// Chart data related methods
|
||||
/// Get mutable Option of the currently selected container chart data
|
||||
pub fn get_chart_data(&self) -> Option<(CpuTuple, MemTuple)> {
|
||||
pub fn get_chart_data(&self) -> Option<ChartsData> {
|
||||
self.containers
|
||||
.state
|
||||
.selected()
|
||||
@@ -839,6 +848,7 @@ impl AppData {
|
||||
|
||||
for container in [&self.containers.items, &self.hidden_containers] {
|
||||
for container in container {
|
||||
// TODO refactor these
|
||||
let cpu_count = container.cpu_stats.back().map_or_else(
|
||||
|| count(&CpuStats::default().to_string()),
|
||||
|i| count(&i.to_string()),
|
||||
@@ -848,14 +858,19 @@ impl AppData {
|
||||
|| count(&ByteStats::default().to_string()),
|
||||
|i| count(&i.to_string()),
|
||||
);
|
||||
|
||||
columns.cpu.1 = columns.cpu.1.max(cpu_count);
|
||||
columns.image.1 = columns.image.1.max(count(&container.image.to_string()));
|
||||
columns.mem.1 = columns.mem.1.max(mem_current_count);
|
||||
columns.mem.2 = columns.mem.2.max(count(&container.mem_limit.to_string()));
|
||||
columns.name.1 = columns.name.1.max(count(&container.name.to_string()));
|
||||
columns.net_rx.1 = columns.net_rx.1.max(count(&container.rx.to_string()));
|
||||
columns.net_tx.1 = columns.net_tx.1.max(count(&container.tx.to_string()));
|
||||
columns.net_rx.1 = columns
|
||||
.net_rx
|
||||
.1
|
||||
.max(count(&container.rx.current_total().to_string()));
|
||||
columns.net_tx.1 = columns
|
||||
.net_tx
|
||||
.1
|
||||
.max(count(&container.tx.current_total().to_string()));
|
||||
columns.state.1 = columns.state.1.max(count(&container.state.to_string()));
|
||||
columns.status.1 = columns.status.1.max(count(container.status.get()));
|
||||
}
|
||||
@@ -899,8 +914,12 @@ impl AppData {
|
||||
container.mem_stats.push_back(ByteStats::new(mem));
|
||||
}
|
||||
|
||||
container.rx.update(rx);
|
||||
container.tx.update(tx);
|
||||
// Only insert if alive, or if is empty, need two to create an entry in the bandwidth chart, so instead this fills in the RX/TX total columns
|
||||
if container.rx.is_empty() || container.state.is_alive() {
|
||||
container.rx.push(rx);
|
||||
container.tx.push(tx);
|
||||
}
|
||||
|
||||
container.mem_limit.update(mem_limit);
|
||||
}
|
||||
if self.is_selected_container(id) {
|
||||
@@ -1336,13 +1355,16 @@ mod tests {
|
||||
assert_eq!(result, &containers);
|
||||
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
|
||||
i.rx = ByteStats::new(40);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(40);
|
||||
}
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
|
||||
i.rx = ByteStats::new(80);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(80);
|
||||
}
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
|
||||
i.rx = ByteStats::new(2);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(2);
|
||||
}
|
||||
|
||||
// descending
|
||||
@@ -1373,13 +1395,16 @@ mod tests {
|
||||
assert_eq!(result, &containers);
|
||||
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
|
||||
i.rx = ByteStats::new(400);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(400);
|
||||
}
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
|
||||
i.rx = ByteStats::new(80);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(80);
|
||||
}
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
|
||||
i.rx = ByteStats::new(83);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(83);
|
||||
}
|
||||
|
||||
// descending
|
||||
@@ -1437,13 +1462,16 @@ mod tests {
|
||||
assert_eq!(result, &containers);
|
||||
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
|
||||
i.rx = ByteStats::new(400);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(400);
|
||||
}
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
|
||||
i.rx = ByteStats::new(80);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(80);
|
||||
}
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
|
||||
i.rx = ByteStats::new(83);
|
||||
i.rx = NetworkBandwidth::new();
|
||||
i.rx.push(83);
|
||||
}
|
||||
|
||||
app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc)));
|
||||
@@ -2223,26 +2251,49 @@ mod tests {
|
||||
|
||||
app_data.containers_start();
|
||||
|
||||
let mut rx = NetworkBandwidth::new();
|
||||
rx.push(200);
|
||||
rx.push(100);
|
||||
rx.push(200);
|
||||
|
||||
let mut tx = NetworkBandwidth::new();
|
||||
tx.push(300);
|
||||
tx.push(600);
|
||||
tx.push(900);
|
||||
|
||||
if let Some(item) = app_data.get_container_by_id(&ContainerId::from("1")) {
|
||||
item.cpu_stats = VecDeque::from([CpuStats::new(1.1), CpuStats::new(1.2)]);
|
||||
item.cpu_stats = VecDeque::from([CpuStats::new(1.2), CpuStats::new(1.2)]);
|
||||
item.mem_stats = VecDeque::from([ByteStats::new(1), ByteStats::new(2)]);
|
||||
item.rx = rx;
|
||||
item.tx = tx;
|
||||
}
|
||||
|
||||
let result = app_data.get_chart_data();
|
||||
assert_eq!(
|
||||
result,
|
||||
Some((
|
||||
(
|
||||
vec![(0.0, 1.1), (1.0, 1.2)],
|
||||
CpuStats::new(1.2),
|
||||
State::Running(RunningState::Healthy),
|
||||
),
|
||||
(
|
||||
vec![(0.0, 1.0), (1.0, 2.0)],
|
||||
ByteStats::new(2),
|
||||
State::Running(RunningState::Healthy),
|
||||
)
|
||||
))
|
||||
Some(ChartsData {
|
||||
memory: ChartSeries {
|
||||
dataset: vec![(0.0, 1.0), (1.0, 2.0)],
|
||||
max: ByteStats::new(2),
|
||||
current: ByteStats::new(2)
|
||||
},
|
||||
cpu: ChartSeries {
|
||||
dataset: vec![(0.0, 1.2), (1.0, 1.2)],
|
||||
max: CpuStats::new(1.2),
|
||||
current: CpuStats::new(1.2)
|
||||
},
|
||||
rx: ChartSeries {
|
||||
dataset: vec![(0.0, 0.0), (1.0, 100.0)],
|
||||
max: BandwidthStat::new(100),
|
||||
current: BandwidthStat::new(100)
|
||||
},
|
||||
tx: ChartSeries {
|
||||
dataset: vec![(0.0, 300.0), (1.0, 300.0)],
|
||||
max: BandwidthStat::new(300),
|
||||
current: BandwidthStat::new(300)
|
||||
},
|
||||
state: State::Running(RunningState::Healthy)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2392,8 +2443,15 @@ mod tests {
|
||||
assert_eq!(result[0].cpu_stats, VecDeque::from([CpuStats::new(10.0)]));
|
||||
assert_eq!(result[0].mem_stats, VecDeque::from([ByteStats::new(10)]));
|
||||
assert_eq!(result[0].mem_limit, ByteStats::new(10));
|
||||
assert_eq!(result[0].rx, ByteStats::new(10));
|
||||
assert_eq!(result[0].tx, ByteStats::new(10));
|
||||
|
||||
let mut rx = NetworkBandwidth::new();
|
||||
rx.push(10);
|
||||
let mut tx = NetworkBandwidth::new();
|
||||
tx.push(10);
|
||||
assert_eq!(result[0].rx, rx);
|
||||
// VecDeque::from([ByteStats::new(10)]));
|
||||
assert_eq!(result[0].tx, tx);
|
||||
// VecDeque::from([ByteStats::new(10)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user