diff --git a/Cargo.lock b/Cargo.lock index 1b7c277c..b2a9c9f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -530,6 +539,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indoc" version = "2.0.6" @@ -947,6 +970,15 @@ dependencies = [ "zerocopy 0.8.14", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "ratatui" version = "0.29.0" @@ -1161,6 +1193,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "smallvec" version = "1.14.0" @@ -1361,6 +1403,12 @@ dependencies = [ "time-core", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1520,6 +1568,10 @@ name = "uu_slabtop" version = "0.0.1" dependencies = [ "clap", + "crossterm", + "im", + "parking_lot", + "ratatui", "uucore", ] @@ -1632,6 +1684,12 @@ version = "0.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb6d972f580f8223cb7052d8580aea2b7061e368cf476de32ea9457b19459ed" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index c5185202..90a64dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,10 @@ clap = { version = "4.5.4", features = ["wrap_help", "cargo"] } clap_complete = "4.5.2" clap_mangen = "0.2.20" crossterm = "0.28.1" +im = "15.1.0" libc = "0.2.154" nix = { version = "0.29", default-features = false, features = ["process"] } +parking_lot = "0.12.3" phf = "0.11.2" phf_codegen = "0.11.2" prettytable-rs = "0.10.0" diff --git a/src/uu/slabtop/Cargo.toml b/src/uu/slabtop/Cargo.toml index 8c9ba46f..1046e8c7 100644 --- a/src/uu/slabtop/Cargo.toml +++ b/src/uu/slabtop/Cargo.toml @@ -13,8 +13,12 @@ categories = ["command-line-utilities"] [dependencies] -uucore = { workspace = true } clap = { workspace = true } +crossterm = { workspace = true } +im = { workspace = true } +parking_lot = { workspace = true } +ratatui = { workspace = true } +uucore = { workspace = true } [lib] path = "src/slabtop.rs" diff --git a/src/uu/slabtop/src/parse.rs b/src/uu/slabtop/src/parse.rs index f073f3a5..4345ef28 100644 --- a/src/uu/slabtop/src/parse.rs +++ b/src/uu/slabtop/src/parse.rs @@ -9,27 +9,29 @@ use std::{ io::{Error, ErrorKind}, }; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub(crate) struct SlabInfo { - pub(crate) meta: Vec, - pub(crate) data: Vec<(String, Vec)>, + pub(crate) meta: im::Vector, + pub(crate) data: im::Vector<(String, Vec)>, } impl SlabInfo { // parse slabinfo from /proc/slabinfo + // // need root permission pub fn new() -> Result { let content = fs::read_to_string("/proc/slabinfo")?; - Self::parse(&content).ok_or(ErrorKind::Unsupported.into()) + Self::with_slabinfo(&content).ok_or(ErrorKind::Unsupported.into()) } - pub fn parse(content: &str) -> Option { + pub fn with_slabinfo(content: &str) -> Option { let mut lines: Vec<&str> = content.lines().collect(); let _ = parse_version(lines.remove(0))?; let meta = parse_meta(lines.remove(0)); - let data: Vec<(String, Vec)> = lines.into_iter().filter_map(parse_data).collect(); + let data: im::Vector<(String, Vec)> = + lines.into_iter().filter_map(parse_data).collect(); Some(SlabInfo { meta, data }) } @@ -43,6 +45,7 @@ impl SlabInfo { item.get(offset).copied() } + // Get all processes name pub fn names(&self) -> Vec<&String> { self.data.iter().map(|(k, _)| k).collect() } @@ -190,15 +193,17 @@ impl SlabInfo { return 0; }; - let iter = self.data.iter().filter_map(|(_, data)| data.get(offset)); - - let count = iter.clone().count(); - let sum = iter.sum::(); + let objsize = self + .data + .iter() + .filter_map(|(_, data)| data.get(offset)) + .collect::>(); + let count = objsize.clone().iter().count(); if count == 0 { 0 } else { - (sum) / (count as u64) + (objsize.into_iter().sum::()) / (count as u64) } } @@ -266,7 +271,7 @@ pub(crate) fn parse_version(line: &str) -> Option { .map(String::from) } -pub(crate) fn parse_meta(line: &str) -> Vec { +pub(crate) fn parse_meta(line: &str) -> im::Vector { line.replace(['#', ':'], " ") .split_whitespace() .filter(|it| it.starts_with('<') && it.ends_with('>')) @@ -310,8 +315,8 @@ mod tests { let result = parse_meta(test); assert_eq!( - result, - [ + result.into_iter().collect::>(), + vec![ "active_objs", "num_objs", "objsize", @@ -348,7 +353,7 @@ mod tests { #[test] fn test_parse() { let test = include_str!("../../../../tests/fixtures/slabtop/data.txt"); - let result = SlabInfo::parse(test.into()).unwrap(); + let result = SlabInfo::with_slabinfo(test).unwrap(); assert_eq!(result.fetch("nf_conntrack_expect", "objsize").unwrap(), 208); assert_eq!( diff --git a/src/uu/slabtop/src/slabtop.rs b/src/uu/slabtop/src/slabtop.rs index b48f90e3..f73dc77b 100644 --- a/src/uu/slabtop/src/slabtop.rs +++ b/src/uu/slabtop/src/slabtop.rs @@ -3,8 +3,21 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + thread::{self, sleep}, + time::Duration, +}; + use crate::parse::SlabInfo; -use clap::{arg, crate_version, ArgAction, Command}; +use clap::{arg, crate_version, value_parser, ArgAction, ArgMatches, Command}; +use crossterm::event::{self, KeyCode, KeyEvent, KeyModifiers}; +use parking_lot::RwLock; +use ratatui::widgets::Widget; +use tui::Tui; use uucore::{error::UResult, format_usage, help_about, help_section, help_usage}; const ABOUT: &str = help_about!("slabtop.md"); @@ -12,114 +25,101 @@ const AFTER_HELP: &str = help_section!("after help", "slabtop.md"); const USAGE: &str = help_usage!("slabtop.md"); mod parse; +mod tui; -#[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; - - let sort_flag = matches - .try_get_one::("sort") - .ok() - .unwrap_or(Some(&'o')) - .unwrap_or(&'o'); - - let slabinfo = SlabInfo::new()?.sort(*sort_flag, false); - - if matches.get_flag("once") { - output_header(&slabinfo); - println!(); - output_list(&slabinfo); - } else { - // TODO: implement TUI - output_header(&slabinfo); - println!(); - output_list(&slabinfo); - } - - Ok(()) +#[derive(Debug)] +struct Settings { + pub(crate) delay: u64, + pub(crate) once: bool, + pub(crate) short_by: char, } -fn to_kb(byte: u64) -> f64 { - byte as f64 / 1024.0 +impl Settings { + fn new(arg: &ArgMatches) -> Settings { + Settings { + delay: *arg.get_one::("delay").unwrap_or(&3), + once: arg.get_flag("once"), + short_by: *arg.get_one::("sort").unwrap_or(&'o'), + } + } } -fn percentage(numerator: u64, denominator: u64) -> f64 { - if denominator == 0 { - return 0.0; +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + let settings = Settings::new(&matches); + + let slabinfo = Arc::new(RwLock::new(SlabInfo::new()?.sort(settings.short_by, false))); + let should_update = Arc::new(AtomicBool::new(true)); + + // Timer + { + let should_update = should_update.clone(); + thread::spawn(move || loop { + sleep(Duration::from_secs(settings.delay)); + should_update.store(true, Ordering::Relaxed); + }); + } + // Update + { + let should_update = should_update.clone(); + let slabinfo = slabinfo.clone(); + thread::spawn(move || loop { + if should_update.load(Ordering::Relaxed) { + *slabinfo.write() = SlabInfo::new().unwrap().sort(settings.short_by, false); + should_update.store(false, Ordering::Relaxed); + } + sleep(Duration::from_millis(20)); + }); } - let numerator = numerator as f64; - let denominator = denominator as f64; - - (numerator / denominator) * 100.0 -} - -fn output_header(slabinfo: &SlabInfo) { - println!( - r" Active / Total Objects (% used) : {} / {} ({:.1}%)", - slabinfo.total_active_objs(), - slabinfo.total_objs(), - percentage(slabinfo.total_active_objs(), slabinfo.total_objs()) - ); - - println!( - r" Active / Total Slabs (% used) : {} / {} ({:.1}%)", - slabinfo.total_active_slabs(), - slabinfo.total_slabs(), - percentage(slabinfo.total_active_slabs(), slabinfo.total_slabs(),) - ); - - // TODO: I don't know the 'cache' meaning. - println!( - r" Active / Total Caches (% used) : {} / {} ({:.1}%)", - slabinfo.total_active_cache(), - slabinfo.total_cache(), - percentage(slabinfo.total_active_cache(), slabinfo.total_cache()) - ); - - println!( - r" Active / Total Size (% used) : {:.2}K / {:.2}K ({:.1}%)", - to_kb(slabinfo.total_active_size()), - to_kb(slabinfo.total_size()), - percentage(slabinfo.total_active_size(), slabinfo.total_size()) - ); - - println!( - r" Minimum / Average / Maximum Object : {:.2}K / {:.2}K / {:.2}K", - to_kb(slabinfo.object_minimum()), - to_kb(slabinfo.object_avg()), - to_kb(slabinfo.object_maximum()) - ); -} + let mut terminal = ratatui::init(); + // Initial output + terminal.draw(|frame| { + Tui::new(&slabinfo.read()).render(frame.area(), frame.buffer_mut()); + })?; + + loop { + if let Ok(true) = event::poll(Duration::from_millis(20)) { + // If event available, break this loop + if let Ok(e) = event::read() { + match e { + event::Event::Key(KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + }) + | event::Event::Key(KeyEvent { + code: KeyCode::Char('q'), + .. + }) => { + uucore::error::set_exit_code(0); + break; + } + event::Event::Resize(_, _) => should_update.store(true, Ordering::Relaxed), + _ => {} + } + } + } + + if should_update.load(Ordering::Relaxed) { + terminal.draw(|frame| { + Tui::new(&slabinfo.read()).render(frame.area(), frame.buffer_mut()); + })?; + } + + if settings.once { + break; + } else { + sleep(Duration::from_millis(20)); + } + } -fn output_list(info: &SlabInfo) { - let title = format!( - "{:>6} {:>6} {:>4} {:>8} {:>6} {:>8} {:>10} {:<}", - "OBJS", "ACTIVE", "USE", "OBJ SIZE", "SLABS", "OBJ/SLAB", "CACHE SIZE", "NAME" - ); - println!("{}", title); - - for name in info.names() { - let objs = info.fetch(name, "num_objs").unwrap_or_default(); - let active = info.fetch(name, "active_objs").unwrap_or_default(); - let used = format!("{:.0}%", percentage(active, objs)); - let objsize = { - let size = info.fetch(name, "objsize").unwrap_or_default(); // Byte to KB :1024 - size as f64 / 1024.0 - }; - let slabs = info.fetch(name, "num_slabs").unwrap_or_default(); - let obj_per_slab = info.fetch(name, "objperslab").unwrap_or_default(); - - let cache_size = (objsize * (objs as f64)) as u64; - let objsize = format!("{:.2}", objsize); - - let content = format!( - "{:>6} {:>6} {:>4} {:>7}K {:>6} {:>8} {:>10} {:<}", - objs, active, used, objsize, slabs, obj_per_slab, cache_size, name - ); - - println!("{}", content); + if !settings.once { + ratatui::restore(); } + + Ok(()) } #[allow(clippy::cognitive_complexity)] @@ -130,9 +130,12 @@ pub fn uu_app() -> Command { .override_usage(format_usage(USAGE)) .infer_long_args(true) .args([ - // arg!(-d --delay "delay updates"), + arg!(-d --delay "delay updates") + .value_parser(value_parser!(u64)) + .default_value("3"), arg!(-o --once "only display once, then exit").action(ArgAction::SetTrue), - arg!(-s --sort "specify sort criteria by character (see below)"), + arg!(-s --sort "specify sort criteria by character (see below)") + .value_parser(value_parser!(char)), ]) .after_help(AFTER_HELP) } diff --git a/src/uu/slabtop/src/tui.rs b/src/uu/slabtop/src/tui.rs new file mode 100644 index 00000000..214012f1 --- /dev/null +++ b/src/uu/slabtop/src/tui.rs @@ -0,0 +1,147 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use ratatui::{ + prelude::*, + widgets::{List, ListItem, Widget}, +}; + +use crate::SlabInfo; + +pub(crate) struct Tui<'a> { + slabinfo: &'a SlabInfo, +} + +impl Tui<'_> { + pub(crate) fn new(slabinfo: &SlabInfo) -> Tui { + Tui { slabinfo } + } + + fn render_header(&self, area: Rect, buf: &mut Buffer) { + let lines = vec![ + format!( + r" Active / Total Objects (% used) : {} / {} ({:.1}%)", + self.slabinfo.total_active_objs(), + self.slabinfo.total_objs(), + percentage( + self.slabinfo.total_active_objs(), + self.slabinfo.total_objs() + ) + ), + format!( + r" Active / Total Slabs (% used) : {} / {} ({:.1}%)", + self.slabinfo.total_active_slabs(), + self.slabinfo.total_slabs(), + percentage( + self.slabinfo.total_active_slabs(), + self.slabinfo.total_slabs(), + ) + ), + // TODO: I don't know the 'cache' meaning. + format!( + r" Active / Total Caches (% used) : {} / {} ({:.1}%)", + self.slabinfo.total_active_cache(), + self.slabinfo.total_cache(), + percentage( + self.slabinfo.total_active_cache(), + self.slabinfo.total_cache() + ) + ), + format!( + r" Active / Total Size (% used) : {:.2}K / {:.2}K ({:.1}%)", + to_kb(self.slabinfo.total_active_size()), + to_kb(self.slabinfo.total_size()), + percentage( + self.slabinfo.total_active_size(), + self.slabinfo.total_size() + ) + ), + format!( + r" Minimum / Average / Maximum Object : {:.2}K / {:.2}K / {:.2}K", + to_kb(self.slabinfo.object_minimum()), + to_kb(self.slabinfo.object_avg()), + to_kb(self.slabinfo.object_maximum()) + ), + ] + .into_iter() + .map(Line::from); + + Widget::render(List::new(lines), area, buf); + } + + fn render_list(&self, area: Rect, buf: &mut Buffer) { + let mut list = vec![ListItem::from(format!( + "{:>6} {:>6} {:>4} {:>8} {:>6} {:>8} {:>10} {:<}", + "OBJS", "ACTIVE", "USE", "OBJ SIZE", "SLABS", "OBJ/SLAB", "CACHE SIZE", "NAME" + )) + .bg(Color::Black)]; + + self.slabinfo.names().truncate(area.height.into()); + list.extend( + self.slabinfo + .names() + .iter() + .map(|name| self.build_list_item(name)), + ); + + Widget::render(List::new(list), area, buf); + } + + fn build_list_item(&self, name: &str) -> ListItem<'_> { + let objs = self.slabinfo.fetch(name, "num_objs").unwrap_or_default(); + let active = self.slabinfo.fetch(name, "active_objs").unwrap_or_default(); + let used = format!("{:.0}%", percentage(active, objs)); + let objsize = { + let size = self.slabinfo.fetch(name, "objsize").unwrap_or_default(); // Byte to KB :1024 + size as f64 / 1024.0 + }; + let slabs = self.slabinfo.fetch(name, "num_slabs").unwrap_or_default(); + let obj_per_slab = self.slabinfo.fetch(name, "objperslab").unwrap_or_default(); + + let cache_size = (objsize * (objs as f64)) as u64; + let objsize = format!("{:.2}", objsize); + + ListItem::from(format!( + "{:>6} {:>6} {:>4} {:>7}K {:>6} {:>8} {:>10} {:<}", + objs, active, used, objsize, slabs, obj_per_slab, cache_size, name + )) + } +} + +impl Widget for Tui<'_> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + // layout[0]: Header + // layout[1]: List of process + let layout = Layout::new( + Direction::Vertical, + [Constraint::Max(6), Constraint::Min(0)], + ) + .split(area); + + let header = layout[0]; + let list = layout[1]; + + self.render_header(header, buf); + self.render_list(list, buf); + } +} + +fn to_kb(byte: u64) -> f64 { + byte as f64 / 1024.0 +} + +fn percentage(numerator: u64, denominator: u64) -> f64 { + if denominator == 0 { + return 0.0; + } + + let numerator = numerator as f64; + let denominator = denominator as f64; + + (numerator / denominator) * 100.0 +}