Repository: denoland/deno
Pull request: feat: implement `process.cpuUsage` (`Deno.cpuUsage`)

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->


Changed files:
filename: ext/node/polyfills/process.ts
patch: @@ -441,11 +441,7 @@ Process.prototype.config = {
 };
 
 Process.prototype.cpuUsage = function () {
-  warnNotImplemented("process.cpuUsage()");
-  return {
-    user: 0,
-    system: 0,
-  };
+  return Deno.cpuUsage();
 };
 
 /** https://nodejs.org/api/process.html#process_process_cwd */

filename: ext/os/README.md
patch: @@ -32,3 +32,11 @@ This crate implements OS specific APIs for Deno
 | Linux         | sysinfo and `/proc/meminfo`                                                                                                                   | -           |
 | Windows       | `sysinfoapi::GlobalMemoryStatusEx`                                                                                                            | -           |
 | macOS         | <br> <pre> sysctl([CTL_HW, HW_MEMSIZE]); <br> sysctl([CTL_VM, VM_SWAPUSAGE]); <br> host_statistics64(mach_host_self(), HOST_VM_INFO64) </pre> | -           |
+
+`cpu_usage`
+
+| Target family | Syscall                              | Description |
+| ------------- | ------------------------------------ | ----------- |
+| Linux         | getrusage                            | -           |
+| Windows       | `processthreadsapi::GetProcessTimes` | -           |
+| macOS         | getrusage                            | -           |

filename: ext/os/lib.rs
patch: @@ -69,6 +69,7 @@ deno_core::extension!(
     op_get_exit_code,
     op_system_memory_info,
     op_uid,
+    op_runtime_cpu_usage,
     op_runtime_memory_usage,
     ops::signal::op_signal_bind,
     ops::signal::op_signal_unbind,
@@ -106,6 +107,7 @@ deno_core::extension!(
     op_get_exit_code,
     op_system_memory_info,
     op_uid,
+    op_runtime_cpu_usage,
     op_runtime_memory_usage,
     ops::signal::op_signal_bind,
     ops::signal::op_signal_unbind,
@@ -420,27 +422,127 @@ fn op_uid(state: &mut OpState) -> Result<Option<u32>, PermissionCheckError> {
   Ok(None)
 }
 
-// HeapStats stores values from a isolate.get_heap_statistics() call
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct MemoryUsage {
-  rss: usize,
-  heap_total: usize,
-  heap_used: usize,
-  external: usize,
+#[op2]
+#[serde]
+fn op_runtime_cpu_usage() -> (usize, usize) {
+  let (sys, user) = get_cpu_usage();
+  (sys.as_micros() as usize, user.as_micros() as usize)
+}
+
+#[cfg(unix)]
+fn get_cpu_usage() -> (std::time::Duration, std::time::Duration) {
+  let mut rusage = std::mem::MaybeUninit::uninit();
+
+  // Uses POSIX getrusage from libc
+  // to retrieve user and system times
+  // SAFETY: libc call
+  let ret = unsafe { libc::getrusage(libc::RUSAGE_SELF, rusage.as_mut_ptr()) };
+  if ret != 0 {
+    return Default::default();
+  }
+
+  // SAFETY: already checked the result
+  let rusage = unsafe { rusage.assume_init() };
+
+  let sys = std::time::Duration::from_micros(rusage.ru_stime.tv_usec as u64)
+    + std::time::Duration::from_secs(rusage.ru_stime.tv_sec as u64);
+  let user = std::time::Duration::from_micros(rusage.ru_utime.tv_usec as u64)
+    + std::time::Duration::from_secs(rusage.ru_utime.tv_sec as u64);
+
+  (sys, user)
+}
+
+#[cfg(windows)]
+fn get_cpu_usage() -> (std::time::Duration, std::time::Duration) {
+  use winapi::shared::minwindef::FALSE;
+  use winapi::shared::minwindef::FILETIME;
+  use winapi::shared::minwindef::TRUE;
+  use winapi::um::minwinbase::SYSTEMTIME;
+  use winapi::um::processthreadsapi::GetCurrentProcess;
+  use winapi::um::processthreadsapi::GetProcessTimes;
+  use winapi::um::timezoneapi::FileTimeToSystemTime;
+
+  fn convert_system_time(system_time: SYSTEMTIME) -> std::time::Duration {
+    std::time::Duration::from_secs(
+      system_time.wHour as u64 * 3600
+        + system_time.wMinute as u64 * 60
+        + system_time.wSecond as u64,
+    ) + std::time::Duration::from_millis(system_time.wMilliseconds as u64)
+  }
+
+  let mut creation_time = std::mem::MaybeUninit::<FILETIME>::uninit();
+  let mut exit_time = std::mem::MaybeUninit::<FILETIME>::uninit();
+  let mut kernel_time = std::mem::MaybeUninit::<FILETIME>::uninit();
+  let mut user_time = std::mem::MaybeUninit::<FILETIME>::uninit();
+
+  // SAFETY: winapi calls
+  let ret = unsafe {
+    GetProcessTimes(
+      GetCurrentProcess(),
+      creation_time.as_mut_ptr(),
+      exit_time.as_mut_ptr(),
+      kernel_time.as_mut_ptr(),
+      user_time.as_mut_ptr(),
+    )
+  };
+
+  if ret != TRUE {
+    return std::default::Default::default();
+  }
+
+  let mut kernel_system_time = std::mem::MaybeUninit::<SYSTEMTIME>::uninit();
+  let mut user_system_time = std::mem::MaybeUninit::<SYSTEMTIME>::uninit();
+
+  // SAFETY: convert to system time
+  unsafe {
+    let sys_ret = FileTimeToSystemTime(
+      kernel_time.assume_init_mut(),
+      kernel_system_time.as_mut_ptr(),
+    );
+    let user_ret = FileTimeToSystemTime(
+      user_time.assume_init_mut(),
+      user_system_time.as_mut_ptr(),
+    );
+
+    match (sys_ret, user_ret) {
+      (TRUE, TRUE) => (
+        convert_system_time(kernel_system_time.assume_init()),
+        convert_system_time(user_system_time.assume_init()),
+      ),
+      (TRUE, FALSE) => (
+        convert_system_time(kernel_system_time.assume_init()),
+        Default::default(),
+      ),
+      (FALSE, TRUE) => (
+        Default::default(),
+        convert_system_time(user_system_time.assume_init()),
+      ),
+      (_, _) => Default::default(),
+    }
+  }
+}
+
+#[cfg(not(any(windows, unix)))]
+fn get_cpu_usage() -> (std::time::Duration, std::time::Duration) {
+  Default::default()
 }
 
 #[op2]
 #[serde]
-fn op_runtime_memory_usage(scope: &mut v8::HandleScope) -> MemoryUsage {
+fn op_runtime_memory_usage(
+  scope: &mut v8::HandleScope,
+) -> (usize, usize, usize, usize) {
   let mut s = v8::HeapStatistics::default();
   scope.get_heap_statistics(&mut s);
-  MemoryUsage {
-    rss: rss(),
-    heap_total: s.total_heap_size(),
-    heap_used: s.used_heap_size(),
-    external: s.external_memory(),
-  }
+
+  let (rss, heap_total, heap_used, external) = (
+    rss(),
+    s.total_heap_size(),
+    s.used_heap_size(),
+    s.external_memory(),
+  );
+
+  (rss, heap_total, heap_used, external)
 }
 
 #[cfg(any(target_os = "android", target_os = "linux"))]

filename: runtime/js/90_deno_ns.js
patch: @@ -4,6 +4,7 @@ import { core, primordials } from "ext:core/mod.js";
 import {
   op_net_listen_udp,
   op_net_listen_unixpacket,
+  op_runtime_cpu_usage,
   op_runtime_memory_usage,
 } from "ext:core/ops";
 
@@ -59,7 +60,15 @@ const denoNs = {
   makeTempDir: fs.makeTempDir,
   makeTempFileSync: fs.makeTempFileSync,
   makeTempFile: fs.makeTempFile,
-  memoryUsage: () => op_runtime_memory_usage(),
+  cpuUsage: () => {
+    const { 0: system, 1: user } = op_runtime_cpu_usage();
+    return { system, user };
+  },
+  memoryUsage: () => {
+    const { 0: rss, 1: heapTotal, 2: heapUsed, 3: external } =
+      op_runtime_memory_usage();
+    return { rss, heapTotal, heapUsed, external };
+  },
   mkdirSync: fs.mkdirSync,
   mkdir: fs.mkdir,
   chdir: fs.chdir,