Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions native/resource-monitor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,35 @@ impl Collector {
self.cpu_baseline_refreshed_at = Some(Instant::now());
}

// Terminal polling asks for this every second while a shell is open. On
// Linux one read of /proc/<pid>/stat has everything; sysinfo also opens
// /proc/<pid>/status and builds a full record per process, ~3x the CPU.
#[cfg(target_os = "linux")]
fn process_table(&self) -> Vec<ProcessTableEntry> {
use std::io::Read;

let Ok(entries) = std::fs::read_dir("/proc") else {
return Vec::new();
};
let mut buffer = Vec::with_capacity(512);
let mut processes = entries
.filter_map(|entry| {
let entry = entry.ok()?;
let pid = entry.file_name().to_str()?.parse::<u32>().ok()?;
buffer.clear();
std::fs::File::open(entry.path().join("stat"))
.ok()?
.read_to_end(&mut buffer)
.ok()?;
let (name, ppid) = parse_proc_stat(&buffer)?;
Some(ProcessTableEntry { pid, ppid, name })
})
.collect::<Vec<_>>();
processes.sort_by_key(|process| process.pid);
processes
}

#[cfg(not(target_os = "linux"))]
fn process_table(&self) -> Vec<ProcessTableEntry> {
// A separate System preserves snapshot CPU baselines.
let mut process_table_system = System::new();
Expand Down Expand Up @@ -575,6 +604,24 @@ fn process_refresh_kind() -> ProcessRefreshKind {
.without_tasks()
}

/// Name and parent pid from `/proc/<pid>/stat`: `pid (comm) state ppid ...`.
/// The name may itself contain spaces and parentheses, so it ends at the last `)`.
#[cfg(any(target_os = "linux", test))]
fn parse_proc_stat(stat: &[u8]) -> Option<(String, u32)> {
let open = stat.iter().position(|&byte| byte == b'(')?;
let close = stat.iter().rposition(|&byte| byte == b')')?;
let name = stat.get(open + 1..close)?;
let mut fields = stat.get(close + 1..)?.split(|&byte| byte == b' ');
fields.next()?;
fields.next()?;
let ppid = std::str::from_utf8(fields.next()?).ok()?.parse().ok()?;
let name = truncate_utf8(
String::from_utf8_lossy(name).into_owned(),
MAX_PROCESS_NAME_BYTES,
);
Some((name, ppid))
}

fn inaccessible_process_count(selected: usize, materialized: usize) -> usize {
selected.saturating_sub(materialized)
}
Expand Down Expand Up @@ -1338,6 +1385,43 @@ mod tests {
assert!(truncated.is_char_boundary(truncated.len()));
}

#[test]
fn parses_proc_stat_names_with_spaces_and_parentheses() {
assert_eq!(
parse_proc_stat(b"4242 (tmux: server) S 1 4242 4242 0 -1 4194560 3101"),
Some(("tmux: server".to_owned(), 1))
);
assert_eq!(
parse_proc_stat(b"77 (a) b (c)) R 4242 77 77 34816 77 4194304"),
Some(("a) b (c)".to_owned(), 4242))
);
assert_eq!(parse_proc_stat(b"77 (truncated"), None);
}

#[cfg(target_os = "linux")]
#[test]
fn linux_process_table_reports_own_process_like_sysinfo() {
let collector = Collector::new();
let mut system = System::new();
system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing().without_tasks(),
);
let own_pid = std::process::id();
let table = collector.process_table();
let own = table
.iter()
.find(|process| process.pid == own_pid)
.expect("own process");
let expected = system
.process(Pid::from_u32(own_pid))
.expect("sysinfo own process");
assert_eq!(own.ppid, expected.parent().map(Pid::as_u32).unwrap_or(0));
assert_eq!(own.name, expected.name().to_string_lossy());
assert!(table.windows(2).all(|pair| pair[0].pid < pair[1].pid));
}

#[test]
fn refreshes_commands_without_enumerating_linux_tasks() {
let refresh_kind = process_refresh_kind();
Expand Down
Loading