Skip to content

Commit

Permalink
✨ add cpu and memory usage on container list (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
pyaillet authored Jan 22, 2024
1 parent 7ed4ee5 commit 227e4a4
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 28 deletions.
2 changes: 1 addition & 1 deletion src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ impl Component {
component_delegate!(self.setup(t), [ContainerExec], Ok(()))
}
pub(crate) fn teardown(&mut self, t: &mut tui::Tui) -> Result<()> {
component_delegate!(self.teardown(t), [ContainerExec], Ok(()))
component_delegate!(self.teardown(t), [ContainerExec, Containers], Ok(()))
}

pub(crate) fn handle_input(
Expand Down
4 changes: 2 additions & 2 deletions src/components/container_logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ use ratatui::layout::{Constraint, Layout};
use ratatui::widgets::Wrap;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio::{select, spawn};
use tokio_util::sync::CancellationToken;

use ratatui::{
style::{Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, ScrollbarState},
};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;

use crate::components::{containers::Containers, Component};
use crate::{action::Action, runtime::get_container_logs};
Expand Down
132 changes: 123 additions & 9 deletions src/components/containers.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
use bollard::container::StatsOptions;
use color_eyre::Result;

use crossterm::event::{self, KeyCode, KeyEventKind};
use futures::{executor::block_on, future::join_all, StreamExt};
use humansize::{format_size, FormatSizeOptions, BINARY};

use std::{collections::HashMap, sync::Arc, time::Duration};

use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, Clear, Padding, Paragraph, TableState, Wrap},
widgets::{Block, Borders, Cell, Clear, Padding, Paragraph, Row, TableState, Wrap},
Frame,
};
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio::{select, spawn};
use tokio::{sync::mpsc::UnboundedSender, time::sleep};
use tokio_util::sync::CancellationToken;

use crate::runtime::{
delete_container, get_container, list_containers, validate_container_filters, Filter,
};
use crate::{action::Action, utils::centered_rect};
use crate::{runtime::ContainerSummary, utils::table};
use crate::{
runtime::{
delete_container,
docker::{compute_cpu, compute_mem},
get_container, get_container_stats, list_containers, validate_container_filters,
ContainerMetrics, Filter,
},
tui,
};

use crate::components::{
container_exec::ContainerExec, container_inspect::ContainerDetails,
container_logs::ContainerLogs, container_view::ContainerView, Component,
};

const CONTAINER_CONSTRAINTS: [Constraint; 5] = [
const CONTAINER_CONSTRAINTS: [Constraint; 7] = [
Constraint::Min(14),
Constraint::Max(30),
Constraint::Percentage(50),
Constraint::Min(14),
Constraint::Min(8),
Constraint::Max(4),
Constraint::Max(5),
Constraint::Max(9),
];

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -78,10 +96,74 @@ pub struct Containers {
action_tx: Option<UnboundedSender<Action>>,
sort_by: SortColumn,
filter: Filter,
metrics: Arc<Mutex<HashMap<String, ContainerMetrics>>>,
task: Arc<JoinHandle<Result<()>>>,
cancellation_token: CancellationToken,
}

async fn run_setup_task(
metrics: Arc<Mutex<HashMap<String, ContainerMetrics>>>,
cancel: CancellationToken,
) -> Result<()> {
let mut should_stop = false;
while !should_stop {
select!(
_ = update_metrics(Arc::clone(&metrics)) => {},
_ = cancel.cancelled() => {
should_stop = true;
}
);
}
Ok(())
}

async fn update_metrics(metrics: Arc<Mutex<HashMap<String, ContainerMetrics>>>) -> Result<()> {
let container_list = list_containers(false, &Filter::default()).await?;
let options = Some(StatsOptions {
stream: false,
one_shot: false,
});
let stats_futures = join_all(container_list.iter().map(|c| async {
match get_container_stats(&c.id, options).await {
Ok(mut stats) => match stats.next().await {
Some(Ok(stats)) => Some((c.id.clone(), compute_cpu(&stats), compute_mem(&stats))),
_ => None,
},
Err(_) => None,
}
}))
.await;

let mut map_lock = metrics.lock().await;
for cid_stats in stats_futures.into_iter().filter(|s| s.is_some()) {
let (cid, cpu_usage, mem_usage) = cid_stats.expect("Already checked and filtered out None");
let entry = map_lock.get_mut(&cid);
match entry {
Some(entry) => entry.push_metrics(cpu_usage, mem_usage),
None => {
let mut cm = ContainerMetrics::new(cid.clone(), 20);
cm.push_metrics(cpu_usage, mem_usage);
map_lock.insert(cid, cm);
}
}
}
drop(map_lock);

sleep(Duration::from_millis(1000)).await;

Ok(())
}

impl Containers {
pub fn new(filter: Filter) -> Self {
let metrics = Arc::new(Mutex::new(HashMap::new()));
let cancel = CancellationToken::new();
let _cancel = cancel.clone();

let _metrics = Arc::clone(&metrics);

let task = Arc::new(spawn(run_setup_task(_metrics, _cancel)));

Containers {
all: false,
state: Default::default(),
Expand All @@ -90,6 +172,9 @@ impl Containers {
action_tx: None,
sort_by: SortColumn::Name(SortOrder::Asc),
filter,
metrics,
task: Arc::clone(&task),
cancellation_token: cancel,
}
}

Expand Down Expand Up @@ -403,6 +488,7 @@ impl Containers {
}

pub(crate) fn draw(&mut self, f: &mut Frame<'_>, area: Rect) {
let stats = block_on(async { self.metrics.lock().await.clone() });
let rects = Layout::default()
.constraints([Constraint::Percentage(100)])
.split(area);
Expand All @@ -413,8 +499,25 @@ impl Containers {
if self.all { "All" } else { "Running" },
self.filter.format()
),
["Id", "Name", "Image", "Status", "Age"],
self.containers.iter().map(|c| c.into()).collect(),
["Id", "Name", "Image", "Status", "Age", "CPU", "MEM"],
self.containers
.iter()
.map(|c| {
let mut cells: Vec<Cell> = c.into();
if let Some(stats) = stats.get(&c.id) {
if let Some(cpu) = stats.cpu_data().next() {
cells.push(Cell::new(format!("{:.1}%", cpu)));
} else {
cells.push(Cell::new("-".to_string()));
}
if let Some(mem) = stats.mem_data().next() {
let format = FormatSizeOptions::from(BINARY).decimal_places(1);
cells.push(Cell::new(format_size(*mem, format)));
}
}
Row::new(cells)
})
.collect(),
&CONTAINER_CONSTRAINTS,
Some(Style::new().gray()),
);
Expand Down Expand Up @@ -460,6 +563,17 @@ impl Containers {
}
}

fn cancel(&mut self) -> Result<()> {
self.cancellation_token.cancel();
self.task.abort();
Ok(())
}

pub(crate) fn teardown(&mut self, _t: &mut tui::Tui) -> Result<()> {
self.cancel()?;
Ok(())
}

pub(crate) fn get_bindings(&self) -> Option<&[(&str, &str)]> {
Some(&[
("Enter", "Container view"),
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ mod runtime;
mod tui;
mod utils;

const DEFAULT_TICK_RATE: f64 = 4.0;
const DEFAULT_FRAME_RATE: f64 = 30.0;
const DEFAULT_TICK_RATE: f64 = 2.0;
const DEFAULT_FRAME_RATE: f64 = 10.0;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
Expand Down
18 changes: 17 additions & 1 deletion src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use tokio::sync::Mutex;

use lazy_static::lazy_static;

use bollard::container::{LogOutput, LogsOptions};
use bollard::container::{LogOutput, LogsOptions, Stats, StatsOptions};
use color_eyre::Result;
use eyre::eyre;

Expand Down Expand Up @@ -316,6 +316,22 @@ pub(crate) async fn get_container_logs(
}
}

pub(crate) async fn get_container_stats(
cid: &str,
options: Option<StatsOptions>,
) -> Result<impl Stream<Item = Result<Stats>>> {
let client = CLIENT.lock().await;
match *client {
Some(ref conn) => match &conn.client {
#[cfg(feature = "docker")]
Client::Docker(client) => client.get_container_stats(cid, options),
#[cfg(feature = "cri")]
_ => unimplemented!(),
},
_ => Err(eyre!("Not initialized")),
}
}

pub(crate) async fn container_exec(cid: &str, cmd: &str) -> Result<()> {
let client = CLIENT.lock().await;
match *client {
Expand Down
59 changes: 57 additions & 2 deletions src/runtime/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::{collections::HashMap, env, fmt::Display, fs, io::Write, path::PathBuf}

use bollard::{
container::{
InspectContainerOptions, ListContainersOptions, LogOutput, LogsOptions,
RemoveContainerOptions,
InspectContainerOptions, ListContainersOptions, LogOutput, LogsOptions, MemoryStatsStats,
RemoveContainerOptions, Stats, StatsOptions,
},
exec::{CreateExecOptions, ResizeExecOptions, StartExecResults},
image::{ListImagesOptions, RemoveImageOptions},
Expand Down Expand Up @@ -376,6 +376,18 @@ impl Client {
}))
}

pub(crate) fn get_container_stats(
&self,
cid: &str,
options: Option<StatsOptions>,
) -> Result<impl Stream<Item = Result<Stats>>> {
let stream = self.client.stats(cid, options);
Ok(stream.map(|item| match item {
Err(e) => Err(color_eyre::Report::from(e)),
Ok(other) => Ok(other),
}))
}

pub(crate) async fn list_compose_projects(&self) -> Result<Vec<Compose>> {
let c: Vec<ContainerDetails> = futures::future::try_join_all(
self.list_containers(true, &Filter::default().compose())
Expand Down Expand Up @@ -714,3 +726,46 @@ pub(crate) fn connect(config: &ConnectionConfig) -> Result<Client> {
};
Ok(Client { client: docker })
}

pub fn compute_cpu(stats: &Stats) -> Option<f64> {
let previous_cpu_usage = stats.precpu_stats.cpu_usage.total_usage;
let previous_system_usage = stats.precpu_stats.system_cpu_usage;
let cpu_delta = stats
.cpu_stats
.cpu_usage
.total_usage
.saturating_sub(previous_cpu_usage);
let system_delta = stats
.cpu_stats
.system_cpu_usage?
.saturating_sub(previous_system_usage?);
let online_cpus = match stats.cpu_stats.online_cpus {
Some(online_cpus) if online_cpus > 0 => online_cpus,
_ => stats
.cpu_stats
.cpu_usage
.percpu_usage
.clone()
.unwrap_or_default()
.len() as u64,
};
if system_delta > 0 && cpu_delta > 0 {
Some(cpu_delta as f64 / system_delta as f64 * online_cpus as f64 * 100.0)
} else {
None
}
}

pub fn compute_mem(stats: &Stats) -> Option<u64> {
let usage = stats.memory_stats.usage?;
let mem_stats = stats.memory_stats.stats?;
let cache = match mem_stats {
MemoryStatsStats::V1(mem_stats) => mem_stats.total_inactive_file,
MemoryStatsStats::V2(mem_stats) => mem_stats.inactive_file,
};
if cache < usage {
Some(usage.saturating_sub(cache))
} else {
Some(usage)
}
}
Loading

0 comments on commit 227e4a4

Please sign in to comment.