redbear-power: add TIME+ cumulative CPU-time column
This commit is contained in:
@@ -21,6 +21,20 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
const MAX_PROCESSES: usize = 50;
|
||||
|
||||
/// Return the number of clock ticks per second for the current
|
||||
/// process. On Linux this comes from `sysconf(_SC_CLK_TCK)`;
|
||||
/// on other targets we assume 100 (the Linux default).
|
||||
fn clock_ticks_per_second() -> u64 {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
unsafe { libc::sysconf(libc::_SC_CLK_TCK) as u64 }
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub enum SortMode {
|
||||
#[default]
|
||||
@@ -37,6 +51,8 @@ pub enum SortMode {
|
||||
VSize,
|
||||
Pid,
|
||||
Name,
|
||||
/// Cumulative CPU time (utime + stime), formatted as TIME+.
|
||||
Time,
|
||||
/// Per-thread IO total (read + write), aggregated across
|
||||
/// all threads. v1.41. Distinct from `Io` (process total)
|
||||
/// because the Linux kernel attributes all IO to the
|
||||
@@ -64,7 +80,8 @@ impl SortMode {
|
||||
SortMode::IoWriteRate => SortMode::RChar,
|
||||
SortMode::RChar => SortMode::WChar,
|
||||
SortMode::WChar => SortMode::VSize,
|
||||
SortMode::VSize => SortMode::Pid,
|
||||
SortMode::VSize => SortMode::Time,
|
||||
SortMode::Time => SortMode::Pid,
|
||||
SortMode::Pid => SortMode::Name,
|
||||
SortMode::Name => SortMode::Rss,
|
||||
SortMode::ThreadIo => SortMode::ThreadIoR,
|
||||
@@ -87,6 +104,7 @@ impl SortMode {
|
||||
SortMode::VSize => "VSZ",
|
||||
SortMode::Pid => "PID",
|
||||
SortMode::Name => "Name",
|
||||
SortMode::Time => "TIME+",
|
||||
SortMode::ThreadIo => "T-IO",
|
||||
SortMode::ThreadIoR => "T-IO-R",
|
||||
SortMode::ThreadIoW => "T-IO-W",
|
||||
@@ -137,6 +155,7 @@ impl SortMode {
|
||||
SortMode::VSize => processes.sort_by_key(|a| a.vsize_kb),
|
||||
SortMode::Pid => processes.sort_by_key(|p| p.pid),
|
||||
SortMode::Name => processes.sort_by(|a, b| a.comm.cmp(&b.comm)),
|
||||
SortMode::Time => processes.sort_by_key(|a| a.total_cpu_ticks()),
|
||||
SortMode::ThreadIo => sort_by_io_field_asc(processes, |p| {
|
||||
match (p.thread_io_read_kb, p.thread_io_write_kb) {
|
||||
(Some(r), Some(w)) => Some(r.saturating_add(w)),
|
||||
@@ -174,6 +193,7 @@ impl SortMode {
|
||||
SortMode::VSize => processes.sort_by_key(|b| std::cmp::Reverse(b.vsize_kb)),
|
||||
SortMode::Pid => processes.sort_by_key(|p| p.pid),
|
||||
SortMode::Name => processes.sort_by(|a, b| a.comm.cmp(&b.comm)),
|
||||
SortMode::Time => processes.sort_by_key(|b| std::cmp::Reverse(b.total_cpu_ticks())),
|
||||
SortMode::ThreadIo => sort_by_io_field(processes, |p| {
|
||||
match (p.thread_io_read_kb, p.thread_io_write_kb) {
|
||||
(Some(r), Some(w)) => Some(r.saturating_add(w)),
|
||||
@@ -471,6 +491,19 @@ impl ProcessInfo {
|
||||
self.utime.saturating_add(self.stime)
|
||||
}
|
||||
|
||||
/// Format cumulative CPU ticks as a TIME+ string
|
||||
/// (`MM:SS.hh`, matching htop). Ticks are converted
|
||||
/// using the host's `_SC_CLK_TCK` (100 on most Linux
|
||||
/// kernels).
|
||||
pub fn format_time_plus(ticks: u64) -> String {
|
||||
let ticks_per_sec = clock_ticks_per_second().max(1);
|
||||
let total_secs = ticks / ticks_per_sec;
|
||||
let hundredths = ((ticks % ticks_per_sec) * 100 / ticks_per_sec) % 100;
|
||||
let minutes = total_secs / 60;
|
||||
let seconds = total_secs % 60;
|
||||
format!("{}:{:02}.{:02}", minutes, seconds, hundredths)
|
||||
}
|
||||
|
||||
/// Total IO bytes (read + write) in KiB. Returns `None` if either
|
||||
/// field is `None` — the panel renders the row as `—` instead of
|
||||
/// silently zeroing a hidden counter. Used by `SortMode::Io` and
|
||||
@@ -1110,6 +1143,15 @@ mod sort_unit_tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_proc_with_time(pid: u32, utime: u64, stime: u64) -> ProcessInfo {
|
||||
ProcessInfo {
|
||||
pid,
|
||||
utime,
|
||||
stime,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_default_is_rss_descending() {
|
||||
assert_eq!(SortMode::default(), SortMode::Rss);
|
||||
@@ -1127,11 +1169,25 @@ mod sort_unit_tests {
|
||||
assert_eq!(SortMode::IoWriteRate.next(), SortMode::RChar);
|
||||
assert_eq!(SortMode::RChar.next(), SortMode::WChar);
|
||||
assert_eq!(SortMode::WChar.next(), SortMode::VSize);
|
||||
assert_eq!(SortMode::VSize.next(), SortMode::Pid);
|
||||
assert_eq!(SortMode::VSize.next(), SortMode::Time);
|
||||
assert_eq!(SortMode::Time.next(), SortMode::Pid);
|
||||
assert_eq!(SortMode::Pid.next(), SortMode::Name);
|
||||
assert_eq!(SortMode::Name.next(), SortMode::Rss);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_by_time_descending() {
|
||||
let mut ps = vec![
|
||||
make_proc_with_time(1, 100, 50),
|
||||
make_proc_with_time(2, 500, 100),
|
||||
make_proc_with_time(3, 300, 0),
|
||||
];
|
||||
SortMode::Time.sort(&mut ps);
|
||||
assert_eq!(ps[0].pid, 2);
|
||||
assert_eq!(ps[1].pid, 3);
|
||||
assert_eq!(ps[2].pid, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_by_rss_descending() {
|
||||
let mut ps = vec![
|
||||
@@ -1301,7 +1357,8 @@ mod io_sort_unit_tests {
|
||||
assert_eq!(SortMode::IoWriteRate.next(), SortMode::RChar);
|
||||
assert_eq!(SortMode::RChar.next(), SortMode::WChar);
|
||||
assert_eq!(SortMode::WChar.next(), SortMode::VSize);
|
||||
assert_eq!(SortMode::VSize.next(), SortMode::Pid);
|
||||
assert_eq!(SortMode::VSize.next(), SortMode::Time);
|
||||
assert_eq!(SortMode::Time.next(), SortMode::Pid);
|
||||
assert_eq!(SortMode::Pid.next(), SortMode::Name);
|
||||
assert_eq!(SortMode::Name.next(), SortMode::Rss);
|
||||
}
|
||||
|
||||
@@ -1214,10 +1214,13 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
_ => "RSS",
|
||||
};
|
||||
let header_str = if app.process_narrow {
|
||||
format!(" PID STATE CPU% {:<11} COMM", mem_header)
|
||||
format!(
|
||||
" PID STATE CPU% TIME+ {:<11} COMM",
|
||||
mem_header
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
" PID STATE SCHED PRIO NI THR CPU% IO RATE {:<11} T-IO T-IO/s IO-RATE CPU% RSS AFF COMM",
|
||||
" PID STATE SCHED PRIO NI THR CPU% TIME+ IO RATE {:<11} T-IO T-IO/s IO-RATE CPU% RSS AFF COMM",
|
||||
mem_header
|
||||
)
|
||||
};
|
||||
@@ -1403,6 +1406,11 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
} else {
|
||||
spans.push(Span::styled(cpu_str, cpu_style));
|
||||
}
|
||||
let time_str = format!(
|
||||
"{:<11}",
|
||||
crate::process::ProcessInfo::format_time_plus(p.total_cpu_ticks())
|
||||
);
|
||||
spans.push(Span::styled(time_str, row_style));
|
||||
if app.process_narrow {
|
||||
let narrow_post = format!(" {:<11}{:<7} ", mem_str, mem_pct);
|
||||
spans.push(Span::styled(narrow_post, row_style));
|
||||
@@ -2511,6 +2519,8 @@ pub fn render_json(app: &App) -> io::Result<()> {
|
||||
cmdline: Option<String>,
|
||||
state: String,
|
||||
cpu_pct: f64,
|
||||
time_ticks: u64,
|
||||
time_plus: String,
|
||||
rss_kb: u64,
|
||||
vsize_kb: u64,
|
||||
io_read_kb: Option<u64>,
|
||||
@@ -2619,6 +2629,8 @@ pub fn render_json(app: &App) -> io::Result<()> {
|
||||
cmdline: p.cmdline.clone(),
|
||||
state: p.state.to_string(),
|
||||
cpu_pct: p.cpu_pct,
|
||||
time_ticks: p.total_cpu_ticks(),
|
||||
time_plus: crate::process::ProcessInfo::format_time_plus(p.total_cpu_ticks()),
|
||||
rss_kb: p.rss_kb,
|
||||
vsize_kb: p.vsize_kb,
|
||||
io_read_kb: p.io_read_kb,
|
||||
|
||||
Reference in New Issue
Block a user