diff --git a/local/recipes/system/redbear-power/source/src/process.rs b/local/recipes/system/redbear-power/source/src/process.rs index da120955e1..907ab3cde2 100644 --- a/local/recipes/system/redbear-power/source/src/process.rs +++ b/local/recipes/system/redbear-power/source/src/process.rs @@ -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); } diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index 0354289461..4629b746cc 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -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, state: String, cpu_pct: f64, + time_ticks: u64, + time_plus: String, rss_kb: u64, vsize_kb: u64, io_read_kb: Option, @@ -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,