diff --git a/Cargo.lock b/Cargo.lock index 2bc6f891ba..362d64ba99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3264,6 +3264,7 @@ name = "nu-system" version = "0.93.1" dependencies = [ "chrono", + "itertools 0.12.1", "libc", "libproc", "log", diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index ba55472e15..a23f9c4ef4 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -164,6 +164,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { #[cfg(any( target_os = "android", target_os = "linux", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", target_os = "macos", target_os = "windows" ))] diff --git a/crates/nu-command/src/system/mod.rs b/crates/nu-command/src/system/mod.rs index 788e1b7740..c0f890232d 100644 --- a/crates/nu-command/src/system/mod.rs +++ b/crates/nu-command/src/system/mod.rs @@ -5,6 +5,8 @@ mod nu_check; target_os = "android", target_os = "linux", target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", target_os = "macos", target_os = "windows" ))] @@ -23,6 +25,8 @@ pub use nu_check::NuCheck; target_os = "android", target_os = "linux", target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", target_os = "macos", target_os = "windows" ))] diff --git a/crates/nu-command/src/system/ps.rs b/crates/nu-command/src/system/ps.rs index 922bf7915b..c64549a44d 100644 --- a/crates/nu-command/src/system/ps.rs +++ b/crates/nu-command/src/system/ps.rs @@ -2,20 +2,13 @@ use itertools::Itertools; use nu_engine::command_prelude::*; -#[cfg(all( - unix, - not(target_os = "freebsd"), - not(target_os = "macos"), - not(target_os = "windows"), - not(target_os = "android"), -))] +#[cfg(target_os = "linux")] use procfs::WithCurrentSystemInfo; use std::time::Duration; #[derive(Clone)] pub struct Ps; -#[cfg(not(target_os = "freebsd"))] impl Command for Ps { fn name(&self) -> &str { "ps" @@ -82,7 +75,6 @@ impl Command for Ps { } } -#[cfg(not(target_os = "freebsd"))] fn run_ps( engine_state: &EngineState, stack: &mut Stack, @@ -111,12 +103,7 @@ fn run_ps( if long { record.push("command", Value::string(proc.command(), span)); - #[cfg(all( - unix, - not(target_os = "macos"), - not(target_os = "windows"), - not(target_os = "android"), - ))] + #[cfg(target_os = "linux")] { let proc_stat = proc .curr_proc diff --git a/crates/nu-system/Cargo.toml b/crates/nu-system/Cargo.toml index 771f4417ca..88d6b28b05 100644 --- a/crates/nu-system/Cargo.toml +++ b/crates/nu-system/Cargo.toml @@ -16,6 +16,7 @@ bench = false libc = { workspace = true } log = { workspace = true } sysinfo = { workspace = true } +itertools = { workspace = true } [target.'cfg(target_family = "unix")'.dependencies] nix = { workspace = true, default-features = false, features = ["fs", "term", "process", "signal"] } diff --git a/crates/nu-system/src/freebsd.rs b/crates/nu-system/src/freebsd.rs new file mode 100644 index 0000000000..ff0f67dd10 --- /dev/null +++ b/crates/nu-system/src/freebsd.rs @@ -0,0 +1,305 @@ +use itertools::{EitherOrBoth, Itertools}; +use libc::{ + kinfo_proc, sysctl, CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ALL, KERN_PROC_ARGS, TDF_IDLETD, +}; +use std::{ + ffi::CStr, + io, + mem::{self, MaybeUninit}, + ptr, + time::{Duration, Instant}, +}; + +#[derive(Debug)] +pub struct ProcessInfo { + pub pid: i32, + pub ppid: i32, + pub name: String, + pub argv: Vec, + pub stat: i8, + pub percent_cpu: f64, + pub mem_resident: u64, // in bytes + pub mem_virtual: u64, // in bytes +} + +pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec { + compare_procs(interval).unwrap_or_else(|err| { + log::warn!("Failed to get processes: {}", err); + vec![] + }) +} + +fn compare_procs(interval: Duration) -> io::Result> { + let pagesize = get_pagesize()? as u64; + + // Compare two full snapshots of all of the processes over the interval + let now = Instant::now(); + let procs_a = get_procs()?; + std::thread::sleep(interval); + let procs_b = get_procs()?; + let true_interval = Instant::now().saturating_duration_since(now); + let true_interval_sec = true_interval.as_secs_f64(); + + // Group all of the threads in each process together + let a_grouped = procs_a.into_iter().group_by(|proc| proc.ki_pid); + let b_grouped = procs_b.into_iter().group_by(|proc| proc.ki_pid); + + // Join the processes between the two snapshots + Ok(a_grouped + .into_iter() + .merge_join_by(b_grouped.into_iter(), |(pid_a, _), (pid_b, _)| { + pid_a.cmp(pid_b) + }) + .map(|threads| { + // Join the threads between the two snapshots for the process + let mut threads = { + let (left, right) = threads.left_and_right(); + left.into_iter() + .flat_map(|(_, threads)| threads) + .merge_join_by( + right.into_iter().flat_map(|(_, threads)| threads), + |thread_a, thread_b| thread_a.ki_tid.cmp(&thread_b.ki_tid), + ) + .peekable() + }; + + // Pick the later process entry of the first thread to use for basic process information + let proc = match threads.peek().ok_or(io::ErrorKind::NotFound)? { + EitherOrBoth::Both(_, b) => b, + EitherOrBoth::Left(a) => a, + EitherOrBoth::Right(b) => b, + } + .clone(); + + // Skip over the idle process. It always appears with high CPU usage when the + // system is idle + if proc.ki_tdflags as u64 & TDF_IDLETD as u64 != 0 { + return Err(io::ErrorKind::NotFound.into()); + } + + // Aggregate all of the threads that exist in both snapshots and sum their runtime. + let (runtime_a, runtime_b) = + threads + .flat_map(|t| t.both()) + .fold((0., 0.), |(runtime_a, runtime_b), (a, b)| { + let runtime_in_seconds = + |proc: &kinfo_proc| proc.ki_runtime as f64 /* µsec */ / 1_000_000.0; + ( + runtime_a + runtime_in_seconds(&a), + runtime_b + runtime_in_seconds(&b), + ) + }); + + // The percentage CPU is the ratio of how much runtime occurred for the process out of + // the true measured interval that occurred. + let percent_cpu = 100. * (runtime_b - runtime_a).max(0.) / true_interval_sec; + + let info = ProcessInfo { + pid: proc.ki_pid, + ppid: proc.ki_ppid, + name: read_cstr(&proc.ki_comm).to_string_lossy().into_owned(), + argv: get_proc_args(proc.ki_pid)?, + stat: proc.ki_stat, + percent_cpu, + mem_resident: proc.ki_rssize.max(0) as u64 * pagesize, + mem_virtual: proc.ki_size.max(0) as u64, + }; + Ok(info) + }) + // Remove errors from the list - probably just processes that are gone now + .flat_map(|result: io::Result<_>| result.ok()) + .collect()) +} + +fn check(err: libc::c_int) -> std::io::Result<()> { + if err < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +/// This is a bounds-checked way to read a `CStr` from a slice of `c_char` +fn read_cstr(slice: &[libc::c_char]) -> &CStr { + unsafe { + // SAFETY: ensure that c_char and u8 are the same size + mem::transmute::(0); + let slice: &[u8] = mem::transmute(slice); + CStr::from_bytes_until_nul(slice).unwrap_or_default() + } +} + +fn get_procs() -> io::Result> { + // To understand what's going on here, see the sysctl(3) manpage for FreeBSD. + unsafe { + const STRUCT_SIZE: usize = mem::size_of::(); + let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_ALL]; + + // First, try to figure out how large a buffer we need to allocate + // (calling with NULL just tells us that) + let mut data_len = 0; + check(sysctl( + ctl_name.as_ptr(), + ctl_name.len() as u32, + ptr::null_mut(), + &mut data_len, + ptr::null(), + 0, + ))?; + + // data_len will be set in bytes, so divide by the size of the structure + let expected_len = data_len.div_ceil(STRUCT_SIZE); + + // Now allocate the Vec and set data_len to the real number of bytes allocated + let mut vec: Vec = Vec::with_capacity(expected_len); + data_len = vec.capacity() * STRUCT_SIZE; + + // Call sysctl() again to put the result in the vec + check(sysctl( + ctl_name.as_ptr(), + ctl_name.len() as u32, + vec.as_mut_ptr() as *mut libc::c_void, + &mut data_len, + ptr::null(), + 0, + ))?; + + // If that was ok, we can set the actual length of the vec to whatever + // data_len was changed to, since that should now all be properly initialized data. + let true_len = data_len.div_ceil(STRUCT_SIZE); + vec.set_len(true_len); + + // Sort the procs by pid and then tid before using them + vec.sort_by_key(|p| (p.ki_pid, p.ki_tid)); + Ok(vec) + } +} + +fn get_proc_args(pid: i32) -> io::Result> { + unsafe { + let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_ARGS, pid]; + + // First, try to figure out how large a buffer we need to allocate + // (calling with NULL just tells us that) + let mut data_len = 0; + check(sysctl( + ctl_name.as_ptr(), + ctl_name.len() as u32, + ptr::null_mut(), + &mut data_len, + ptr::null(), + 0, + ))?; + + // Now allocate the Vec and set data_len to the real number of bytes allocated + let mut vec: Vec = Vec::with_capacity(data_len); + data_len = vec.capacity(); + + // Call sysctl() again to put the result in the vec + check(sysctl( + ctl_name.as_ptr(), + ctl_name.len() as u32, + vec.as_mut_ptr() as *mut libc::c_void, + &mut data_len, + ptr::null(), + 0, + ))?; + + // If that was ok, we can set the actual length of the vec to whatever + // data_len was changed to, since that should now all be properly initialized data. + vec.set_len(data_len); + Ok(vec) + } +} + +// For getting simple values from the sysctl interface +unsafe fn get_ctl(ctl_name: &[i32]) -> io::Result { + let mut value: MaybeUninit = MaybeUninit::uninit(); + let mut value_len = mem::size_of_val(&value); + check(sysctl( + ctl_name.as_ptr(), + ctl_name.len() as u32, + value.as_mut_ptr() as *mut libc::c_void, + &mut value_len, + ptr::null(), + 0, + ))?; + Ok(value.assume_init()) +} + +fn get_pagesize() -> io::Result { + // not in libc for some reason + const HW_PAGESIZE: i32 = 7; + unsafe { get_ctl(&[CTL_HW, HW_PAGESIZE]) } +} + +impl ProcessInfo { + /// PID of process + pub fn pid(&self) -> i32 { + self.pid + } + + /// Parent PID of process + pub fn ppid(&self) -> i32 { + self.ppid + } + + /// Name of command + pub fn name(&self) -> String { + let argv_name = self + .argv + .split(|b| *b == 0) + .next() + .map(String::from_utf8_lossy) + .unwrap_or_default() + .into_owned(); + + if !argv_name.is_empty() { + argv_name + } else { + // Just use the command name alone. + self.name.clone() + } + } + + /// Full name of command, with arguments + pub fn command(&self) -> String { + if let Some(last_nul) = self.argv.iter().rposition(|b| *b == 0) { + // The command string is NUL separated + // Take the string up to the last NUL, then replace the NULs with spaces + String::from_utf8_lossy(&self.argv[0..last_nul]).replace("\0", " ") + } else { + // The argv is empty, so use the name instead + self.name() + } + } + + /// Get the status of the process + pub fn status(&self) -> String { + match self.stat { + libc::SIDL | libc::SRUN => "Running", + libc::SSLEEP => "Sleeping", + libc::SSTOP => "Stopped", + libc::SWAIT => "Waiting", + libc::SLOCK => "Locked", + libc::SZOMB => "Zombie", + _ => "Unknown", + } + .into() + } + + /// CPU usage as a percent of total + pub fn cpu_usage(&self) -> f64 { + self.percent_cpu + } + + /// Memory size in number of bytes + pub fn mem_size(&self) -> u64 { + self.mem_resident + } + + /// Virtual memory size in bytes + pub fn virtual_size(&self) -> u64 { + self.mem_virtual + } +} diff --git a/crates/nu-system/src/lib.rs b/crates/nu-system/src/lib.rs index 6058ed4fcf..2fc97d2e7e 100644 --- a/crates/nu-system/src/lib.rs +++ b/crates/nu-system/src/lib.rs @@ -1,8 +1,13 @@ mod foreground; + +#[cfg(target_os = "freebsd")] +mod freebsd; #[cfg(any(target_os = "android", target_os = "linux"))] mod linux; #[cfg(target_os = "macos")] mod macos; +#[cfg(any(target_os = "netbsd", target_os = "openbsd"))] +mod netbsd; pub mod os_info; #[cfg(target_os = "windows")] mod windows; @@ -10,9 +15,14 @@ mod windows; #[cfg(unix)] pub use self::foreground::stdin_fd; pub use self::foreground::{ForegroundChild, ForegroundGuard}; + +#[cfg(target_os = "freebsd")] +pub use self::freebsd::*; #[cfg(any(target_os = "android", target_os = "linux"))] pub use self::linux::*; #[cfg(target_os = "macos")] pub use self::macos::*; +#[cfg(any(target_os = "netbsd", target_os = "openbsd"))] +pub use self::netbsd::*; #[cfg(target_os = "windows")] pub use self::windows::*; diff --git a/crates/nu-system/src/netbsd.rs b/crates/nu-system/src/netbsd.rs new file mode 100644 index 0000000000..04f69b35fe --- /dev/null +++ b/crates/nu-system/src/netbsd.rs @@ -0,0 +1,336 @@ +//! This is used for both NetBSD and OpenBSD, because they are fairly similar. + +use itertools::{EitherOrBoth, Itertools}; +use libc::{sysctl, CTL_HW, CTL_KERN, KERN_PROC_ALL, KERN_PROC_ARGS, KERN_PROC_ARGV}; +use std::{ + io, + mem::{self, MaybeUninit}, + ptr, + time::{Duration, Instant}, +}; + +#[cfg(target_os = "netbsd")] +type KInfoProc = libc::kinfo_proc2; +#[cfg(target_os = "openbsd")] +type KInfoProc = libc::kinfo_proc; + +#[derive(Debug)] +pub struct ProcessInfo { + pub pid: i32, + pub ppid: i32, + pub argv: Vec, + pub stat: i8, + pub percent_cpu: f64, + pub mem_resident: u64, // in bytes + pub mem_virtual: u64, // in bytes +} + +pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec { + compare_procs(interval).unwrap_or_else(|err| { + log::warn!("Failed to get processes: {}", err); + vec![] + }) +} + +fn compare_procs(interval: Duration) -> io::Result> { + let pagesize = get_pagesize()? as u64; + + // Compare two full snapshots of all of the processes over the interval + let now = Instant::now(); + let procs_a = get_procs()?; + std::thread::sleep(interval); + let procs_b = get_procs()?; + let true_interval = Instant::now().saturating_duration_since(now); + let true_interval_sec = true_interval.as_secs_f64(); + + // Join the processes between the two snapshots + Ok(procs_a + .into_iter() + .merge_join_by(procs_b.into_iter(), |a, b| a.p_pid.cmp(&b.p_pid)) + .map(|proc| { + // Take both snapshotted processes if we can, but if not then just keep the one that + // exists and set prev_proc to None + let (prev_proc, proc) = match proc { + EitherOrBoth::Both(a, b) => (Some(a), b), + EitherOrBoth::Left(a) => (None, a), + EitherOrBoth::Right(b) => (None, b), + }; + + // The percentage CPU is the ratio of how much runtime occurred for the process out of + // the true measured interval that occurred. + let percent_cpu = if let Some(prev_proc) = prev_proc { + let prev_rtime = + prev_proc.p_rtime_sec as f64 + prev_proc.p_rtime_usec as f64 / 1_000_000.0; + let rtime = proc.p_rtime_sec as f64 + proc.p_rtime_usec as f64 / 1_000_000.0; + 100. * (rtime - prev_rtime).max(0.) / true_interval_sec + } else { + 0.0 + }; + + Ok(ProcessInfo { + pid: proc.p_pid, + ppid: proc.p_ppid, + argv: get_proc_args(proc.p_pid, KERN_PROC_ARGV)?, + stat: proc.p_stat, + percent_cpu, + mem_resident: proc.p_vm_rssize.max(0) as u64 * pagesize, + #[cfg(target_os = "netbsd")] + mem_virtual: proc.p_vm_msize.max(0) as u64 * pagesize, + #[cfg(target_os = "openbsd")] + mem_virtual: proc.p_vm_map_size.max(0) as u64 * pagesize, + }) + }) + // Remove errors from the list - probably just processes that are gone now + .flat_map(|result: io::Result<_>| result.ok()) + .collect()) +} + +fn check(err: libc::c_int) -> std::io::Result<()> { + if err < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +/// Call `sysctl()` in read mode (i.e. the last two arguments are NULL and zero) +unsafe fn sysctl_get( + name: *const i32, + name_len: u32, + data: *mut libc::c_void, + data_len: *mut usize, +) -> i32 { + sysctl( + name, + name_len, + data, + data_len, + // NetBSD and OpenBSD differ in mutability for this pointer, but it's null anyway + #[cfg(target_os = "netbsd")] + ptr::null(), + #[cfg(target_os = "openbsd")] + ptr::null_mut(), + 0, + ) +} + +fn get_procs() -> io::Result> { + // To understand what's going on here, see the sysctl(3) and sysctl(7) manpages for NetBSD. + unsafe { + const STRUCT_SIZE: usize = mem::size_of::(); + + #[cfg(target_os = "netbsd")] + const TGT_KERN_PROC: i32 = libc::KERN_PROC2; + #[cfg(target_os = "openbsd")] + const TGT_KERN_PROC: i32 = libc::KERN_PROC; + + let mut ctl_name = [ + CTL_KERN, + TGT_KERN_PROC, + KERN_PROC_ALL, + 0, + STRUCT_SIZE as i32, + 0, + ]; + + // First, try to figure out how large a buffer we need to allocate + // (calling with NULL just tells us that) + let mut data_len = 0; + check(sysctl_get( + ctl_name.as_ptr(), + ctl_name.len() as u32, + ptr::null_mut(), + &mut data_len, + ))?; + + // data_len will be set in bytes, so divide by the size of the structure + let expected_len = data_len.div_ceil(STRUCT_SIZE); + + // Now allocate the Vec and set data_len to the real number of bytes allocated + let mut vec: Vec = Vec::with_capacity(expected_len); + data_len = vec.capacity() * STRUCT_SIZE; + + // We are also supposed to set ctl_name[5] to the number of structures we want + ctl_name[5] = expected_len.try_into().expect("expected_len too big"); + + // Call sysctl() again to put the result in the vec + check(sysctl_get( + ctl_name.as_ptr(), + ctl_name.len() as u32, + vec.as_mut_ptr() as *mut libc::c_void, + &mut data_len, + ))?; + + // If that was ok, we can set the actual length of the vec to whatever + // data_len was changed to, since that should now all be properly initialized data. + let true_len = data_len.div_ceil(STRUCT_SIZE); + vec.set_len(true_len); + + // Sort the procs by pid before using them + vec.sort_by_key(|p| p.p_pid); + Ok(vec) + } +} + +fn get_proc_args(pid: i32, what: i32) -> io::Result> { + unsafe { + let ctl_name = [CTL_KERN, KERN_PROC_ARGS, pid, what]; + + // First, try to figure out how large a buffer we need to allocate + // (calling with NULL just tells us that) + let mut data_len = 0; + check(sysctl_get( + ctl_name.as_ptr(), + ctl_name.len() as u32, + ptr::null_mut(), + &mut data_len, + ))?; + + // Now allocate the Vec and set data_len to the real number of bytes allocated + let mut vec: Vec = Vec::with_capacity(data_len); + data_len = vec.capacity(); + + // Call sysctl() again to put the result in the vec + check(sysctl_get( + ctl_name.as_ptr(), + ctl_name.len() as u32, + vec.as_mut_ptr() as *mut libc::c_void, + &mut data_len, + ))?; + + // If that was ok, we can set the actual length of the vec to whatever + // data_len was changed to, since that should now all be properly initialized data. + vec.set_len(data_len); + + // On OpenBSD we have to do an extra step, because it fills the buffer with pointers to the + // strings first, even though the strings are within the buffer as well. + #[cfg(target_os = "openbsd")] + let vec = { + use std::ffi::CStr; + + // Set up some bounds checking. We assume there will be some pointers at the base until + // we reach NULL, but we want to make sure we only ever read data within the range of + // min_ptr..max_ptr. + let ptrs = vec.as_ptr() as *const *const u8; + let min_ptr = vec.as_ptr() as *const u8; + let max_ptr = vec.as_ptr().add(vec.len()) as *const u8; + let max_index: isize = (vec.len() / mem::size_of::<*const u8>()) + .try_into() + .expect("too big for isize"); + + let mut new_vec = Vec::with_capacity(vec.len()); + for index in 0..max_index { + let ptr = ptrs.offset(index); + if *ptr == ptr::null() { + break; + } else { + // Make sure it's within the bounds of the buffer + assert!( + *ptr >= min_ptr && *ptr < max_ptr, + "pointer out of bounds of the buffer returned by sysctl()" + ); + // Also bounds-check the C strings, to make sure we don't overrun the buffer + new_vec.extend( + CStr::from_bytes_until_nul(std::slice::from_raw_parts( + *ptr, + max_ptr.offset_from(*ptr) as usize, + )) + .expect("invalid C string") + .to_bytes_with_nul(), + ); + } + } + new_vec + }; + + Ok(vec) + } +} + +// For getting simple values from the sysctl interface +unsafe fn get_ctl(ctl_name: &[i32]) -> io::Result { + let mut value: MaybeUninit = MaybeUninit::uninit(); + let mut value_len = mem::size_of_val(&value); + check(sysctl_get( + ctl_name.as_ptr(), + ctl_name.len() as u32, + value.as_mut_ptr() as *mut libc::c_void, + &mut value_len, + ))?; + Ok(value.assume_init()) +} + +fn get_pagesize() -> io::Result { + // not in libc for some reason + const HW_PAGESIZE: i32 = 7; + unsafe { get_ctl(&[CTL_HW, HW_PAGESIZE]) } +} + +impl ProcessInfo { + /// PID of process + pub fn pid(&self) -> i32 { + self.pid + } + + /// Parent PID of process + pub fn ppid(&self) -> i32 { + self.ppid + } + + /// Name of command + pub fn name(&self) -> String { + self.argv + .split(|b| *b == 0) + .next() + .map(String::from_utf8_lossy) + .unwrap_or_default() + .into_owned() + } + + /// Full name of command, with arguments + pub fn command(&self) -> String { + if let Some(last_nul) = self.argv.iter().rposition(|b| *b == 0) { + // The command string is NUL separated + // Take the string up to the last NUL, then replace the NULs with spaces + String::from_utf8_lossy(&self.argv[0..last_nul]).replace("\0", " ") + } else { + "".into() + } + } + + /// Get the status of the process + pub fn status(&self) -> String { + // see sys/proc.h (OpenBSD), sys/lwp.h (NetBSD) + // the names given here are the NetBSD ones, starting with LS*, but the OpenBSD ones are + // the same, just starting with S* instead + match self.stat { + 1 /* LSIDL */ => "", + 2 /* LSRUN */ => "Waiting", + 3 /* LSSLEEP */ => "Sleeping", + 4 /* LSSTOP */ => "Stopped", + 5 /* LSZOMB */ => "Zombie", + #[cfg(target_os = "openbsd")] // removed in NetBSD + 6 /* LSDEAD */ => "Dead", + 7 /* LSONPROC */ => "Running", + #[cfg(target_os = "netbsd")] // doesn't exist in OpenBSD + 8 /* LSSUSPENDED */ => "Suspended", + _ => "Unknown", + } + .into() + } + + /// CPU usage as a percent of total + pub fn cpu_usage(&self) -> f64 { + self.percent_cpu + } + + /// Memory size in number of bytes + pub fn mem_size(&self) -> u64 { + self.mem_resident + } + + /// Virtual memory size in bytes + pub fn virtual_size(&self) -> u64 { + self.mem_virtual + } +}