Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce ProcessRefreshKind::Thread #1436

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
26 changes: 23 additions & 3 deletions src/common/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ impl System {
self.inner.refresh_cpu_specifics(refresh_kind)
}

/// Gets all processes and updates their information.
/// Gets all processes and updates their information, along with all the threads/tasks each process has.
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
///
/// It does the same as:
///
Expand All @@ -276,7 +276,8 @@ impl System {
/// .with_memory()
/// .with_cpu()
/// .with_disk_usage()
/// .with_exe(UpdateKind::OnlyIfNotSet),
/// .with_exe(UpdateKind::OnlyIfNotSet)
/// .with_thread(),
/// );
/// ```
///
Expand All @@ -287,6 +288,11 @@ impl System {
/// ⚠️ On Linux, `sysinfo` keeps the `stat` files open by default. You can change this behaviour
/// by using [`set_open_files_limit`][crate::set_open_files_limit].
///
/// ⚠️ On Linux, if you dont need the threads/tasks of each process, you can use
/// `refresh_processes_specifics` with `ProcessRefreshKind::everything().without_threads()`.
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
/// Refreshesing all processes and their threads can be quite expensive. For more information
/// see [`ProcessRefreshKind`].
///
/// Example:
///
/// ```no_run
Expand All @@ -307,7 +313,8 @@ impl System {
.with_memory()
.with_cpu()
.with_disk_usage()
.with_exe(UpdateKind::OnlyIfNotSet),
.with_exe(UpdateKind::OnlyIfNotSet)
.with_thread(),
)
}

Expand Down Expand Up @@ -1818,6 +1825,16 @@ pub enum ProcessesToUpdate<'a> {
/// the information won't be retrieved if the information is accessible without needing
/// extra computation.
///
/// ⚠️ ** Linux Specific ** ⚠️
/// When using `ProcessRefreshKind::everything()`, in linux we will fetch all relevant
/// information from `/proc/<pid>/` as well as all the information from `/proc/<pid>/task/<tid>/`
/// dirs. This makes the refresh mechanism a lot slower depending on the number of threads
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
/// each process has.
///
/// If you dont care about threads information, use `ProcessRefreshKind::everything().without_thread()`
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
/// as much as possible.
///
/// In windows, this will not have any effect.
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
/// ```
/// use sysinfo::{ProcessesToUpdate, ProcessRefreshKind, System};
///
Expand Down Expand Up @@ -1848,6 +1865,7 @@ pub struct ProcessRefreshKind {
environ: UpdateKind,
cmd: UpdateKind,
exe: UpdateKind,
thread: bool,
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
}

impl ProcessRefreshKind {
Expand Down Expand Up @@ -1887,6 +1905,7 @@ impl ProcessRefreshKind {
environ: UpdateKind::OnlyIfNotSet,
cmd: UpdateKind::OnlyIfNotSet,
exe: UpdateKind::OnlyIfNotSet,
thread: true,
}
}

Expand Down Expand Up @@ -1929,6 +1948,7 @@ It will retrieve the following information:
);
impl_get_set!(ProcessRefreshKind, cmd, with_cmd, without_cmd, UpdateKind);
impl_get_set!(ProcessRefreshKind, exe, with_exe, without_exe, UpdateKind);
impl_get_set!(ProcessRefreshKind, thread, with_thread, without_thread);
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
}

/// Used to determine what you want to refresh specifically on the [`Cpu`] type.
Expand Down
31 changes: 20 additions & 11 deletions src/unix/linux/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ fn get_all_pid_entries(
parent_pid: Option<Pid>,
entry: DirEntry,
data: &mut Vec<ProcAndTasks>,
enable_thread_stats: bool,
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
) -> Option<Pid> {
let Ok(file_type) = entry.file_type() else {
return None;
Expand All @@ -678,17 +679,25 @@ fn get_all_pid_entries(
let name = name?;
let pid = Pid::from(usize::from_str(name.to_str()?).ok()?);

let tasks_dir = Path::join(&entry, "task");

let tasks = if let Ok(entries) = fs::read_dir(tasks_dir) {
let mut tasks = HashSet::new();
for task in entries
.into_iter()
.filter_map(|entry| get_all_pid_entries(Some(name), Some(pid), entry.ok()?, data))
{
tasks.insert(task);
let tasks = if enable_thread_stats {
let tasks_dir = Path::join(&entry, "task");
if let Ok(entries) = fs::read_dir(tasks_dir) {
let mut tasks = HashSet::new();
for task in entries.into_iter().filter_map(|entry| {
get_all_pid_entries(
Some(name),
Some(pid),
entry.ok()?,
data,
enable_thread_stats,
)
}) {
tasks.insert(task);
}
Some(tasks)
} else {
None
}
Some(tasks)
} else {
None
};
Expand Down Expand Up @@ -773,7 +782,7 @@ pub(crate) fn refresh_procs(
.map(|entry| {
let Ok(entry) = entry else { return Vec::new() };
let mut entries = Vec::new();
get_all_pid_entries(None, None, entry, &mut entries);
get_all_pid_entries(None, None, entry, &mut entries, refresh_kind.thread());
entries
})
.flatten()
Expand Down
133 changes: 102 additions & 31 deletions tests/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#![cfg(feature = "system")]

use bstr::ByteSlice;
use std::{sync::mpsc, time::Duration};
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System, UpdateKind};

macro_rules! start_proc {
Expand Down Expand Up @@ -411,11 +412,103 @@ fn test_refresh_process_doesnt_remove() {

// Checks that `refresh_processes` is adding and removing task.
#[test]
fn test_refresh_tasks() {
// Skip if unsupported.
sigsegved marked this conversation as resolved.
Show resolved Hide resolved
if !sysinfo::IS_SUPPORTED_SYSTEM || cfg!(feature = "apple-sandbox") {
return;
}

// 1) Spawn a thread that waits on a channel, so we control when it exits.
let task_name = "controlled_test_thread";
let (tx, rx) = mpsc::channel::<()>();

std::thread::Builder::new()
.name(task_name.to_string())
.spawn(move || {
// Wait until the main thread signals we can exit.
let _ = rx.recv();
})
.unwrap();

let pid = Pid::from_u32(std::process::id() as _);
let mut sys = System::new();

// Wait until the new thread shows up in the process/tasks list.
// We do a short loop and check each time by refreshing processes.
const MAX_POLLS: usize = 20;
const POLL_INTERVAL: Duration = Duration::from_millis(100);

for _ in 0..MAX_POLLS {
println!("Waiting for thread start...");
sys.refresh_processes(ProcessesToUpdate::All, /*refresh_users=*/ false);

// Check if our thread is present in two ways:
// (a) via parent's tasks
// (b) by exact name
let parent_proc = sys.process(pid);
let tasks_contain_thread = parent_proc
.and_then(|p| p.tasks())
.map(|tids| {
tids.iter().any(|tid| {
sys.process(*tid)
.map(|t| t.name() == task_name)
.unwrap_or(false)
})
})
.unwrap_or(false);

let by_exact_name_exists = sys
.processes_by_exact_name(task_name.as_ref())
.next()
.is_some();

if tasks_contain_thread && by_exact_name_exists {
// We confirmed the thread is now visible
break;
}
std::thread::sleep(POLL_INTERVAL);
}

// 3) Signal the thread to exit.
drop(tx);

// 4) Wait until the thread is gone from the system’s process/tasks list.
for _ in 0..MAX_POLLS {
println!("Waiting for thread stop...");
sys.refresh_processes(ProcessesToUpdate::All, /*refresh_users=*/ true);

let parent_proc = sys.process(pid as sysinfo::Pid);
let tasks_contain_thread = parent_proc
.and_then(|p| p.tasks())
.map(|tids| {
tids.iter().any(|tid| {
sys.process(*tid)
.map(|t| t.name() == task_name)
.unwrap_or(false)
})
})
.unwrap_or(false);

let by_exact_name_exists = sys
.processes_by_exact_name(task_name.as_ref())
.next()
.is_some();

// If it's gone from both checks, we're good.
if !tasks_contain_thread && !by_exact_name_exists {
break;
}
std::thread::sleep(POLL_INTERVAL);
}
}

// Checks that `RefreshKind::Thread` when disabled doesnt get thread information.
#[test]
#[cfg(all(
any(target_os = "linux", target_os = "android"),
not(feature = "unknown-ci")
))]
fn test_refresh_tasks() {
fn test_refresh_thread() {
if !sysinfo::IS_SUPPORTED_SYSTEM || cfg!(feature = "apple-sandbox") {
return;
}
Expand All @@ -429,38 +522,16 @@ fn test_refresh_tasks() {

let pid = Pid::from_u32(std::process::id() as _);

// Checks that the task is listed as it should.
// Refresh everything but threads.
let mut s = System::new();
s.refresh_processes(ProcessesToUpdate::All, false);

assert!(s
.process(pid)
.unwrap()
.tasks()
.map(|tasks| tasks.iter().any(|task_pid| s
.process(*task_pid)
.map(|task| task.name() == task_name)
.unwrap_or(false)))
.unwrap_or(false));
assert!(s
.processes_by_exact_name(task_name.as_ref())
.next()
.is_some());

// Let's give some time to the system to clean up...
std::thread::sleep(std::time::Duration::from_secs(2));

s.refresh_processes(ProcessesToUpdate::All, true);
s.refresh_processes_specifics(
ProcessesToUpdate::All,
false,
ProcessRefreshKind::everything().without_thread(),
);

assert!(!s
.process(pid)
.unwrap()
.tasks()
.map(|tasks| tasks.iter().any(|task_pid| s
.process(*task_pid)
.map(|task| task.name() == task_name)
.unwrap_or(false)))
.unwrap_or(false));
// Check that we have an empty thread list.
assert!(s.process(pid).unwrap().tasks().is_none());
assert!(s
.processes_by_exact_name(task_name.as_ref())
.next()
Expand Down
Loading