mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-09-21 08:47:53 +02:00
feature: add support for threads in linux (#1793)
* feature: add support for threads in linux * bump version too * only enable for linux for now * thread some things around * update changelog * add highlighting support * fmt and run schema * how did this get added * hmmm cfg in if seems to not work * fix updated fields * fixes * revert uptime rename * some cleanup * fix doc * oop
This commit is contained in:
parent
2578f20ce5
commit
3ff7977e6f
@ -20,6 +20,12 @@ That said, these are more guidelines rather than hardset rules, though the proje
|
||||
|
||||
---
|
||||
|
||||
## [0.12.0] - Unreleased
|
||||
|
||||
### Features
|
||||
|
||||
- [#1793](https://github.com/ClementTsang/bottom/pull/1793): Add support for threads in Linux.
|
||||
|
||||
## [0.11.1] - 2025-08-15
|
||||
|
||||
### Bug Fixes
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -145,7 +145,7 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bottom"
|
||||
version = "0.11.1"
|
||||
version = "0.12.0-nightly"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bottom"
|
||||
version = "0.11.1"
|
||||
version = "0.12.0-nightly"
|
||||
repository = "https://github.com/ClementTsang/bottom"
|
||||
license = "MIT"
|
||||
description = "A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows."
|
||||
|
@ -207,7 +207,10 @@
|
||||
"properties": {
|
||||
"columns": {
|
||||
"description": "A list of disk widget columns.",
|
||||
"type": "array",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/DiskColumn"
|
||||
}
|
||||
@ -751,6 +754,13 @@
|
||||
"items": {
|
||||
"$ref": "#/$defs/ProcColumn"
|
||||
}
|
||||
},
|
||||
"get_threads": {
|
||||
"description": "Whether to get process child threads.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -42,6 +42,7 @@ pub struct AppConfigFields {
|
||||
pub show_average_cpu: bool, // TODO: Unify this in CPU options
|
||||
pub use_current_cpu_total: bool,
|
||||
pub unnormalized_cpu: bool,
|
||||
pub get_process_threads: bool,
|
||||
pub use_basic_mode: bool,
|
||||
pub default_time_value: u64,
|
||||
pub time_interval: u64,
|
||||
|
@ -166,7 +166,7 @@ mod test {
|
||||
}
|
||||
|
||||
impl DataToCell<&'static str> for TestType {
|
||||
fn to_cell(
|
||||
fn to_cell_text(
|
||||
&self, _column: &&'static str, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
None
|
||||
|
@ -10,15 +10,29 @@ where
|
||||
H: ColumnHeader,
|
||||
{
|
||||
/// Given data, a column, and its corresponding width, return the string in
|
||||
/// the cell that will be displayed in the
|
||||
/// [`DataTable`](super::DataTable).
|
||||
fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;
|
||||
/// the cell that will be displayed in the [`super::DataTable`].
|
||||
fn to_cell_text(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;
|
||||
|
||||
/// Given a column, how to style a cell if one needs to override the default styling.
|
||||
///
|
||||
/// By default this just returns [`None`], deferring to the row or table styling.
|
||||
#[expect(
|
||||
unused_variables,
|
||||
reason = "The default implementation just returns `None`."
|
||||
)]
|
||||
fn style_cell(&self, column: &H, painter: &Painter) -> Option<tui::style::Style> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Apply styling to the generated [`Row`] of cells.
|
||||
///
|
||||
/// The default implementation just returns the `row` that is passed in.
|
||||
#[inline(always)]
|
||||
fn style_row<'a>(&self, row: Row<'a>, _painter: &Painter) -> Row<'a> {
|
||||
#[expect(
|
||||
unused_variables,
|
||||
reason = "The default implementation just returns an unstyled row."
|
||||
)]
|
||||
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
|
||||
row
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ use tui::{
|
||||
Frame,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Row, Table},
|
||||
widgets::{Block, Cell, Row, Table},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@ -217,9 +217,17 @@ where
|
||||
.iter()
|
||||
.zip(&self.state.calculated_widths)
|
||||
.filter_map(|(column, &width)| {
|
||||
data_row
|
||||
.to_cell(column.inner(), width)
|
||||
.map(|content| truncate_to_text(&content, width.get()))
|
||||
data_row.to_cell_text(column.inner(), width).map(|content| {
|
||||
let content = truncate_to_text(&content, width.get());
|
||||
|
||||
if let Some(style) =
|
||||
data_row.style_cell(column.inner(), painter)
|
||||
{
|
||||
Cell::new(content).style(style)
|
||||
} else {
|
||||
Cell::new(content)
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -360,7 +360,7 @@ mod test {
|
||||
}
|
||||
|
||||
impl DataToCell<ColumnType> for TestType {
|
||||
fn to_cell(
|
||||
fn to_cell_text(
|
||||
&self, _column: &ColumnType, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
None
|
||||
|
@ -68,7 +68,7 @@ impl Painter {
|
||||
}
|
||||
|
||||
/// Draws the process sort box.
|
||||
/// - `widget_id` represents the widget ID of the process widget itself.an
|
||||
/// - `widget_id` represents the widget ID of the process widget itself.
|
||||
fn draw_processes_table(
|
||||
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
|
@ -160,14 +160,13 @@ pub struct DataCollector {
|
||||
unnormalized_cpu: bool,
|
||||
use_current_cpu_total: bool,
|
||||
show_average_cpu: bool,
|
||||
get_process_threads: bool,
|
||||
|
||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
||||
last_list_collection_time: Instant,
|
||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
||||
should_refresh_list: bool,
|
||||
should_run_less_routine_tasks: bool,
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pid_mapping: HashMap<Pid, processes::PrevProcDetails>,
|
||||
prev_process_details: HashMap<Pid, processes::PrevProcDetails>,
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_idle: f64,
|
||||
#[cfg(target_os = "linux")]
|
||||
@ -187,25 +186,26 @@ pub struct DataCollector {
|
||||
gpus_total_mem: Option<u64>,
|
||||
}
|
||||
|
||||
const LIST_REFRESH_TIME: Duration = Duration::from_secs(60);
|
||||
const LESS_ROUTINE_TASK_TIME: Duration = Duration::from_secs(60);
|
||||
|
||||
impl DataCollector {
|
||||
pub fn new(filters: DataFilters) -> Self {
|
||||
// Initialize it to the past to force it to load on initialization.
|
||||
let now = Instant::now();
|
||||
let last_collection_time = now.checked_sub(LIST_REFRESH_TIME * 10).unwrap_or(now);
|
||||
let last_collection_time = now.checked_sub(LESS_ROUTINE_TASK_TIME * 10).unwrap_or(now);
|
||||
|
||||
DataCollector {
|
||||
data: Data::default(),
|
||||
sys: SysinfoSource::default(),
|
||||
#[cfg(target_os = "linux")]
|
||||
pid_mapping: HashMap::default(),
|
||||
prev_process_details: HashMap::default(),
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_idle: 0_f64,
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_non_idle: 0_f64,
|
||||
use_current_cpu_total: false,
|
||||
unnormalized_cpu: false,
|
||||
get_process_threads: false,
|
||||
last_collection_time,
|
||||
total_rx: 0,
|
||||
total_tx: 0,
|
||||
@ -222,30 +222,27 @@ impl DataCollector {
|
||||
gpu_pids: None,
|
||||
#[cfg(feature = "gpu")]
|
||||
gpus_total_mem: None,
|
||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
||||
last_list_collection_time: last_collection_time,
|
||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
||||
should_refresh_list: true,
|
||||
should_run_less_routine_tasks: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the check for updating things like lists of batteries, etc.
|
||||
/// Update the check for routine tasks like updating lists of batteries, cleanup, etc.
|
||||
/// This is useful for things that we don't want to update all the time.
|
||||
///
|
||||
/// Note this should be set back to false if `self.last_list_collection_time` is updated.
|
||||
#[inline]
|
||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
||||
fn update_refresh_list_check(&mut self) {
|
||||
fn run_less_routine_tasks(&mut self) {
|
||||
if self
|
||||
.data
|
||||
.collection_time
|
||||
.duration_since(self.last_list_collection_time)
|
||||
> LIST_REFRESH_TIME
|
||||
> LESS_ROUTINE_TASK_TIME
|
||||
{
|
||||
self.should_refresh_list = true;
|
||||
self.should_run_less_routine_tasks = true;
|
||||
}
|
||||
|
||||
if self.should_refresh_list {
|
||||
if self.should_run_less_routine_tasks {
|
||||
self.last_list_collection_time = self.data.collection_time;
|
||||
}
|
||||
}
|
||||
@ -266,6 +263,10 @@ impl DataCollector {
|
||||
self.show_average_cpu = show_average_cpu;
|
||||
}
|
||||
|
||||
pub fn set_get_process_threads(&mut self, get_process_threads: bool) {
|
||||
self.get_process_threads = get_process_threads;
|
||||
}
|
||||
|
||||
/// Refresh sysinfo data. We use sysinfo for the following data:
|
||||
/// - CPU usage
|
||||
/// - Memory usage
|
||||
@ -307,13 +308,13 @@ impl DataCollector {
|
||||
|
||||
// For Windows, sysinfo also handles the users list.
|
||||
#[cfg(target_os = "windows")]
|
||||
if self.should_refresh_list {
|
||||
if self.should_run_less_routine_tasks {
|
||||
self.sys.users.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
if self.widgets_to_harvest.use_temp {
|
||||
if self.should_refresh_list {
|
||||
if self.should_run_less_routine_tasks {
|
||||
self.sys.temps.refresh(true);
|
||||
}
|
||||
|
||||
@ -324,7 +325,7 @@ impl DataCollector {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if self.widgets_to_harvest.use_disk {
|
||||
if self.should_refresh_list {
|
||||
if self.should_run_less_routine_tasks {
|
||||
self.sys.disks.refresh(true);
|
||||
}
|
||||
|
||||
@ -341,10 +342,7 @@ impl DataCollector {
|
||||
pub fn update_data(&mut self) {
|
||||
self.data.collection_time = Instant::now();
|
||||
|
||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
||||
{
|
||||
self.update_refresh_list_check();
|
||||
}
|
||||
self.run_less_routine_tasks();
|
||||
|
||||
self.refresh_sysinfo_data();
|
||||
|
||||
@ -362,10 +360,8 @@ impl DataCollector {
|
||||
self.update_network_usage();
|
||||
self.update_disks();
|
||||
|
||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
||||
{
|
||||
self.should_refresh_list = false;
|
||||
}
|
||||
// Make sure to run this to refresh the setting.
|
||||
self.should_run_less_routine_tasks = false;
|
||||
|
||||
// Update times for future reference.
|
||||
self.last_collection_time = self.data.collection_time;
|
||||
@ -503,14 +499,14 @@ impl DataCollector {
|
||||
///
|
||||
/// If the battery manager is not initialized, it will attempt to initialize it if at least one battery is found.
|
||||
///
|
||||
/// This function also refreshes the list of batteries if `self.should_refresh_list` is true.
|
||||
/// This function also refreshes the list of batteries if `self.should_run_less_routine_tasks` is true.
|
||||
#[inline]
|
||||
#[cfg(feature = "battery")]
|
||||
fn update_batteries(&mut self) {
|
||||
let battery_manager = match &self.battery_manager {
|
||||
Some(manager) => {
|
||||
// Also check if we need to refresh the list of batteries.
|
||||
if self.should_refresh_list {
|
||||
if self.should_run_less_routine_tasks {
|
||||
let battery_list = manager
|
||||
.batteries()
|
||||
.map(|batteries| batteries.filter_map(Result::ok).collect::<Vec<_>>());
|
||||
|
@ -48,6 +48,36 @@ cfg_if! {
|
||||
|
||||
pub type Bytes = u64;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// The process entry "type".
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum ProcessType {
|
||||
/// A regular user process.
|
||||
#[default]
|
||||
Regular,
|
||||
|
||||
/// A kernel process.
|
||||
///
|
||||
/// TODO: Use <https://github.com/htop-dev/htop/commit/07496eafb0166aafd9c33a6a95e16bcbc64c34d4>?
|
||||
Kernel,
|
||||
|
||||
/// A thread spawned by a regular user process.
|
||||
ProcessThread,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl ProcessType {
|
||||
/// Returns `true` if this is a thread.
|
||||
pub fn is_thread(&self) -> bool {
|
||||
matches!(self, Self::ProcessThread)
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a kernel process.
|
||||
pub fn is_kernel(&self) -> bool {
|
||||
matches!(self, Self::Kernel)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProcessHarvest {
|
||||
/// The pid of the process.
|
||||
@ -60,6 +90,8 @@ pub struct ProcessHarvest {
|
||||
pub cpu_usage_percent: f32,
|
||||
|
||||
/// Memory usage as a percentage.
|
||||
///
|
||||
/// TODO: Maybe calculate this on usage? Store the total mem along with the vector of results.
|
||||
pub mem_usage_percent: f32,
|
||||
|
||||
/// Memory usage as bytes.
|
||||
@ -105,12 +137,18 @@ pub struct ProcessHarvest {
|
||||
pub gpu_mem: u64,
|
||||
|
||||
/// Gpu memory usage as percentage.
|
||||
///
|
||||
/// TODO: Maybe calculate this on usage? Store the total GPU mem along with the vector of results.
|
||||
#[cfg(feature = "gpu")]
|
||||
pub gpu_mem_percent: f32,
|
||||
|
||||
/// Gpu utilization as a percentage.
|
||||
#[cfg(feature = "gpu")]
|
||||
pub gpu_util: u32,
|
||||
|
||||
/// The process entry "type".
|
||||
#[cfg(target_os = "linux")]
|
||||
pub process_type: ProcessType,
|
||||
// TODO: Additional fields
|
||||
// pub rss_kb: u64,
|
||||
// pub virt_kb: u64,
|
||||
|
@ -9,14 +9,16 @@ use std::{
|
||||
};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use hashbrown::HashSet;
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use process::*;
|
||||
use sysinfo::ProcessStatus;
|
||||
|
||||
use super::{Pid, ProcessHarvest, UserTable, process_status_str};
|
||||
use crate::collection::{DataCollector, error::CollectionResult};
|
||||
use crate::collection::{DataCollector, error::CollectionResult, processes::ProcessType};
|
||||
|
||||
/// Maximum character length of a `/proc/<PID>/stat`` process name.
|
||||
/// Maximum character length of a `/proc/<PID>/stat` process name (the length is 16,
|
||||
/// but this includes a null terminator).
|
||||
///
|
||||
/// If it's equal or greater, then we instead refer to the command for the name.
|
||||
const MAX_STAT_NAME_LEN: usize = 15;
|
||||
|
||||
@ -132,6 +134,7 @@ fn get_linux_cpu_usage(
|
||||
|
||||
fn read_proc(
|
||||
prev_proc: &PrevProcDetails, process: Process, args: ReadProcArgs, user_table: &mut UserTable,
|
||||
thread_parent: Option<Pid>,
|
||||
) -> CollectionResult<(ProcessHarvest, u64)> {
|
||||
let Process {
|
||||
pid: _,
|
||||
@ -147,7 +150,8 @@ fn read_proc(
|
||||
cpu_fraction,
|
||||
total_memory,
|
||||
time_difference_in_secs,
|
||||
uptime,
|
||||
system_uptime,
|
||||
get_process_threads: _,
|
||||
} = args;
|
||||
|
||||
let process_state_char = stat.state;
|
||||
@ -162,7 +166,13 @@ fn read_proc(
|
||||
prev_proc.cpu_time,
|
||||
use_current_cpu_total,
|
||||
);
|
||||
let parent_pid = Some(stat.ppid);
|
||||
|
||||
let (parent_pid, process_type) = if let Some(thread_parent) = thread_parent {
|
||||
(Some(thread_parent), ProcessType::ProcessThread)
|
||||
} else {
|
||||
(Some(stat.ppid), ProcessType::Regular)
|
||||
};
|
||||
|
||||
let mem_usage = stat.rss_bytes();
|
||||
let mem_usage_percent = (mem_usage as f64 / total_memory as f64 * 100.0) as f32;
|
||||
let virtual_mem = stat.vsize;
|
||||
@ -202,7 +212,9 @@ fn read_proc(
|
||||
if ticks_per_sec == 0 {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
Duration::from_secs(uptime.saturating_sub(stat.start_time / ticks_per_sec as u64))
|
||||
Duration::from_secs(
|
||||
system_uptime.saturating_sub(stat.start_time / ticks_per_sec as u64),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Duration::ZERO
|
||||
@ -222,12 +234,15 @@ fn read_proc(
|
||||
|
||||
// We're only interested in the executable part, not the file path (part of command),
|
||||
// so strip everything but the command name if needed.
|
||||
let last_part = match first_part.rsplit_once('/') {
|
||||
let command = match first_part.rsplit_once('/') {
|
||||
Some((_, last)) => last,
|
||||
None => first_part,
|
||||
};
|
||||
|
||||
last_part.to_string()
|
||||
// TODO: Needed as some processes have stuff like "systemd-userwork: waiting..."
|
||||
// command.trim_end_matches(':').to_string()
|
||||
|
||||
command.to_string()
|
||||
} else {
|
||||
truncated_name
|
||||
};
|
||||
@ -263,6 +278,7 @@ fn read_proc(
|
||||
gpu_mem_percent: 0.0,
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu_util: 0,
|
||||
process_type,
|
||||
},
|
||||
new_process_times,
|
||||
))
|
||||
@ -276,6 +292,7 @@ pub(crate) struct PrevProc<'a> {
|
||||
pub(crate) struct ProcHarvestOptions {
|
||||
pub use_current_cpu_total: bool,
|
||||
pub unnormalized_cpu: bool,
|
||||
pub get_process_threads: bool,
|
||||
}
|
||||
|
||||
fn is_str_numeric(s: &str) -> bool {
|
||||
@ -285,12 +302,13 @@ fn is_str_numeric(s: &str) -> bool {
|
||||
/// General args to keep around for reading proc data.
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct ReadProcArgs {
|
||||
pub(crate) use_current_cpu_total: bool,
|
||||
pub(crate) cpu_usage: f64,
|
||||
pub(crate) cpu_fraction: f64,
|
||||
pub(crate) total_memory: u64,
|
||||
pub(crate) time_difference_in_secs: u64,
|
||||
pub(crate) uptime: u64,
|
||||
pub use_current_cpu_total: bool,
|
||||
pub cpu_usage: f64,
|
||||
pub cpu_fraction: f64,
|
||||
pub total_memory: u64,
|
||||
pub time_difference_in_secs: u64,
|
||||
pub system_uptime: u64,
|
||||
pub get_process_threads: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn linux_process_data(
|
||||
@ -304,13 +322,15 @@ pub(crate) fn linux_process_data(
|
||||
let proc_harvest_options = ProcHarvestOptions {
|
||||
use_current_cpu_total: collector.use_current_cpu_total,
|
||||
unnormalized_cpu: collector.unnormalized_cpu,
|
||||
get_process_threads: collector.get_process_threads,
|
||||
};
|
||||
let pid_mapping = &mut collector.pid_mapping;
|
||||
let prev_process_details = &mut collector.prev_process_details;
|
||||
let user_table = &mut collector.user_table;
|
||||
|
||||
let ProcHarvestOptions {
|
||||
use_current_cpu_total,
|
||||
unnormalized_cpu,
|
||||
get_process_threads: get_threads,
|
||||
} = proc_harvest_options;
|
||||
|
||||
let PrevProc {
|
||||
@ -333,9 +353,13 @@ pub(crate) fn linux_process_data(
|
||||
cpu_usage /= num_processors;
|
||||
}
|
||||
|
||||
let mut pids_to_clear: HashSet<Pid> = pid_mapping.keys().cloned().collect();
|
||||
// TODO: Could maybe use a double buffer hashmap to avoid allocating this each time?
|
||||
// e.g. we swap which is prev and which is new.
|
||||
let mut seen_pids: HashSet<Pid> = HashSet::new();
|
||||
|
||||
// Note this will only return PIDs of _processes_, not threads. You can get those from /proc/<PID>/task though.
|
||||
let pids = fs::read_dir("/proc")?.flatten().filter_map(|dir| {
|
||||
// Need to filter out non-PID entries.
|
||||
if is_str_numeric(dir.file_name().to_string_lossy().trim()) {
|
||||
Some(dir.path())
|
||||
} else {
|
||||
@ -349,20 +373,25 @@ pub(crate) fn linux_process_data(
|
||||
cpu_fraction,
|
||||
total_memory,
|
||||
time_difference_in_secs,
|
||||
uptime: sysinfo::System::uptime(),
|
||||
system_uptime: sysinfo::System::uptime(),
|
||||
get_process_threads: get_threads,
|
||||
};
|
||||
|
||||
// TODO: Maybe pre-allocate these buffers in the future w/ routine cleanup.
|
||||
let mut buffer = String::new();
|
||||
let mut process_threads_to_check = HashMap::new();
|
||||
|
||||
let process_vector: Vec<ProcessHarvest> = pids
|
||||
let mut process_vector: Vec<ProcessHarvest> = pids
|
||||
.filter_map(|pid_path| {
|
||||
if let Ok(process) = Process::from_path(pid_path, &mut buffer) {
|
||||
if let Ok((process, threads)) =
|
||||
Process::from_path(pid_path, &mut buffer, args.get_process_threads)
|
||||
{
|
||||
let pid = process.pid;
|
||||
let prev_proc_details = pid_mapping.entry(pid).or_default();
|
||||
let prev_proc_details = prev_process_details.entry(pid).or_default();
|
||||
|
||||
#[cfg_attr(not(feature = "gpu"), expect(unused_mut))]
|
||||
if let Ok((mut process_harvest, new_process_times)) =
|
||||
read_proc(prev_proc_details, process, args, user_table)
|
||||
read_proc(prev_proc_details, process, args, user_table, None)
|
||||
{
|
||||
#[cfg(feature = "gpu")]
|
||||
if let Some(gpus) = &collector.gpu_pids {
|
||||
@ -384,7 +413,11 @@ pub(crate) fn linux_process_data(
|
||||
prev_proc_details.total_read_bytes = process_harvest.total_read;
|
||||
prev_proc_details.total_write_bytes = process_harvest.total_write;
|
||||
|
||||
pids_to_clear.remove(&pid);
|
||||
if !threads.is_empty() {
|
||||
process_threads_to_check.insert(pid, threads);
|
||||
}
|
||||
|
||||
seen_pids.insert(pid);
|
||||
return Some(process_harvest);
|
||||
}
|
||||
}
|
||||
@ -393,10 +426,37 @@ pub(crate) fn linux_process_data(
|
||||
})
|
||||
.collect();
|
||||
|
||||
pids_to_clear.iter().for_each(|pid| {
|
||||
pid_mapping.remove(pid);
|
||||
});
|
||||
// Get thread data.
|
||||
for (pid, tid_paths) in process_threads_to_check {
|
||||
for tid_path in tid_paths {
|
||||
if let Ok((process, _)) = Process::from_path(tid_path, &mut buffer, false) {
|
||||
let tid = process.pid;
|
||||
let prev_proc_details = prev_process_details.entry(tid).or_default();
|
||||
|
||||
if let Ok((process_harvest, new_process_times)) =
|
||||
read_proc(prev_proc_details, process, args, user_table, Some(pid))
|
||||
{
|
||||
prev_proc_details.cpu_time = new_process_times;
|
||||
prev_proc_details.total_read_bytes = process_harvest.total_read;
|
||||
prev_proc_details.total_write_bytes = process_harvest.total_write;
|
||||
|
||||
seen_pids.insert(tid);
|
||||
process_vector.push(process_harvest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up values we don't care about anymore.
|
||||
prev_process_details.retain(|pid, _| seen_pids.contains(pid));
|
||||
|
||||
// Occasional garbage collection.
|
||||
if collector.should_run_less_routine_tasks {
|
||||
prev_process_details.shrink_to_fit();
|
||||
}
|
||||
|
||||
// TODO: This might be more efficient to just separate threads into their own list, but for now this works so it
|
||||
// fits with existing code.
|
||||
Ok(process_vector)
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ use rustix::{
|
||||
path::Arg,
|
||||
};
|
||||
|
||||
use crate::collection::processes::Pid;
|
||||
use crate::collection::processes::{Pid, linux::is_str_numeric};
|
||||
|
||||
static PAGESIZE: OnceLock<u64> = OnceLock::new();
|
||||
|
||||
@ -26,8 +26,9 @@ fn next_part<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Result<&'a str, io
|
||||
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))
|
||||
}
|
||||
|
||||
/// A wrapper around the data in `/proc/<PID>/stat`. For documentation, see
|
||||
/// [here](https://man7.org/linux/man-pages/man5/proc.5.html).
|
||||
/// A wrapper around the data in `/proc/<PID>/stat`. For documentation, see:
|
||||
/// - <https://manpages.ubuntu.com/manpages/noble/man5/proc_pid_stat.5.html>
|
||||
/// - <https://man7.org/linux/man-pages/man5/proc_pid_status.5.html>
|
||||
///
|
||||
/// Note this does not necessarily get all fields, only the ones we use in
|
||||
/// bottom.
|
||||
@ -62,7 +63,8 @@ pub(crate) struct Stat {
|
||||
|
||||
impl Stat {
|
||||
/// Get process stats from a file; this assumes the file is located at
|
||||
/// `/proc/<PID>/stat`.
|
||||
/// `/proc/<PID>/stat`. For documentation, see
|
||||
/// [here](https://manpages.ubuntu.com/manpages/noble/man5/proc_pid_stat.5.html) as a reference.
|
||||
fn from_file(mut f: File, buffer: &mut String) -> anyhow::Result<Stat> {
|
||||
// Since this is just one line, we can read it all at once. However, since it
|
||||
// (technically) might have non-utf8 characters, we can't just use read_to_string.
|
||||
@ -229,12 +231,15 @@ impl Process {
|
||||
/// will be discarded quickly.
|
||||
///
|
||||
/// This takes in a buffer to avoid allocs; this function will clear the buffer.
|
||||
pub(crate) fn from_path(pid_path: PathBuf, buffer: &mut String) -> anyhow::Result<Process> {
|
||||
#[inline]
|
||||
pub(crate) fn from_path(
|
||||
pid_path: PathBuf, buffer: &mut String, get_threads: bool,
|
||||
) -> anyhow::Result<(Process, Vec<PathBuf>)> {
|
||||
buffer.clear();
|
||||
|
||||
let fd = rustix::fs::openat(
|
||||
rustix::fs::CWD,
|
||||
&pid_path,
|
||||
pid_path.as_path(),
|
||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
)?;
|
||||
@ -245,7 +250,7 @@ impl Process {
|
||||
.next_back()
|
||||
.and_then(|s| s.to_string_lossy().parse::<Pid>().ok())
|
||||
.or_else(|| {
|
||||
rustix::fs::readlinkat(rustix::fs::CWD, &pid_path, vec![])
|
||||
rustix::fs::readlinkat(rustix::fs::CWD, pid_path.as_path(), vec![])
|
||||
.ok()
|
||||
.and_then(|s| s.to_string_lossy().parse::<Pid>().ok())
|
||||
})
|
||||
@ -281,13 +286,44 @@ impl Process {
|
||||
.and_then(|file| Io::from_file(file, buffer))
|
||||
.ok();
|
||||
|
||||
Ok(Process {
|
||||
pid,
|
||||
uid,
|
||||
stat,
|
||||
io,
|
||||
cmdline,
|
||||
})
|
||||
reset(&mut root, buffer);
|
||||
|
||||
let threads = if get_threads {
|
||||
root.push("task");
|
||||
|
||||
if let Ok(task) = std::fs::read_dir(root) {
|
||||
let pid_str = pid.to_string();
|
||||
|
||||
task.flatten()
|
||||
.filter_map(|thread_dir| {
|
||||
let file_name = thread_dir.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
let file_name = file_name.trim();
|
||||
|
||||
if is_str_numeric(file_name) && file_name != pid_str {
|
||||
Some(thread_dir.path())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Ok((
|
||||
Process {
|
||||
pid,
|
||||
uid,
|
||||
stat,
|
||||
io,
|
||||
cmdline,
|
||||
},
|
||||
threads,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,6 +222,7 @@ fn create_collection_thread(
|
||||
let unnormalized_cpu = app_config_fields.unnormalized_cpu;
|
||||
let show_average_cpu = app_config_fields.show_average_cpu;
|
||||
let update_sleep = app_config_fields.update_rate;
|
||||
let get_process_threads = app_config_fields.get_process_threads;
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut data_collector = collection::DataCollector::new(filters);
|
||||
@ -230,6 +231,7 @@ fn create_collection_thread(
|
||||
data_collector.set_use_current_cpu_total(use_current_cpu_total);
|
||||
data_collector.set_unnormalized_cpu(unnormalized_cpu);
|
||||
data_collector.set_show_average_cpu(show_average_cpu);
|
||||
data_collector.set_get_process_threads(get_process_threads);
|
||||
|
||||
data_collector.update_data();
|
||||
data_collector.data = Data::default();
|
||||
|
@ -60,6 +60,20 @@ macro_rules! is_flag_enabled {
|
||||
};
|
||||
}
|
||||
|
||||
/// A new version if [`is_flag_enabled`] which instead expects the user to pass in `config_section`, which is
|
||||
/// the section the flag is located, rather than defaulting to `config.flags` where `config` is passed in.
|
||||
macro_rules! is_flag_enabled_new {
|
||||
($flag_name:ident, $arg:expr, $config_section:expr) => {
|
||||
if $arg.$flag_name {
|
||||
true
|
||||
} else if let Some(options) = &$config_section {
|
||||
options.$flag_name.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The default config file sub-path.
|
||||
const DEFAULT_CONFIG_FILE_LOCATION: &str = "bottom/bottom.toml";
|
||||
|
||||
@ -283,6 +297,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
cpu_left_legend: is_flag_enabled!(cpu_left_legend, args.cpu, config),
|
||||
use_current_cpu_total: is_flag_enabled!(current_usage, args.process, config),
|
||||
unnormalized_cpu: is_flag_enabled!(unnormalized_cpu, args.process, config),
|
||||
get_process_threads: is_flag_enabled_new!(get_threads, args.process, config.processes),
|
||||
use_basic_mode,
|
||||
default_time_value,
|
||||
time_interval: get_time_interval(args, config, retention_ms)?,
|
||||
@ -1228,7 +1243,7 @@ mod test {
|
||||
.widget_states
|
||||
.iter()
|
||||
.zip(testing_app.states.proc_state.widget_states.iter())
|
||||
.all(|(a, b)| (a.1.test_equality(b.1)))
|
||||
.all(|(a, b)| a.1.test_equality(b.1))
|
||||
{
|
||||
panic!("failed on {arg_name}");
|
||||
}
|
||||
|
@ -315,6 +315,14 @@ pub struct ProcessArgs {
|
||||
)]
|
||||
pub disable_advanced_kill: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
action = ArgAction::SetTrue,
|
||||
help = "Also gather process thread information.",
|
||||
alias = "get-threads",
|
||||
)]
|
||||
pub get_threads: bool,
|
||||
|
||||
#[arg(
|
||||
short = 'g',
|
||||
long,
|
||||
@ -354,6 +362,14 @@ pub struct ProcessArgs {
|
||||
)]
|
||||
pub tree: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
action = ArgAction::SetTrue,
|
||||
help = "Collapse process tree by default.",
|
||||
alias = "tree-collapse"
|
||||
)]
|
||||
pub tree_collapse: bool,
|
||||
|
||||
#[arg(
|
||||
short = 'n',
|
||||
long,
|
||||
@ -371,14 +387,6 @@ pub struct ProcessArgs {
|
||||
alias = "whole-word"
|
||||
)]
|
||||
pub whole_word: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
action = ArgAction::SetTrue,
|
||||
help = "Collapse process tree by default.",
|
||||
alias = "tree-collapse"
|
||||
)]
|
||||
pub tree_collapse: bool,
|
||||
}
|
||||
|
||||
/// Temperature arguments/config options.
|
||||
|
@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::StringOrNum;
|
||||
|
||||
// TODO: Break this up.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
|
||||
|
@ -9,7 +9,10 @@ use crate::widgets::ProcColumn;
|
||||
pub(crate) struct ProcessesConfig {
|
||||
/// A list of process widget columns.
|
||||
#[serde(default)]
|
||||
pub(crate) columns: Vec<ProcColumn>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets
|
||||
pub columns: Vec<ProcColumn>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets
|
||||
|
||||
/// Whether to get process child threads.
|
||||
pub get_threads: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -124,6 +124,8 @@ pub struct Styles {
|
||||
pub(crate) low_battery: Style,
|
||||
pub(crate) invalid_query_style: Style,
|
||||
pub(crate) disabled_text_style: Style,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) thread_text_style: Style,
|
||||
pub(crate) border_type: BorderType,
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,8 @@ impl Styles {
|
||||
invalid_query_style: color!(Color::Red),
|
||||
disabled_text_style: color!(Color::DarkGray),
|
||||
border_type: BorderType::Plain,
|
||||
#[cfg(target_os = "linux")]
|
||||
thread_text_style: color!(Color::Green),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,8 @@ impl Styles {
|
||||
invalid_query_style: color!(Color::Red),
|
||||
disabled_text_style: hex!("#665c54"),
|
||||
border_type: BorderType::Plain,
|
||||
#[cfg(target_os = "linux")]
|
||||
thread_text_style: hex!("#458588"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,6 +132,8 @@ impl Styles {
|
||||
invalid_query_style: color!(Color::Red),
|
||||
disabled_text_style: hex!("#d5c4a1"),
|
||||
border_type: BorderType::Plain,
|
||||
#[cfg(target_os = "linux")]
|
||||
thread_text_style: hex!("#458588"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,8 @@ impl Styles {
|
||||
invalid_query_style: color!(Color::Red),
|
||||
disabled_text_style: hex!("#4c566a"),
|
||||
border_type: BorderType::Plain,
|
||||
#[cfg(target_os = "linux")]
|
||||
thread_text_style: hex!("#a3be8c"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,6 +108,8 @@ impl Styles {
|
||||
invalid_query_style: color!(Color::Red),
|
||||
disabled_text_style: hex!("#d8dee9"),
|
||||
border_type: BorderType::Plain,
|
||||
#[cfg(target_os = "linux")]
|
||||
thread_text_style: hex!("#a3be8c"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ impl CpuWidgetTableData {
|
||||
}
|
||||
|
||||
impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
|
||||
fn to_cell(
|
||||
fn to_cell_text(
|
||||
&self, column: &CpuWidgetColumn, calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
const CPU_TRUNCATE_BREAKPOINT: u16 = 5;
|
||||
|
@ -159,7 +159,7 @@ impl ColumnHeader for DiskColumn {
|
||||
|
||||
impl DataToCell<DiskColumn> for DiskWidgetData {
|
||||
// FIXME: (points_rework_v1) Can we change the return type to 'a instead of 'static?
|
||||
fn to_cell(
|
||||
fn to_cell_text(
|
||||
&self, column: &DiskColumn, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
fn percent_string(value: Option<f64>) -> Cow<'static, str> {
|
||||
|
@ -1176,6 +1176,8 @@ mod test {
|
||||
gpu_mem_usage: MemUsage::Percent(1.1),
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu_usage: 0,
|
||||
#[cfg(target_os = "linux")]
|
||||
process_type: crate::collection::processes::ProcessType::Regular,
|
||||
};
|
||||
|
||||
let b = ProcWidgetData {
|
||||
@ -1210,23 +1212,23 @@ mod test {
|
||||
data.sort_by_key(|p| p.pid);
|
||||
sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Descending);
|
||||
assert_eq!(
|
||||
[&c, &b, &a, &d].iter().map(|d| (d.pid)).collect::<Vec<_>>(),
|
||||
data.iter().map(|d| (d.pid)).collect::<Vec<_>>(),
|
||||
[&c, &b, &a, &d].iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||
data.iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
// Note that the PID ordering for ties is still ascending.
|
||||
data.sort_by_key(|p| p.pid);
|
||||
sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Ascending);
|
||||
assert_eq!(
|
||||
[&a, &d, &b, &c].iter().map(|d| (d.pid)).collect::<Vec<_>>(),
|
||||
data.iter().map(|d| (d.pid)).collect::<Vec<_>>(),
|
||||
[&a, &d, &b, &c].iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||
data.iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
data.sort_by_key(|p| p.pid);
|
||||
sort_skip_pid_asc(&ProcColumn::MemPercent, &mut data, SortOrder::Descending);
|
||||
assert_eq!(
|
||||
[&b, &a, &c, &d].iter().map(|d| (d.pid)).collect::<Vec<_>>(),
|
||||
data.iter().map(|d| (d.pid)).collect::<Vec<_>>(),
|
||||
[&b, &a, &c, &d].iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||
data.iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
// Note that the PID ordering for ties is still ascending.
|
||||
|
@ -10,6 +10,7 @@ use concat_string::concat_string;
|
||||
use tui::widgets::Row;
|
||||
|
||||
use super::process_columns::ProcColumn;
|
||||
|
||||
use crate::{
|
||||
canvas::{
|
||||
Painter,
|
||||
@ -214,6 +215,9 @@ pub struct ProcWidgetData {
|
||||
pub gpu_mem_usage: MemUsage,
|
||||
#[cfg(feature = "gpu")]
|
||||
pub gpu_usage: u32,
|
||||
/// The process "type". Used to color things.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub process_type: crate::collection::processes::ProcessType,
|
||||
}
|
||||
|
||||
impl ProcWidgetData {
|
||||
@ -258,6 +262,8 @@ impl ProcWidgetData {
|
||||
},
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu_usage: process.gpu_util,
|
||||
#[cfg(target_os = "linux")]
|
||||
process_type: process.process_type,
|
||||
}
|
||||
}
|
||||
|
||||
@ -324,7 +330,7 @@ impl ProcWidgetData {
|
||||
}
|
||||
|
||||
impl DataToCell<ProcColumn> for ProcWidgetData {
|
||||
fn to_cell(
|
||||
fn to_cell_text(
|
||||
&self, column: &ProcColumn, calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
let calculated_width = calculated_width.get();
|
||||
@ -361,6 +367,17 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline(always)]
|
||||
fn style_cell(&self, column: &ProcColumn, painter: &Painter) -> Option<tui::style::Style> {
|
||||
match column {
|
||||
ProcColumn::Name | ProcColumn::Command if self.process_type.is_thread() => {
|
||||
Some(painter.styles.thread_text_style)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
|
||||
if self.disabled {
|
||||
|
@ -11,7 +11,7 @@ impl ColumnHeader for SortTableColumn {
|
||||
}
|
||||
|
||||
impl DataToCell<SortTableColumn> for &'static str {
|
||||
fn to_cell(
|
||||
fn to_cell_text(
|
||||
&self, _column: &SortTableColumn, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
Some(Cow::Borrowed(self))
|
||||
@ -26,7 +26,7 @@ impl DataToCell<SortTableColumn> for &'static str {
|
||||
}
|
||||
|
||||
impl DataToCell<SortTableColumn> for Cow<'static, str> {
|
||||
fn to_cell(
|
||||
fn to_cell_text(
|
||||
&self, _column: &SortTableColumn, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
Some(self.clone())
|
||||
|
@ -40,7 +40,7 @@ impl TempWidgetData {
|
||||
}
|
||||
|
||||
impl DataToCell<TempWidgetColumn> for TempWidgetData {
|
||||
fn to_cell(
|
||||
fn to_cell_text(
|
||||
&self, column: &TempWidgetColumn, _calculated_width: NonZeroU16,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
Some(match column {
|
||||
|
Loading…
x
Reference in New Issue
Block a user