From 5e4d32db908b6f1028adb0a65906c7d47ddaaf2a Mon Sep 17 00:00:00 2001
From: Guillaume Gomez <guillaume1.gomez@gmail.com>
Date: Mon, 20 Jan 2025 21:48:39 +0100
Subject: [PATCH] Add Process::accumulated_cpu_time feature

Co-authored-by: Bruce Guenter <bruce.guenter@datadoghq.com>
---
 src/common/system.rs                | 25 ++++++++++++++++++++-
 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                    | 32 +++++++++++++++++++++++++++
 12 files changed, 160 insertions(+), 13 deletions(-)

diff --git a/src/common/system.rs b/src/common/system.rs
index 283660524..2353a6c27 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,14 @@ 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<libc::proc_bsdinfo>,
+    timebase_to_ms: f64,
 ) -> Result<Option<Process>, ()> {
     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<Option<Process>, ()> {
     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<Instant>,
@@ -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<Process> = {
                 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<ThreadKind>,
     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<Pid>, 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:
 /// <https://github.com/GuillaumeGomez/sysinfo/issues/459>
 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..5c3447fcd 100644
--- a/tests/process.rs
+++ b/tests/process.rs
@@ -929,3 +929,35 @@ 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);
+    // We generate some CPU time usage.
+    for _ in 0..3 {
+        System::new_all();
+    }
+    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
+    );
+}