Skip to content

Commit 3700d8d

Browse files
authored
Merge pull request #59 from posit-dev/feature/resource-usage-lean
Improve efficiency of resource usage monitor
2 parents 6a425cb + f3b1162 commit 3700d8d

9 files changed

Lines changed: 1083 additions & 62 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/kcserver/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "kcserver"
3-
version = "0.1.62"
3+
version = "0.1.63"
44
rust-version.workspace = true
55
edition.workspace = true
66
license.workspace = true
@@ -76,5 +76,6 @@ features = [
7676
"Win32_System_IO",
7777
"Win32_System_Pipes",
7878
"Win32_System_Threading",
79+
"Win32_System_Diagnostics_ToolHelp",
7980
"Win32_UI_WindowsAndMessaging"
8081
]

crates/kcserver/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub mod jupyter_messages;
1818
pub mod kernel_connection;
1919
pub mod kernel_session;
2020
pub mod kernel_state;
21+
#[cfg(target_os = "linux")]
22+
pub mod proc_stat;
23+
pub mod process_tree;
2124
pub mod registration_file;
2225
pub mod registration_socket;
2326
pub mod resource_monitor;

crates/kcserver/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ mod jupyter_messages;
2626
mod kernel_connection;
2727
mod kernel_session;
2828
mod kernel_state;
29+
#[cfg(target_os = "linux")]
30+
mod proc_stat;
31+
mod process_tree;
2932
mod registration_file;
3033
mod registration_socket;
3134
mod resource_monitor;
@@ -378,7 +381,7 @@ async fn main() {
378381
| \ / | |/ |/ | / |/ \ / \_/ | |/
379382
| \_/\_/|_/|__/|__/|_/\___/| |_/\__/ |_/|__/
380383
A Jupyter Kernel supervisor. Version {}.
381-
Copyright (c) 2025, Posit Software PBC. All rights reserved.
384+
Copyright (c) 2026, Posit Software PBC. All rights reserved.
382385
"#,
383386
env!("CARGO_PKG_VERSION")
384387
);

crates/kcserver/src/proc_stat.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//
2+
// proc_stat.rs
3+
//
4+
// Copyright (C) 2026 Posit Software, PBC. All rights reserved.
5+
// Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
6+
//
7+
8+
//! Linux-specific utilities for parsing /proc filesystem.
9+
//!
10+
//! This module provides shared utilities for parsing /proc/[pid]/stat files,
11+
//! used by both the process tree enumeration and CPU tracking code.
12+
13+
#![cfg(target_os = "linux")]
14+
15+
use std::fs;
16+
17+
/// Parsed fields from /proc/[pid]/stat
18+
#[derive(Debug, Clone)]
19+
pub struct ProcStat {
20+
/// Parent process ID (field 4, index 1 after comm)
21+
pub ppid: u32,
22+
/// Process group ID (field 5, index 2 after comm)
23+
pub pgid: u32,
24+
/// User mode CPU time in jiffies (field 14, index 11 after comm)
25+
pub utime: u64,
26+
/// Kernel mode CPU time in jiffies (field 15, index 12 after comm)
27+
pub stime: u64,
28+
}
29+
30+
impl ProcStat {
31+
/// Total CPU time (utime + stime)
32+
pub fn cpu_time(&self) -> u64 {
33+
self.utime + self.stime
34+
}
35+
}
36+
37+
/// Parse /proc/[pid]/stat to extract process information.
38+
///
39+
/// The stat file format is: `pid (comm) state ppid pgrp session tty_nr tpgid flags
40+
/// minflt cminflt majflt cmajflt utime stime cutime cstime ...`
41+
///
42+
/// Note: `comm` can contain spaces and parentheses, so we find the last ')' to
43+
/// reliably parse the remaining fields.
44+
pub fn parse_proc_stat(pid: u32) -> Option<ProcStat> {
45+
let stat_path = format!("/proc/{}/stat", pid);
46+
let stat_content = fs::read_to_string(stat_path).ok()?;
47+
48+
// comm can contain spaces and parens, so find the last ')'
49+
let last_paren = stat_content.rfind(')')?;
50+
let fields_after_comm = stat_content.get(last_paren + 2..)?; // Skip ") "
51+
let fields: Vec<&str> = fields_after_comm.split_whitespace().collect();
52+
53+
// fields[0] = state, fields[1] = ppid, fields[2] = pgrp, ...
54+
// fields[11] = utime, fields[12] = stime
55+
if fields.len() < 13 {
56+
return None;
57+
}
58+
59+
Some(ProcStat {
60+
ppid: fields[1].parse().ok()?,
61+
pgid: fields[2].parse().ok()?,
62+
utime: fields[11].parse().ok()?,
63+
stime: fields[12].parse().ok()?,
64+
})
65+
}
66+
67+
/// Read total CPU time from /proc/stat (sum of all jiffies across all CPUs).
68+
///
69+
/// The first line of /proc/stat is:
70+
/// `cpu user nice system idle iowait irq softirq steal guest guest_nice`
71+
///
72+
/// We sum all these values to get total CPU time.
73+
pub fn read_total_cpu_time() -> u64 {
74+
let Ok(content) = fs::read_to_string("/proc/stat") else {
75+
return 0;
76+
};
77+
78+
let Some(cpu_line) = content.lines().next() else {
79+
return 0;
80+
};
81+
82+
if !cpu_line.starts_with("cpu ") {
83+
return 0;
84+
}
85+
86+
// Sum all the values (skip "cpu" label)
87+
cpu_line
88+
.split_whitespace()
89+
.skip(1)
90+
.filter_map(|s| s.parse::<u64>().ok())
91+
.sum()
92+
}
93+
94+
/// Count the number of CPUs by counting cpu[N] lines in /proc/stat.
95+
pub fn count_cpus() -> usize {
96+
let Ok(content) = fs::read_to_string("/proc/stat") else {
97+
return 1;
98+
};
99+
100+
// Count lines starting with "cpu" followed by a digit (cpu0, cpu1, etc.)
101+
content
102+
.lines()
103+
.filter(|line| {
104+
line.starts_with("cpu")
105+
&& line
106+
.chars()
107+
.nth(3)
108+
.map(|c| c.is_ascii_digit())
109+
.unwrap_or(false)
110+
})
111+
.count()
112+
.max(1)
113+
}
114+
115+
#[cfg(test)]
116+
mod tests {
117+
use super::*;
118+
119+
#[test]
120+
fn test_read_total_cpu_time() {
121+
// Should return a non-zero value on a real Linux system
122+
let cpu_time = read_total_cpu_time();
123+
// Just verify it doesn't panic and returns something reasonable
124+
assert!(cpu_time > 0 || cfg!(not(target_os = "linux")));
125+
}
126+
127+
#[test]
128+
fn test_count_cpus() {
129+
let cpus = count_cpus();
130+
assert!(cpus >= 1);
131+
}
132+
133+
#[test]
134+
fn test_parse_proc_stat_current_process() {
135+
// Parse our own process's stat
136+
let pid = std::process::id();
137+
if let Some(stat) = parse_proc_stat(pid) {
138+
// Our parent PID should be non-zero
139+
assert!(stat.ppid > 0);
140+
// PGID should be set
141+
assert!(stat.pgid > 0);
142+
}
143+
// It's OK if this fails on non-Linux or in restricted environments
144+
}
145+
}

0 commit comments

Comments
 (0)