From fc72ef8d8559f2c6330685225a96782565b82358 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 20 Jan 2025 21:48:39 +0100 Subject: [PATCH] Add Process::accumulated_cpu_time feature Co-authored-by: Bruce Guenter --- src/common/system.rs | 19 +++++++++++++++- src/debug.rs | 1 + src/serde.rs | 1 + src/unix/apple/app_store/process.rs | 4 ++++ src/unix/apple/macos/process.rs | 34 ++++++++++++++++++++++++----- src/unix/apple/macos/system.rs | 6 ++++- src/unix/apple/system.rs | 17 +++++++++++++-- src/unix/freebsd/process.rs | 19 ++++++++++++++++ src/unix/linux/process.rs | 13 +++++++++++ src/unknown/process.rs | 4 ++++ src/windows/process.rs | 17 +++++++++++---- tests/process.rs | 29 ++++++++++++++++++++++++ 12 files changed, 151 insertions(+), 13 deletions(-) diff --git a/src/common/system.rs b/src/common/system.rs index 283660524..d03d5687f 100644 --- a/src/common/system.rs +++ b/src/common/system.rs @@ -1480,6 +1480,22 @@ impl Process { self.inner.cpu_usage() } + /// Returns the total accumulated CPU usage (in CPU-milliseconds). Note + /// that it might be bigger than the total clock run time of a process if + /// run on a multi-core machine. + /// + /// ```no_run + /// use sysinfo::{Pid, System}; + /// + /// let s = System::new_all(); + /// if let Some(process) = s.process(Pid::from(1337)) { + /// println!("{}", process.accumulated_cpu_time()); + /// } + /// ``` + pub fn accumulated_cpu_time(&self) -> u64 { + self.inner.accumulated_cpu_time() + } + /// Returns number of bytes read and written to disk. /// /// ⚠️ On Windows, this method actually returns **ALL** I/O read and @@ -1897,7 +1913,8 @@ impl ProcessRefreshKind { } } - impl_get_set!(ProcessRefreshKind, cpu, with_cpu, without_cpu); + impl_get_set!(ProcessRefreshKind, cpu, with_cpu, without_cpu, "\ +It will retrieve both CPU usage and CPU accumulated time,"); impl_get_set!( ProcessRefreshKind, disk_usage, diff --git a/src/debug.rs b/src/debug.rs index 82fad1372..a9f3fb2ed 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -43,6 +43,7 @@ impl std::fmt::Debug for crate::Process { .field("memory usage", &self.memory()) .field("virtual memory usage", &self.virtual_memory()) .field("CPU usage", &self.cpu_usage()) + .field("accumulated CPU time", &self.accumulated_cpu_time()) .field("status", &self.status()) .field("root", &self.root()) .field("disk_usage", &self.disk_usage()) diff --git a/src/serde.rs b/src/serde.rs index ced789558..e33c7bd6b 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -95,6 +95,7 @@ impl Serialize for crate::Process { state.serialize_field("start_time", &self.start_time())?; state.serialize_field("run_time", &self.run_time())?; state.serialize_field("cpu_usage", &self.cpu_usage())?; + state.serialize_field("accumulated_cpu_time", &self.accumulated_cpu_time())?; state.serialize_field("disk_usage", &self.disk_usage())?; state.serialize_field("user_id", &self.user_id())?; state.serialize_field("group_id", &self.group_id())?; diff --git a/src/unix/apple/app_store/process.rs b/src/unix/apple/app_store/process.rs index 6a0582731..dc7006dc9 100644 --- a/src/unix/apple/app_store/process.rs +++ b/src/unix/apple/app_store/process.rs @@ -69,6 +69,10 @@ impl ProcessInner { 0.0 } + pub(crate) fn accumulated_cpu_time(&self) -> u64 { + 0 + } + pub(crate) fn disk_usage(&self) -> DiskUsage { DiskUsage::default() } diff --git a/src/unix/apple/macos/process.rs b/src/unix/apple/macos/process.rs index 18f1d6eaf..2f9c94fd2 100644 --- a/src/unix/apple/macos/process.rs +++ b/src/unix/apple/macos/process.rs @@ -45,6 +45,7 @@ pub(crate) struct ProcessInner { pub(crate) old_written_bytes: u64, pub(crate) read_bytes: u64, pub(crate) written_bytes: u64, + accumulated_cpu_time: u64, } impl ProcessInner { @@ -76,6 +77,7 @@ impl ProcessInner { old_written_bytes: 0, read_bytes: 0, written_bytes: 0, + accumulated_cpu_time: 0, } } @@ -107,6 +109,7 @@ impl ProcessInner { old_written_bytes: 0, read_bytes: 0, written_bytes: 0, + accumulated_cpu_time: 0, } } @@ -178,6 +181,10 @@ impl ProcessInner { self.cpu_usage } + pub(crate) fn accumulated_cpu_time(&self) -> u64 { + self.accumulated_cpu_time + } + pub(crate) fn disk_usage(&self) -> DiskUsage { DiskUsage { read_bytes: self.read_bytes.saturating_sub(self.old_read_bytes), @@ -342,6 +349,7 @@ unsafe fn create_new_process( now: u64, refresh_kind: ProcessRefreshKind, info: Option, + timebase_to_ms: f64, ) -> Result, ()> { let info = match info { Some(info) => info, @@ -368,10 +376,20 @@ unsafe fn create_new_process( } get_cwd_root(&mut p, refresh_kind); - if refresh_kind.memory() { + if refresh_kind.cpu() || refresh_kind.memory() { let task_info = get_task_info(pid); - p.memory = task_info.pti_resident_size; - p.virtual_memory = task_info.pti_virtual_size; + + if refresh_kind.cpu() { + p.accumulated_cpu_time = (task_info + .pti_total_user + .saturating_add(task_info.pti_total_system) + as f64 + * timebase_to_ms) as u64; + } + if refresh_kind.memory() { + p.memory = task_info.pti_resident_size; + p.virtual_memory = task_info.pti_virtual_size; + } } p.user_id = Some(Uid(info.pbi_ruid)); @@ -630,6 +648,7 @@ pub(crate) fn update_process( now: u64, refresh_kind: ProcessRefreshKind, check_if_alive: bool, + timebase_to_ms: f64, ) -> Result, ()> { unsafe { if let Some(ref mut p) = (*wrap.0.get()).get_mut(&pid) { @@ -640,7 +659,7 @@ pub(crate) fn update_process( // We don't it to be removed, just replaced. p.updated = true; // The owner of this PID changed. - return create_new_process(pid, now, refresh_kind, Some(info)); + return create_new_process(pid, now, refresh_kind, Some(info), timebase_to_ms); } let parent = get_parent(&info); // Update the parent if it changed. @@ -688,6 +707,11 @@ pub(crate) fn update_process( if refresh_kind.cpu() { compute_cpu_usage(p, task_info, system_time, user_time, time_interval); + p.accumulated_cpu_time = (task_info + .pti_total_user + .saturating_add(task_info.pti_total_system) + as f64 + * timebase_to_ms) as u64; } if refresh_kind.memory() { p.memory = task_info.pti_resident_size; @@ -697,7 +721,7 @@ pub(crate) fn update_process( p.updated = true; Ok(None) } else { - create_new_process(pid, now, refresh_kind, get_bsd_info(pid)) + create_new_process(pid, now, refresh_kind, get_bsd_info(pid), timebase_to_ms) } } } diff --git a/src/unix/apple/macos/system.rs b/src/unix/apple/macos/system.rs index e2c934982..5d1e84b47 100644 --- a/src/unix/apple/macos/system.rs +++ b/src/unix/apple/macos/system.rs @@ -54,6 +54,7 @@ impl Drop for ProcessorCpuLoadInfo { pub(crate) struct SystemTimeInfo { timebase_to_ns: f64, + pub(crate) timebase_to_ms: f64, clock_per_sec: f64, old_cpu_info: ProcessorCpuLoadInfo, last_update: Option, @@ -95,9 +96,12 @@ impl SystemTimeInfo { }; let nano_per_seconds = 1_000_000_000.; + let timebase_to_ns = info.numer as f64 / info.denom as f64; sysinfo_debug!(""); Some(Self { - timebase_to_ns: info.numer as f64 / info.denom as f64, + timebase_to_ns, + // We convert from nano (10^-9) to ms (10^3). + timebase_to_ms: timebase_to_ns / 1_000_000., clock_per_sec: nano_per_seconds / clock_ticks_per_sec as f64, old_cpu_info, last_update: None, diff --git a/src/unix/apple/system.rs b/src/unix/apple/system.rs index d58b0456a..88cf11893 100644 --- a/src/unix/apple/system.rs +++ b/src/unix/apple/system.rs @@ -281,6 +281,11 @@ impl SystemInner { let now = get_now(); let port = self.port; let time_interval = self.clock_info.as_mut().map(|c| c.get_time_interval(port)); + let timebase_to_ms = self + .clock_info + .as_ref() + .map(|c| c.timebase_to_ms) + .unwrap_or_default(); let entries: Vec = { let wrap = &Wrap(UnsafeCell::new(&mut self.process_list)); @@ -293,8 +298,16 @@ impl SystemInner { return None; } nb_updated.fetch_add(1, Ordering::Relaxed); - update_process(wrap, pid, time_interval, now, refresh_kind, false) - .unwrap_or_default() + update_process( + wrap, + pid, + time_interval, + now, + refresh_kind, + false, + timebase_to_ms, + ) + .unwrap_or_default() }) .collect() }; diff --git a/src/unix/freebsd/process.rs b/src/unix/freebsd/process.rs index 7ab9d0d9b..2aae0ba86 100644 --- a/src/unix/freebsd/process.rs +++ b/src/unix/freebsd/process.rs @@ -64,6 +64,7 @@ pub(crate) struct ProcessInner { old_read_bytes: u64, written_bytes: u64, old_written_bytes: u64, + accumulated_cpu_time: u64, } impl ProcessInner { @@ -128,6 +129,10 @@ impl ProcessInner { self.cpu_usage } + pub(crate) fn accumulated_cpu_time(&self) -> u64 { + self.accumulated_cpu_time + } + pub(crate) fn disk_usage(&self) -> DiskUsage { DiskUsage { written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes), @@ -173,6 +178,12 @@ impl ProcessInner { } } +#[inline] +fn get_accumulated_cpu_time(kproc: &libc::kinfo_proc) -> u64 { + // from FreeBSD source /bin/ps/print.c + kproc.ki_runtime / 1_000 +} + pub(crate) unsafe fn get_process_data( kproc: &libc::kinfo_proc, wrap: &WrapMap, @@ -236,6 +247,9 @@ pub(crate) unsafe fn get_process_data( proc_.old_written_bytes = proc_.written_bytes; proc_.written_bytes = kproc.ki_rusage.ru_oublock as _; } + if refresh_kind.cpu() { + proc_.accumulated_cpu_time = get_accumulated_cpu_time(kproc); + } return Ok(None); } @@ -286,6 +300,11 @@ pub(crate) unsafe fn get_process_data( old_read_bytes: 0, written_bytes: kproc.ki_rusage.ru_oublock as _, old_written_bytes: 0, + accumulated_cpu_time: if refresh_kind.cpu() { + get_accumulated_cpu_time(kproc) + } else { + 0 + }, updated: true, }, })) diff --git a/src/unix/linux/process.rs b/src/unix/linux/process.rs index 33769192c..310802b98 100644 --- a/src/unix/linux/process.rs +++ b/src/unix/linux/process.rs @@ -126,6 +126,7 @@ pub(crate) struct ProcessInner { written_bytes: u64, thread_kind: Option, proc_path: PathBuf, + accumulated_cpu_time: u64, } impl ProcessInner { @@ -163,6 +164,7 @@ impl ProcessInner { written_bytes: 0, thread_kind: None, proc_path, + accumulated_cpu_time: 0, } } @@ -227,6 +229,10 @@ impl ProcessInner { self.cpu_usage } + pub(crate) fn accumulated_cpu_time(&self) -> u64 { + self.accumulated_cpu_time + } + pub(crate) fn disk_usage(&self) -> DiskUsage { DiskUsage { written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes), @@ -426,6 +432,13 @@ fn update_proc_info( if refresh_kind.disk_usage() { update_process_disk_activity(p, proc_path); } + // Needs to be after `update_time_and_memory`. + if refresh_kind.cpu() { + // The external values for CPU times are in "ticks", which are + // scaled by "HZ", which is pegged externally at 100 ticks/second. + p.accumulated_cpu_time = + p.utime.saturating_add(p.stime).saturating_mul(1_000) / info.clock_cycle; + } } fn update_parent_pid(p: &mut ProcessInner, parent_pid: Option, str_parts: &[&str]) { diff --git a/src/unknown/process.rs b/src/unknown/process.rs index dc59c2914..7a82131f4 100644 --- a/src/unknown/process.rs +++ b/src/unknown/process.rs @@ -79,6 +79,10 @@ impl ProcessInner { 0.0 } + pub(crate) fn accumulated_cpu_time(&self) -> u64 { + 0 + } + pub(crate) fn disk_usage(&self) -> DiskUsage { DiskUsage::default() } diff --git a/src/windows/process.rs b/src/windows/process.rs index f74d9a33d..428312b3b 100644 --- a/src/windows/process.rs +++ b/src/windows/process.rs @@ -52,6 +52,8 @@ use windows::Win32::UI::Shell::CommandLineToArgvW; use super::MINIMUM_CPU_UPDATE_INTERVAL; +const FILETIMES_PER_MILLISECONDS: u64 = 10_000; // 100 nanosecond units + impl fmt::Display for ProcessStatus { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match *self { @@ -194,6 +196,7 @@ pub(crate) struct ProcessInner { old_written_bytes: u64, read_bytes: u64, written_bytes: u64, + accumulated_cpu_time: u64, } struct CPUsageCalculationValues { @@ -274,6 +277,7 @@ impl ProcessInner { old_written_bytes: 0, read_bytes: 0, written_bytes: 0, + accumulated_cpu_time: 0, } } @@ -414,6 +418,10 @@ impl ProcessInner { self.cpu_usage } + pub(crate) fn accumulated_cpu_time(&self) -> u64 { + self.accumulated_cpu_time + } + pub(crate) fn disk_usage(&self) -> DiskUsage { DiskUsage { written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes), @@ -986,10 +994,7 @@ fn check_sub(a: u64, b: u64) -> u64 { /// Before changing this function, you must consider the following: /// pub(crate) fn compute_cpu_usage(p: &mut ProcessInner, nb_cpus: u64) { - if p.cpu_calc_values.last_update.elapsed() <= MINIMUM_CPU_UPDATE_INTERVAL { - // cpu usage hasn't updated. p.cpu_usage remains the same - return; - } + let need_update = p.cpu_calc_values.last_update.elapsed() > MINIMUM_CPU_UPDATE_INTERVAL; unsafe { let mut ftime: FILETIME = zeroed(); @@ -1018,6 +1023,10 @@ pub(crate) fn compute_cpu_usage(p: &mut ProcessInner, nb_cpus: u64) { let global_kernel_time = filetime_to_u64(fglobal_kernel_time); let global_user_time = filetime_to_u64(fglobal_user_time); + p.accumulated_cpu_time = user.saturating_add(sys) / FILETIMES_PER_MILLISECONDS; + if !need_update { + return; + } let delta_global_kernel_time = check_sub(global_kernel_time, p.cpu_calc_values.old_system_sys_cpu); let delta_global_user_time = diff --git a/tests/process.rs b/tests/process.rs index 4b64c7af1..7ab1cc9a5 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -929,3 +929,32 @@ fn test_multiple_single_process_refresh() { assert!(cpu_b - 5. < cpu_a && cpu_b + 5. > cpu_a); } + +#[test] +fn accumulated_cpu_time() { + if !sysinfo::IS_SUPPORTED_SYSTEM || cfg!(feature = "apple-sandbox") { + return; + } + + let mut s = System::new(); + let current_pid = sysinfo::get_current_pid().expect("failed to get current pid"); + let refresh_kind = ProcessRefreshKind::nothing().with_cpu(); + s.refresh_processes_specifics(ProcessesToUpdate::Some(&[current_pid]), false, refresh_kind); + let acc_time = s + .process(current_pid) + .expect("no process found") + .accumulated_cpu_time(); + assert_ne!(acc_time, 0); + std::thread::sleep(std::time::Duration::from_secs(2)); + s.refresh_processes_specifics(ProcessesToUpdate::Some(&[current_pid]), true, refresh_kind); + let new_acc_time = s + .process(current_pid) + .expect("no process found") + .accumulated_cpu_time(); + assert!( + new_acc_time > acc_time, + "{} not superior to {}", + new_acc_time, + acc_time + ); +}