redbear-power: add TIME+ cumulative CPU-time column

This commit is contained in:
2026-07-07 10:39:43 +03:00
parent 346daee089
commit 4dc60bdc5d
2 changed files with 74 additions and 5 deletions
@@ -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,