diff --git a/src/lib.rs b/src/lib.rs index d30ecc72d..233a9e7df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,6 +175,9 @@ mod doctest { #[cfg(test)] mod test { + use std::collections::HashMap; + use std::time::Instant; + use crate::*; #[cfg(feature = "unknown-ci")] @@ -255,6 +258,63 @@ mod test { .any(|(_, proc_)| proc_.cpu_usage() > 0.0)); } + #[test] + fn check_processes_total_accumulated_cpu_usage() { + if System::IS_SUPPORTED { + let mut s = System::new(); + + // Grab the intial accumulated CPU usages + s.refresh_cpu(); + s.refresh_processes(); + s.refresh_processes(); // Needed on some OS to fully populate the accumulated CPU usage + let first_time = Instant::now(); + let all_procs: HashMap<_, _> = s + .processes() + .iter() + .map(|(pid, proc)| (*pid, proc.total_accumulated_cpu_usage())) + .collect(); + + // All accumulated CPU usages will be non-negative. + all_procs.values().for_each(|&usage| assert!(usage >= 0.0)); + // At least one will be positive. + assert!(all_procs.values().any(|&usage| usage > 0.0)); + + // Wait a bit to update CPU usage values + std::thread::sleep(System::MINIMUM_CPU_UPDATE_INTERVAL); + s.refresh_processes(); + let duration = Instant::now().duration_since(first_time).as_secs_f32(); + + // They will still all be non-negative. + s.processes() + .values() + .for_each(|proc| assert!(proc.total_accumulated_cpu_usage() >= 0.0)); + + // They will all have either remained the same or + // increased no more than a valid amount. + let max_delta = s.cpus().len() as f32 * duration; + s.processes().iter().for_each(|(pid, proc)| { + if let Some(prev) = all_procs.get(pid) { + let delta = proc.total_accumulated_cpu_usage() - prev; + assert!( + delta >= 0.0 && delta <= max_delta, + "CPU time delta is out of range delta={} max_delta={} pid={}", + delta, + max_delta, + pid, + ); + } + }); + + // At least one of them will have accumulated some CPU time. + #[cfg(not(windows))] // Windows CPU timers appear to have insufficient resolution + assert!(s.processes().iter().any(|(pid, proc)| { + all_procs + .get(pid) + .map_or(false, |&prev| proc.total_accumulated_cpu_usage() > prev) + })); + } + } + #[test] fn check_cpu_usage() { if !System::IS_SUPPORTED { diff --git a/src/traits.rs b/src/traits.rs index af90f9e35..bbdd655dc 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -375,6 +375,20 @@ pub trait ProcessExt: Debug { /// ``` fn cpu_usage(&self) -> f32; + /// Returns the total accumulated CPU usage (in + /// CPU-seconds). Notice 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, ProcessExt, System, SystemExt}; + /// + /// let s = System::new_all(); + /// if let Some(process) = s.process(Pid::from(1337)) { + /// println!("{}sec", process.total_accumulated_cpu_usage()); + /// } + /// ``` + fn total_accumulated_cpu_usage(&self) -> f32; + /// Returns number of bytes read and written to disk. /// /// ⚠️ On Windows and FreeBSD, this method actually returns **ALL** I/O read and written bytes. diff --git a/src/unix/apple/app_store/process.rs b/src/unix/apple/app_store/process.rs index 033075bfa..0eacc309c 100644 --- a/src/unix/apple/app_store/process.rs +++ b/src/unix/apple/app_store/process.rs @@ -68,6 +68,10 @@ impl ProcessExt for Process { 0.0 } + fn total_accumulated_cpu_usage(&self) -> f32 { + 0.0 + } + fn disk_usage(&self) -> DiskUsage { DiskUsage::default() } diff --git a/src/unix/apple/cpu.rs b/src/unix/apple/cpu.rs index 30df4547c..bed474d86 100644 --- a/src/unix/apple/cpu.rs +++ b/src/unix/apple/cpu.rs @@ -255,7 +255,7 @@ pub(crate) fn update_cpu_usage, *mut i32) -> (f32, usize) let mut cpu_info: *mut i32 = std::ptr::null_mut(); let mut num_cpu_info = 0u32; - let mut total_cpu_usage = 0f32; + let mut total_accumulated_cpu_usage = 0f32; unsafe { if host_processor_info( @@ -268,9 +268,9 @@ pub(crate) fn update_cpu_usage, *mut i32) -> (f32, usize) { let (total_percentage, len) = f(Arc::new(CpuData::new(cpu_info, num_cpu_info)), cpu_info); - total_cpu_usage = total_percentage / len as f32; + total_accumulated_cpu_usage = total_percentage / len as f32; } - global_cpu.set_cpu_usage(total_cpu_usage); + global_cpu.set_cpu_usage(total_accumulated_cpu_usage); } } diff --git a/src/unix/apple/macos/process.rs b/src/unix/apple/macos/process.rs index e6fa4f58e..1fc376294 100644 --- a/src/unix/apple/macos/process.rs +++ b/src/unix/apple/macos/process.rs @@ -32,6 +32,7 @@ pub struct Process { run_time: u64, pub(crate) updated: bool, cpu_usage: f32, + accum_cpu_usage: f32, user_id: Option, effective_user_id: Option, group_id: Option, @@ -62,6 +63,7 @@ impl Process { memory: 0, virtual_memory: 0, cpu_usage: 0., + accum_cpu_usage: 0., old_utime: 0, old_stime: 0, updated: true, @@ -93,6 +95,7 @@ impl Process { memory: 0, virtual_memory: 0, cpu_usage: 0., + accum_cpu_usage: 0., old_utime: 0, old_stime: 0, updated: true, @@ -181,6 +184,10 @@ impl ProcessExt for Process { self.cpu_usage } + fn total_accumulated_cpu_usage(&self) -> f32 { + self.accum_cpu_usage + } + fn disk_usage(&self) -> DiskUsage { DiskUsage { read_bytes: self.read_bytes.saturating_sub(self.old_read_bytes), @@ -558,6 +565,7 @@ unsafe fn create_new_process( Ok(Some(p)) } +#[allow(clippy::too_many_arguments)] pub(crate) fn update_process( wrap: &Wrap, pid: Pid, @@ -566,6 +574,7 @@ pub(crate) fn update_process( now: u64, refresh_kind: ProcessRefreshKind, check_if_alive: bool, + timebase_to_seconds: f64, ) -> Result, ()> { unsafe { if let Some(ref mut p) = (*wrap.0.get()).get_mut(&pid) { @@ -614,6 +623,10 @@ pub(crate) fn update_process( if refresh_kind.cpu() { compute_cpu_usage(p, task_info, system_time, user_time, time_interval); } + p.accum_cpu_usage = (task_info + .pti_total_user + .saturating_add(task_info.pti_total_system) as f64 + * timebase_to_seconds) as f32; p.memory = task_info.pti_resident_size; p.virtual_memory = task_info.pti_virtual_size; diff --git a/src/unix/apple/macos/system.rs b/src/unix/apple/macos/system.rs index 6c5a79031..c433fde64 100644 --- a/src/unix/apple/macos/system.rs +++ b/src/unix/apple/macos/system.rs @@ -11,6 +11,8 @@ use libc::{ }; use std::ptr::null_mut; +pub(crate) const NANOS_PER_SECOND: f64 = 1_000_000_000.; + struct ProcessorCpuLoadInfo { cpu_load: processor_cpu_load_info_t, cpu_count: natural_t, @@ -55,6 +57,7 @@ impl Drop for ProcessorCpuLoadInfo { pub(crate) struct SystemTimeInfo { timebase_to_ns: f64, + pub timebase_to_sec: f64, clock_per_sec: f64, old_cpu_info: ProcessorCpuLoadInfo, } @@ -93,11 +96,12 @@ impl SystemTimeInfo { } }; - let nano_per_seconds = 1_000_000_000.; sysinfo_debug!(""); + let timebase_to_ns = info.numer as f64 / info.denom as f64; Some(Self { - timebase_to_ns: info.numer as f64 / info.denom as f64, - clock_per_sec: nano_per_seconds / clock_ticks_per_sec as f64, + timebase_to_ns, + timebase_to_sec: timebase_to_ns / NANOS_PER_SECOND, + clock_per_sec: NANOS_PER_SECOND / clock_ticks_per_sec as f64, old_cpu_info, }) } diff --git a/src/unix/apple/system.rs b/src/unix/apple/system.rs index 04891eaa0..d7ebcaae4 100644 --- a/src/unix/apple/system.rs +++ b/src/unix/apple/system.rs @@ -237,6 +237,10 @@ impl SystemExt for System { let arg_max = get_arg_max(); let port = self.port; let time_interval = self.clock_info.as_mut().map(|c| c.get_time_interval(port)); + let timebase_to_sec = self + .clock_info + .as_ref() + .map_or(1.0, |ci| ci.timebase_to_sec); let entries: Vec = { let wrap = &Wrap(UnsafeCell::new(&mut self.process_list)); @@ -253,6 +257,7 @@ impl SystemExt for System { now, refresh_kind, false, + timebase_to_sec, ) { Ok(x) => x, _ => None, @@ -293,6 +298,9 @@ impl SystemExt for System { now, refresh_kind, true, + self.clock_info + .as_ref() + .map_or(1.0, |ci| ci.timebase_to_sec), ) } { Ok(Some(p)) => { diff --git a/src/unix/freebsd/process.rs b/src/unix/freebsd/process.rs index 40c5fce58..575ba1730 100644 --- a/src/unix/freebsd/process.rs +++ b/src/unix/freebsd/process.rs @@ -54,6 +54,7 @@ pub struct Process { pub(crate) virtual_memory: u64, pub(crate) updated: bool, cpu_usage: f32, + accum_cpu_usage: f32, start_time: u64, run_time: u64, pub(crate) status: ProcessStatus, @@ -129,6 +130,10 @@ impl ProcessExt for Process { self.cpu_usage } + fn total_accumulated_cpu_usage(&self) -> f32 { + self.accum_cpu_usage + } + fn disk_usage(&self) -> DiskUsage { DiskUsage { written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes), @@ -207,6 +212,9 @@ pub(crate) unsafe fn get_process_data( }; let status = ProcessStatus::from(kproc.ki_stat); + // from FreeBSD source /bin/ps/print.c + let accum_cpu_usage = (kproc.ki_runtime as f64 / 1000000.0) as f32; + // from FreeBSD source /src/usr.bin/top/machine.c let virtual_memory = kproc.ki_size as _; let memory = (kproc.ki_rssize as u64).saturating_mul(page_size as _); @@ -275,6 +283,7 @@ pub(crate) unsafe fn get_process_data( start_time, run_time: now.saturating_sub(start_time), cpu_usage, + accum_cpu_usage, virtual_memory, memory, // procstat_getfiles diff --git a/src/unix/linux/process.rs b/src/unix/linux/process.rs index 9fc8e5547..1649b96bf 100644 --- a/src/unix/linux/process.rs +++ b/src/unix/linux/process.rs @@ -89,10 +89,11 @@ pub struct Process { old_written_bytes: u64, read_bytes: u64, written_bytes: u64, + clock_cycle: u64, } impl Process { - pub(crate) fn new(pid: Pid) -> Process { + pub(crate) fn new(pid: Pid, info: &SystemInfo) -> Process { Process { name: String::with_capacity(20), pid, @@ -128,6 +129,7 @@ impl Process { old_written_bytes: 0, read_bytes: 0, written_bytes: 0, + clock_cycle: info.clock_cycle, } } } @@ -194,6 +196,13 @@ impl ProcessExt for Process { self.cpu_usage } + fn total_accumulated_cpu_usage(&self) -> f32 { + // The external values for CPU times are in "ticks", which are + // scaled by "HZ", which is pegged externally at 100 + // ticks/second. + self.utime.saturating_add(self.stime) as f32 / self.clock_cycle as f32 + } + fn disk_usage(&self) -> DiskUsage { DiskUsage { written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes), @@ -369,7 +378,7 @@ fn retrieve_all_new_process_info( refresh_kind: ProcessRefreshKind, uptime: u64, ) -> Process { - let mut p = Process::new(pid); + let mut p = Process::new(pid, info); let mut tmp = PathHandler::new(path); let name = parts[1]; diff --git a/src/unix/linux/system.rs b/src/unix/linux/system.rs index 2ac80ecb8..d0d31e8b4 100644 --- a/src/unix/linux/system.rs +++ b/src/unix/linux/system.rs @@ -215,7 +215,8 @@ impl SystemExt for System { const MINIMUM_CPU_UPDATE_INTERVAL: Duration = Duration::from_millis(200); fn new_with_specifics(refreshes: RefreshKind) -> System { - let process_list = Process::new(Pid(0)); + let info = SystemInfo::new(); + let process_list = Process::new(Pid(0), &info); let mut s = System { process_list, mem_total: 0, @@ -229,7 +230,7 @@ impl SystemExt for System { swap_free: 0, cpus: CpusWrapper::new(), users: Vec::new(), - info: SystemInfo::new(), + info, }; s.refresh_specifics(refreshes); s diff --git a/src/unknown/process.rs b/src/unknown/process.rs index 4e116d7df..a4185e5ce 100644 --- a/src/unknown/process.rs +++ b/src/unknown/process.rs @@ -78,6 +78,10 @@ impl ProcessExt for Process { 0.0 } + fn total_accumulated_cpu_usage(&self) -> f32 { + 0.0 + } + fn disk_usage(&self) -> DiskUsage { DiskUsage::default() } diff --git a/src/windows/process.rs b/src/windows/process.rs index 0e9ff1cd5..763c1480a 100644 --- a/src/windows/process.rs +++ b/src/windows/process.rs @@ -58,6 +58,8 @@ use winapi::um::winnt::{ RTL_OSVERSIONINFOEXW, TOKEN_QUERY, TOKEN_USER, ULARGE_INTEGER, }; +const FILETIMES_PER_SECOND: f32 = 10_000_000.0; // 100 nanosecond units + impl fmt::Display for ProcessStatus { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match *self { @@ -207,11 +209,13 @@ pub struct Process { written_bytes: u64, } +#[derive(Debug)] struct CPUsageCalculationValues { old_process_sys_cpu: u64, old_process_user_cpu: u64, old_system_sys_cpu: u64, old_system_user_cpu: u64, + nb_cpus: u64, } impl CPUsageCalculationValues { @@ -221,9 +225,17 @@ impl CPUsageCalculationValues { old_process_user_cpu: 0, old_system_sys_cpu: 0, old_system_user_cpu: 0, + nb_cpus: 0, } } + + fn total_accumulated_cpu_usage(&self) -> f32 { + self.old_process_user_cpu + .saturating_add(self.old_process_sys_cpu) as f32 + / FILETIMES_PER_SECOND + } } + static WINDOWS_8_1_OR_NEWER: Lazy = Lazy::new(|| unsafe { let mut version_info: RTL_OSVERSIONINFOEXW = MaybeUninit::zeroed().assume_init(); @@ -599,6 +611,10 @@ impl ProcessExt for Process { self.cpu_usage } + fn total_accumulated_cpu_usage(&self) -> f32 { + self.cpu_calc_values.total_accumulated_cpu_usage() + } + fn disk_usage(&self) -> DiskUsage { DiskUsage { written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes), @@ -1108,6 +1124,7 @@ pub(crate) fn compute_cpu_usage(p: &mut Process, nb_cpus: u64) { p.cpu_calc_values.old_process_sys_cpu = sys; p.cpu_calc_values.old_system_user_cpu = global_user_time; p.cpu_calc_values.old_system_sys_cpu = global_kernel_time; + p.cpu_calc_values.nb_cpus = nb_cpus; let denominator = delta_global_user_time.saturating_add(delta_global_kernel_time) as f32;