mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-09-22 17:28:19 +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
|
## [0.11.1] - 2025-08-15
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -145,7 +145,7 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bottom"
|
name = "bottom"
|
||||||
version = "0.11.1"
|
version = "0.12.0-nightly"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bottom"
|
name = "bottom"
|
||||||
version = "0.11.1"
|
version = "0.12.0-nightly"
|
||||||
repository = "https://github.com/ClementTsang/bottom"
|
repository = "https://github.com/ClementTsang/bottom"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows."
|
description = "A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows."
|
||||||
|
@ -207,7 +207,10 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"columns": {
|
"columns": {
|
||||||
"description": "A list of disk widget columns.",
|
"description": "A list of disk widget columns.",
|
||||||
"type": "array",
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/DiskColumn"
|
"$ref": "#/$defs/DiskColumn"
|
||||||
}
|
}
|
||||||
@ -751,6 +754,13 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/ProcColumn"
|
"$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 show_average_cpu: bool, // TODO: Unify this in CPU options
|
||||||
pub use_current_cpu_total: bool,
|
pub use_current_cpu_total: bool,
|
||||||
pub unnormalized_cpu: bool,
|
pub unnormalized_cpu: bool,
|
||||||
|
pub get_process_threads: bool,
|
||||||
pub use_basic_mode: bool,
|
pub use_basic_mode: bool,
|
||||||
pub default_time_value: u64,
|
pub default_time_value: u64,
|
||||||
pub time_interval: u64,
|
pub time_interval: u64,
|
||||||
|
@ -166,7 +166,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DataToCell<&'static str> for TestType {
|
impl DataToCell<&'static str> for TestType {
|
||||||
fn to_cell(
|
fn to_cell_text(
|
||||||
&self, _column: &&'static str, _calculated_width: NonZeroU16,
|
&self, _column: &&'static str, _calculated_width: NonZeroU16,
|
||||||
) -> Option<Cow<'static, str>> {
|
) -> Option<Cow<'static, str>> {
|
||||||
None
|
None
|
||||||
|
@ -10,15 +10,29 @@ where
|
|||||||
H: ColumnHeader,
|
H: ColumnHeader,
|
||||||
{
|
{
|
||||||
/// Given data, a column, and its corresponding width, return the string in
|
/// Given data, a column, and its corresponding width, return the string in
|
||||||
/// the cell that will be displayed in the
|
/// the cell that will be displayed in the [`super::DataTable`].
|
||||||
/// [`DataTable`](super::DataTable).
|
fn to_cell_text(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;
|
||||||
fn to_cell(&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.
|
/// Apply styling to the generated [`Row`] of cells.
|
||||||
///
|
///
|
||||||
/// The default implementation just returns the `row` that is passed in.
|
/// The default implementation just returns the `row` that is passed in.
|
||||||
#[inline(always)]
|
#[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
|
row
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ use tui::{
|
|||||||
Frame,
|
Frame,
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, Row, Table},
|
widgets::{Block, Cell, Row, Table},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -217,9 +217,17 @@ where
|
|||||||
.iter()
|
.iter()
|
||||||
.zip(&self.state.calculated_widths)
|
.zip(&self.state.calculated_widths)
|
||||||
.filter_map(|(column, &width)| {
|
.filter_map(|(column, &width)| {
|
||||||
data_row
|
data_row.to_cell_text(column.inner(), width).map(|content| {
|
||||||
.to_cell(column.inner(), width)
|
let content = truncate_to_text(&content, width.get());
|
||||||
.map(|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 {
|
impl DataToCell<ColumnType> for TestType {
|
||||||
fn to_cell(
|
fn to_cell_text(
|
||||||
&self, _column: &ColumnType, _calculated_width: NonZeroU16,
|
&self, _column: &ColumnType, _calculated_width: NonZeroU16,
|
||||||
) -> Option<Cow<'static, str>> {
|
) -> Option<Cow<'static, str>> {
|
||||||
None
|
None
|
||||||
|
@ -68,7 +68,7 @@ impl Painter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draws the process sort box.
|
/// 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(
|
fn draw_processes_table(
|
||||||
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
|
@ -160,14 +160,13 @@ pub struct DataCollector {
|
|||||||
unnormalized_cpu: bool,
|
unnormalized_cpu: bool,
|
||||||
use_current_cpu_total: bool,
|
use_current_cpu_total: bool,
|
||||||
show_average_cpu: bool,
|
show_average_cpu: bool,
|
||||||
|
get_process_threads: bool,
|
||||||
|
|
||||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
|
||||||
last_list_collection_time: Instant,
|
last_list_collection_time: Instant,
|
||||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
should_run_less_routine_tasks: bool,
|
||||||
should_refresh_list: bool,
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pid_mapping: HashMap<Pid, processes::PrevProcDetails>,
|
prev_process_details: HashMap<Pid, processes::PrevProcDetails>,
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
prev_idle: f64,
|
prev_idle: f64,
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@ -187,25 +186,26 @@ pub struct DataCollector {
|
|||||||
gpus_total_mem: Option<u64>,
|
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 {
|
impl DataCollector {
|
||||||
pub fn new(filters: DataFilters) -> Self {
|
pub fn new(filters: DataFilters) -> Self {
|
||||||
// Initialize it to the past to force it to load on initialization.
|
// Initialize it to the past to force it to load on initialization.
|
||||||
let now = Instant::now();
|
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 {
|
DataCollector {
|
||||||
data: Data::default(),
|
data: Data::default(),
|
||||||
sys: SysinfoSource::default(),
|
sys: SysinfoSource::default(),
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pid_mapping: HashMap::default(),
|
prev_process_details: HashMap::default(),
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
prev_idle: 0_f64,
|
prev_idle: 0_f64,
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
prev_non_idle: 0_f64,
|
prev_non_idle: 0_f64,
|
||||||
use_current_cpu_total: false,
|
use_current_cpu_total: false,
|
||||||
unnormalized_cpu: false,
|
unnormalized_cpu: false,
|
||||||
|
get_process_threads: false,
|
||||||
last_collection_time,
|
last_collection_time,
|
||||||
total_rx: 0,
|
total_rx: 0,
|
||||||
total_tx: 0,
|
total_tx: 0,
|
||||||
@ -222,30 +222,27 @@ impl DataCollector {
|
|||||||
gpu_pids: None,
|
gpu_pids: None,
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
gpus_total_mem: None,
|
gpus_total_mem: None,
|
||||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
|
||||||
last_list_collection_time: last_collection_time,
|
last_list_collection_time: last_collection_time,
|
||||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
should_run_less_routine_tasks: true,
|
||||||
should_refresh_list: 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.
|
/// 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.
|
/// Note this should be set back to false if `self.last_list_collection_time` is updated.
|
||||||
#[inline]
|
#[inline]
|
||||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
fn run_less_routine_tasks(&mut self) {
|
||||||
fn update_refresh_list_check(&mut self) {
|
|
||||||
if self
|
if self
|
||||||
.data
|
.data
|
||||||
.collection_time
|
.collection_time
|
||||||
.duration_since(self.last_list_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;
|
self.last_list_collection_time = self.data.collection_time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,6 +263,10 @@ impl DataCollector {
|
|||||||
self.show_average_cpu = show_average_cpu;
|
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:
|
/// Refresh sysinfo data. We use sysinfo for the following data:
|
||||||
/// - CPU usage
|
/// - CPU usage
|
||||||
/// - Memory usage
|
/// - Memory usage
|
||||||
@ -307,13 +308,13 @@ impl DataCollector {
|
|||||||
|
|
||||||
// For Windows, sysinfo also handles the users list.
|
// For Windows, sysinfo also handles the users list.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
if self.should_refresh_list {
|
if self.should_run_less_routine_tasks {
|
||||||
self.sys.users.refresh();
|
self.sys.users.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.widgets_to_harvest.use_temp {
|
if self.widgets_to_harvest.use_temp {
|
||||||
if self.should_refresh_list {
|
if self.should_run_less_routine_tasks {
|
||||||
self.sys.temps.refresh(true);
|
self.sys.temps.refresh(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +325,7 @@ impl DataCollector {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
if self.widgets_to_harvest.use_disk {
|
if self.widgets_to_harvest.use_disk {
|
||||||
if self.should_refresh_list {
|
if self.should_run_less_routine_tasks {
|
||||||
self.sys.disks.refresh(true);
|
self.sys.disks.refresh(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,10 +342,7 @@ impl DataCollector {
|
|||||||
pub fn update_data(&mut self) {
|
pub fn update_data(&mut self) {
|
||||||
self.data.collection_time = Instant::now();
|
self.data.collection_time = Instant::now();
|
||||||
|
|
||||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
self.run_less_routine_tasks();
|
||||||
{
|
|
||||||
self.update_refresh_list_check();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.refresh_sysinfo_data();
|
self.refresh_sysinfo_data();
|
||||||
|
|
||||||
@ -362,10 +360,8 @@ impl DataCollector {
|
|||||||
self.update_network_usage();
|
self.update_network_usage();
|
||||||
self.update_disks();
|
self.update_disks();
|
||||||
|
|
||||||
#[cfg(any(not(target_os = "linux"), feature = "battery"))]
|
// Make sure to run this to refresh the setting.
|
||||||
{
|
self.should_run_less_routine_tasks = false;
|
||||||
self.should_refresh_list = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update times for future reference.
|
// Update times for future reference.
|
||||||
self.last_collection_time = self.data.collection_time;
|
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.
|
/// 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]
|
#[inline]
|
||||||
#[cfg(feature = "battery")]
|
#[cfg(feature = "battery")]
|
||||||
fn update_batteries(&mut self) {
|
fn update_batteries(&mut self) {
|
||||||
let battery_manager = match &self.battery_manager {
|
let battery_manager = match &self.battery_manager {
|
||||||
Some(manager) => {
|
Some(manager) => {
|
||||||
// Also check if we need to refresh the list of batteries.
|
// 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
|
let battery_list = manager
|
||||||
.batteries()
|
.batteries()
|
||||||
.map(|batteries| batteries.filter_map(Result::ok).collect::<Vec<_>>());
|
.map(|batteries| batteries.filter_map(Result::ok).collect::<Vec<_>>());
|
||||||
|
@ -48,6 +48,36 @@ cfg_if! {
|
|||||||
|
|
||||||
pub type Bytes = u64;
|
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)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ProcessHarvest {
|
pub struct ProcessHarvest {
|
||||||
/// The pid of the process.
|
/// The pid of the process.
|
||||||
@ -60,6 +90,8 @@ pub struct ProcessHarvest {
|
|||||||
pub cpu_usage_percent: f32,
|
pub cpu_usage_percent: f32,
|
||||||
|
|
||||||
/// Memory usage as a percentage.
|
/// 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,
|
pub mem_usage_percent: f32,
|
||||||
|
|
||||||
/// Memory usage as bytes.
|
/// Memory usage as bytes.
|
||||||
@ -105,12 +137,18 @@ pub struct ProcessHarvest {
|
|||||||
pub gpu_mem: u64,
|
pub gpu_mem: u64,
|
||||||
|
|
||||||
/// Gpu memory usage as percentage.
|
/// 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")]
|
#[cfg(feature = "gpu")]
|
||||||
pub gpu_mem_percent: f32,
|
pub gpu_mem_percent: f32,
|
||||||
|
|
||||||
/// Gpu utilization as a percentage.
|
/// Gpu utilization as a percentage.
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
pub gpu_util: u32,
|
pub gpu_util: u32,
|
||||||
|
|
||||||
|
/// The process entry "type".
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub process_type: ProcessType,
|
||||||
// TODO: Additional fields
|
// TODO: Additional fields
|
||||||
// pub rss_kb: u64,
|
// pub rss_kb: u64,
|
||||||
// pub virt_kb: u64,
|
// pub virt_kb: u64,
|
||||||
|
@ -9,14 +9,16 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use concat_string::concat_string;
|
use concat_string::concat_string;
|
||||||
use hashbrown::HashSet;
|
use hashbrown::{HashMap, HashSet};
|
||||||
use process::*;
|
use process::*;
|
||||||
use sysinfo::ProcessStatus;
|
use sysinfo::ProcessStatus;
|
||||||
|
|
||||||
use super::{Pid, ProcessHarvest, UserTable, process_status_str};
|
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.
|
/// If it's equal or greater, then we instead refer to the command for the name.
|
||||||
const MAX_STAT_NAME_LEN: usize = 15;
|
const MAX_STAT_NAME_LEN: usize = 15;
|
||||||
|
|
||||||
@ -132,6 +134,7 @@ fn get_linux_cpu_usage(
|
|||||||
|
|
||||||
fn read_proc(
|
fn read_proc(
|
||||||
prev_proc: &PrevProcDetails, process: Process, args: ReadProcArgs, user_table: &mut UserTable,
|
prev_proc: &PrevProcDetails, process: Process, args: ReadProcArgs, user_table: &mut UserTable,
|
||||||
|
thread_parent: Option<Pid>,
|
||||||
) -> CollectionResult<(ProcessHarvest, u64)> {
|
) -> CollectionResult<(ProcessHarvest, u64)> {
|
||||||
let Process {
|
let Process {
|
||||||
pid: _,
|
pid: _,
|
||||||
@ -147,7 +150,8 @@ fn read_proc(
|
|||||||
cpu_fraction,
|
cpu_fraction,
|
||||||
total_memory,
|
total_memory,
|
||||||
time_difference_in_secs,
|
time_difference_in_secs,
|
||||||
uptime,
|
system_uptime,
|
||||||
|
get_process_threads: _,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
let process_state_char = stat.state;
|
let process_state_char = stat.state;
|
||||||
@ -162,7 +166,13 @@ fn read_proc(
|
|||||||
prev_proc.cpu_time,
|
prev_proc.cpu_time,
|
||||||
use_current_cpu_total,
|
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 = stat.rss_bytes();
|
||||||
let mem_usage_percent = (mem_usage as f64 / total_memory as f64 * 100.0) as f32;
|
let mem_usage_percent = (mem_usage as f64 / total_memory as f64 * 100.0) as f32;
|
||||||
let virtual_mem = stat.vsize;
|
let virtual_mem = stat.vsize;
|
||||||
@ -202,7 +212,9 @@ fn read_proc(
|
|||||||
if ticks_per_sec == 0 {
|
if ticks_per_sec == 0 {
|
||||||
Duration::ZERO
|
Duration::ZERO
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
Duration::ZERO
|
Duration::ZERO
|
||||||
@ -222,12 +234,15 @@ fn read_proc(
|
|||||||
|
|
||||||
// We're only interested in the executable part, not the file path (part of command),
|
// We're only interested in the executable part, not the file path (part of command),
|
||||||
// so strip everything but the command name if needed.
|
// 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,
|
Some((_, last)) => last,
|
||||||
None => first_part,
|
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 {
|
} else {
|
||||||
truncated_name
|
truncated_name
|
||||||
};
|
};
|
||||||
@ -263,6 +278,7 @@ fn read_proc(
|
|||||||
gpu_mem_percent: 0.0,
|
gpu_mem_percent: 0.0,
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
gpu_util: 0,
|
gpu_util: 0,
|
||||||
|
process_type,
|
||||||
},
|
},
|
||||||
new_process_times,
|
new_process_times,
|
||||||
))
|
))
|
||||||
@ -276,6 +292,7 @@ pub(crate) struct PrevProc<'a> {
|
|||||||
pub(crate) struct ProcHarvestOptions {
|
pub(crate) struct ProcHarvestOptions {
|
||||||
pub use_current_cpu_total: bool,
|
pub use_current_cpu_total: bool,
|
||||||
pub unnormalized_cpu: bool,
|
pub unnormalized_cpu: bool,
|
||||||
|
pub get_process_threads: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_str_numeric(s: &str) -> 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.
|
/// General args to keep around for reading proc data.
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub(crate) struct ReadProcArgs {
|
pub(crate) struct ReadProcArgs {
|
||||||
pub(crate) use_current_cpu_total: bool,
|
pub use_current_cpu_total: bool,
|
||||||
pub(crate) cpu_usage: f64,
|
pub cpu_usage: f64,
|
||||||
pub(crate) cpu_fraction: f64,
|
pub cpu_fraction: f64,
|
||||||
pub(crate) total_memory: u64,
|
pub total_memory: u64,
|
||||||
pub(crate) time_difference_in_secs: u64,
|
pub time_difference_in_secs: u64,
|
||||||
pub(crate) uptime: u64,
|
pub system_uptime: u64,
|
||||||
|
pub get_process_threads: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn linux_process_data(
|
pub(crate) fn linux_process_data(
|
||||||
@ -304,13 +322,15 @@ pub(crate) fn linux_process_data(
|
|||||||
let proc_harvest_options = ProcHarvestOptions {
|
let proc_harvest_options = ProcHarvestOptions {
|
||||||
use_current_cpu_total: collector.use_current_cpu_total,
|
use_current_cpu_total: collector.use_current_cpu_total,
|
||||||
unnormalized_cpu: collector.unnormalized_cpu,
|
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 user_table = &mut collector.user_table;
|
||||||
|
|
||||||
let ProcHarvestOptions {
|
let ProcHarvestOptions {
|
||||||
use_current_cpu_total,
|
use_current_cpu_total,
|
||||||
unnormalized_cpu,
|
unnormalized_cpu,
|
||||||
|
get_process_threads: get_threads,
|
||||||
} = proc_harvest_options;
|
} = proc_harvest_options;
|
||||||
|
|
||||||
let PrevProc {
|
let PrevProc {
|
||||||
@ -333,9 +353,13 @@ pub(crate) fn linux_process_data(
|
|||||||
cpu_usage /= num_processors;
|
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| {
|
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()) {
|
if is_str_numeric(dir.file_name().to_string_lossy().trim()) {
|
||||||
Some(dir.path())
|
Some(dir.path())
|
||||||
} else {
|
} else {
|
||||||
@ -349,20 +373,25 @@ pub(crate) fn linux_process_data(
|
|||||||
cpu_fraction,
|
cpu_fraction,
|
||||||
total_memory,
|
total_memory,
|
||||||
time_difference_in_secs,
|
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 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| {
|
.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 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))]
|
#[cfg_attr(not(feature = "gpu"), expect(unused_mut))]
|
||||||
if let Ok((mut process_harvest, new_process_times)) =
|
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")]
|
#[cfg(feature = "gpu")]
|
||||||
if let Some(gpus) = &collector.gpu_pids {
|
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_read_bytes = process_harvest.total_read;
|
||||||
prev_proc_details.total_write_bytes = process_harvest.total_write;
|
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);
|
return Some(process_harvest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -393,10 +426,37 @@ pub(crate) fn linux_process_data(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
pids_to_clear.iter().for_each(|pid| {
|
// Get thread data.
|
||||||
pid_mapping.remove(pid);
|
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)
|
Ok(process_vector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ use rustix::{
|
|||||||
path::Arg,
|
path::Arg,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::collection::processes::Pid;
|
use crate::collection::processes::{Pid, linux::is_str_numeric};
|
||||||
|
|
||||||
static PAGESIZE: OnceLock<u64> = OnceLock::new();
|
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))
|
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper around the data in `/proc/<PID>/stat`. For documentation, see
|
/// A wrapper around the data in `/proc/<PID>/stat`. For documentation, see:
|
||||||
/// [here](https://man7.org/linux/man-pages/man5/proc.5.html).
|
/// - <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
|
/// Note this does not necessarily get all fields, only the ones we use in
|
||||||
/// bottom.
|
/// bottom.
|
||||||
@ -62,7 +63,8 @@ pub(crate) struct Stat {
|
|||||||
|
|
||||||
impl Stat {
|
impl Stat {
|
||||||
/// Get process stats from a file; this assumes the file is located at
|
/// 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> {
|
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
|
// 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.
|
// (technically) might have non-utf8 characters, we can't just use read_to_string.
|
||||||
@ -229,12 +231,15 @@ impl Process {
|
|||||||
/// will be discarded quickly.
|
/// will be discarded quickly.
|
||||||
///
|
///
|
||||||
/// This takes in a buffer to avoid allocs; this function will clear the buffer.
|
/// 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();
|
buffer.clear();
|
||||||
|
|
||||||
let fd = rustix::fs::openat(
|
let fd = rustix::fs::openat(
|
||||||
rustix::fs::CWD,
|
rustix::fs::CWD,
|
||||||
&pid_path,
|
pid_path.as_path(),
|
||||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||||
Mode::empty(),
|
Mode::empty(),
|
||||||
)?;
|
)?;
|
||||||
@ -245,7 +250,7 @@ impl Process {
|
|||||||
.next_back()
|
.next_back()
|
||||||
.and_then(|s| s.to_string_lossy().parse::<Pid>().ok())
|
.and_then(|s| s.to_string_lossy().parse::<Pid>().ok())
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
rustix::fs::readlinkat(rustix::fs::CWD, &pid_path, vec![])
|
rustix::fs::readlinkat(rustix::fs::CWD, pid_path.as_path(), vec![])
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.to_string_lossy().parse::<Pid>().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))
|
.and_then(|file| Io::from_file(file, buffer))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
Ok(Process {
|
reset(&mut root, buffer);
|
||||||
pid,
|
|
||||||
uid,
|
let threads = if get_threads {
|
||||||
stat,
|
root.push("task");
|
||||||
io,
|
|
||||||
cmdline,
|
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 unnormalized_cpu = app_config_fields.unnormalized_cpu;
|
||||||
let show_average_cpu = app_config_fields.show_average_cpu;
|
let show_average_cpu = app_config_fields.show_average_cpu;
|
||||||
let update_sleep = app_config_fields.update_rate;
|
let update_sleep = app_config_fields.update_rate;
|
||||||
|
let get_process_threads = app_config_fields.get_process_threads;
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut data_collector = collection::DataCollector::new(filters);
|
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_use_current_cpu_total(use_current_cpu_total);
|
||||||
data_collector.set_unnormalized_cpu(unnormalized_cpu);
|
data_collector.set_unnormalized_cpu(unnormalized_cpu);
|
||||||
data_collector.set_show_average_cpu(show_average_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.update_data();
|
||||||
data_collector.data = Data::default();
|
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.
|
/// The default config file sub-path.
|
||||||
const DEFAULT_CONFIG_FILE_LOCATION: &str = "bottom/bottom.toml";
|
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),
|
cpu_left_legend: is_flag_enabled!(cpu_left_legend, args.cpu, config),
|
||||||
use_current_cpu_total: is_flag_enabled!(current_usage, args.process, config),
|
use_current_cpu_total: is_flag_enabled!(current_usage, args.process, config),
|
||||||
unnormalized_cpu: is_flag_enabled!(unnormalized_cpu, 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,
|
use_basic_mode,
|
||||||
default_time_value,
|
default_time_value,
|
||||||
time_interval: get_time_interval(args, config, retention_ms)?,
|
time_interval: get_time_interval(args, config, retention_ms)?,
|
||||||
@ -1228,7 +1243,7 @@ mod test {
|
|||||||
.widget_states
|
.widget_states
|
||||||
.iter()
|
.iter()
|
||||||
.zip(testing_app.states.proc_state.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}");
|
panic!("failed on {arg_name}");
|
||||||
}
|
}
|
||||||
|
@ -315,6 +315,14 @@ pub struct ProcessArgs {
|
|||||||
)]
|
)]
|
||||||
pub disable_advanced_kill: bool,
|
pub disable_advanced_kill: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
action = ArgAction::SetTrue,
|
||||||
|
help = "Also gather process thread information.",
|
||||||
|
alias = "get-threads",
|
||||||
|
)]
|
||||||
|
pub get_threads: bool,
|
||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'g',
|
short = 'g',
|
||||||
long,
|
long,
|
||||||
@ -354,6 +362,14 @@ pub struct ProcessArgs {
|
|||||||
)]
|
)]
|
||||||
pub tree: bool,
|
pub tree: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
action = ArgAction::SetTrue,
|
||||||
|
help = "Collapse process tree by default.",
|
||||||
|
alias = "tree-collapse"
|
||||||
|
)]
|
||||||
|
pub tree_collapse: bool,
|
||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'n',
|
short = 'n',
|
||||||
long,
|
long,
|
||||||
@ -371,14 +387,6 @@ pub struct ProcessArgs {
|
|||||||
alias = "whole-word"
|
alias = "whole-word"
|
||||||
)]
|
)]
|
||||||
pub whole_word: bool,
|
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.
|
/// Temperature arguments/config options.
|
||||||
|
@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use super::StringOrNum;
|
use super::StringOrNum;
|
||||||
|
|
||||||
|
// TODO: Break this up.
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
|
||||||
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
|
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
|
||||||
|
@ -9,7 +9,10 @@ use crate::widgets::ProcColumn;
|
|||||||
pub(crate) struct ProcessesConfig {
|
pub(crate) struct ProcessesConfig {
|
||||||
/// A list of process widget columns.
|
/// A list of process widget columns.
|
||||||
#[serde(default)]
|
#[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)]
|
#[cfg(test)]
|
||||||
|
@ -124,6 +124,8 @@ pub struct Styles {
|
|||||||
pub(crate) low_battery: Style,
|
pub(crate) low_battery: Style,
|
||||||
pub(crate) invalid_query_style: Style,
|
pub(crate) invalid_query_style: Style,
|
||||||
pub(crate) disabled_text_style: Style,
|
pub(crate) disabled_text_style: Style,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(crate) thread_text_style: Style,
|
||||||
pub(crate) border_type: BorderType,
|
pub(crate) border_type: BorderType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +67,8 @@ impl Styles {
|
|||||||
invalid_query_style: color!(Color::Red),
|
invalid_query_style: color!(Color::Red),
|
||||||
disabled_text_style: color!(Color::DarkGray),
|
disabled_text_style: color!(Color::DarkGray),
|
||||||
border_type: BorderType::Plain,
|
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),
|
invalid_query_style: color!(Color::Red),
|
||||||
disabled_text_style: hex!("#665c54"),
|
disabled_text_style: hex!("#665c54"),
|
||||||
border_type: BorderType::Plain,
|
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),
|
invalid_query_style: color!(Color::Red),
|
||||||
disabled_text_style: hex!("#d5c4a1"),
|
disabled_text_style: hex!("#d5c4a1"),
|
||||||
border_type: BorderType::Plain,
|
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),
|
invalid_query_style: color!(Color::Red),
|
||||||
disabled_text_style: hex!("#4c566a"),
|
disabled_text_style: hex!("#4c566a"),
|
||||||
border_type: BorderType::Plain,
|
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),
|
invalid_query_style: color!(Color::Red),
|
||||||
disabled_text_style: hex!("#d8dee9"),
|
disabled_text_style: hex!("#d8dee9"),
|
||||||
border_type: BorderType::Plain,
|
border_type: BorderType::Plain,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
thread_text_style: hex!("#a3be8c"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ impl CpuWidgetTableData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
|
impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
|
||||||
fn to_cell(
|
fn to_cell_text(
|
||||||
&self, column: &CpuWidgetColumn, calculated_width: NonZeroU16,
|
&self, column: &CpuWidgetColumn, calculated_width: NonZeroU16,
|
||||||
) -> Option<Cow<'static, str>> {
|
) -> Option<Cow<'static, str>> {
|
||||||
const CPU_TRUNCATE_BREAKPOINT: u16 = 5;
|
const CPU_TRUNCATE_BREAKPOINT: u16 = 5;
|
||||||
|
@ -159,7 +159,7 @@ impl ColumnHeader for DiskColumn {
|
|||||||
|
|
||||||
impl DataToCell<DiskColumn> for DiskWidgetData {
|
impl DataToCell<DiskColumn> for DiskWidgetData {
|
||||||
// FIXME: (points_rework_v1) Can we change the return type to 'a instead of 'static?
|
// 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,
|
&self, column: &DiskColumn, _calculated_width: NonZeroU16,
|
||||||
) -> Option<Cow<'static, str>> {
|
) -> Option<Cow<'static, str>> {
|
||||||
fn percent_string(value: Option<f64>) -> 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),
|
gpu_mem_usage: MemUsage::Percent(1.1),
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
gpu_usage: 0,
|
gpu_usage: 0,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
process_type: crate::collection::processes::ProcessType::Regular,
|
||||||
};
|
};
|
||||||
|
|
||||||
let b = ProcWidgetData {
|
let b = ProcWidgetData {
|
||||||
@ -1210,23 +1212,23 @@ mod test {
|
|||||||
data.sort_by_key(|p| p.pid);
|
data.sort_by_key(|p| p.pid);
|
||||||
sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Descending);
|
sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Descending);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
[&c, &b, &a, &d].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<_>>(),
|
data.iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Note that the PID ordering for ties is still ascending.
|
// Note that the PID ordering for ties is still ascending.
|
||||||
data.sort_by_key(|p| p.pid);
|
data.sort_by_key(|p| p.pid);
|
||||||
sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Ascending);
|
sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Ascending);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
[&a, &d, &b, &c].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.iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
data.sort_by_key(|p| p.pid);
|
data.sort_by_key(|p| p.pid);
|
||||||
sort_skip_pid_asc(&ProcColumn::MemPercent, &mut data, SortOrder::Descending);
|
sort_skip_pid_asc(&ProcColumn::MemPercent, &mut data, SortOrder::Descending);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
[&b, &a, &c, &d].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<_>>(),
|
data.iter().map(|d| d.pid).collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Note that the PID ordering for ties is still ascending.
|
// Note that the PID ordering for ties is still ascending.
|
||||||
|
@ -10,6 +10,7 @@ use concat_string::concat_string;
|
|||||||
use tui::widgets::Row;
|
use tui::widgets::Row;
|
||||||
|
|
||||||
use super::process_columns::ProcColumn;
|
use super::process_columns::ProcColumn;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
canvas::{
|
canvas::{
|
||||||
Painter,
|
Painter,
|
||||||
@ -214,6 +215,9 @@ pub struct ProcWidgetData {
|
|||||||
pub gpu_mem_usage: MemUsage,
|
pub gpu_mem_usage: MemUsage,
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
pub gpu_usage: u32,
|
pub gpu_usage: u32,
|
||||||
|
/// The process "type". Used to color things.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub process_type: crate::collection::processes::ProcessType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcWidgetData {
|
impl ProcWidgetData {
|
||||||
@ -258,6 +262,8 @@ impl ProcWidgetData {
|
|||||||
},
|
},
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
gpu_usage: process.gpu_util,
|
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 {
|
impl DataToCell<ProcColumn> for ProcWidgetData {
|
||||||
fn to_cell(
|
fn to_cell_text(
|
||||||
&self, column: &ProcColumn, calculated_width: NonZeroU16,
|
&self, column: &ProcColumn, calculated_width: NonZeroU16,
|
||||||
) -> Option<Cow<'static, str>> {
|
) -> Option<Cow<'static, str>> {
|
||||||
let calculated_width = calculated_width.get();
|
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)]
|
#[inline(always)]
|
||||||
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
|
fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
|
||||||
if self.disabled {
|
if self.disabled {
|
||||||
|
@ -11,7 +11,7 @@ impl ColumnHeader for SortTableColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DataToCell<SortTableColumn> for &'static str {
|
impl DataToCell<SortTableColumn> for &'static str {
|
||||||
fn to_cell(
|
fn to_cell_text(
|
||||||
&self, _column: &SortTableColumn, _calculated_width: NonZeroU16,
|
&self, _column: &SortTableColumn, _calculated_width: NonZeroU16,
|
||||||
) -> Option<Cow<'static, str>> {
|
) -> Option<Cow<'static, str>> {
|
||||||
Some(Cow::Borrowed(self))
|
Some(Cow::Borrowed(self))
|
||||||
@ -26,7 +26,7 @@ impl DataToCell<SortTableColumn> for &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DataToCell<SortTableColumn> for Cow<'static, str> {
|
impl DataToCell<SortTableColumn> for Cow<'static, str> {
|
||||||
fn to_cell(
|
fn to_cell_text(
|
||||||
&self, _column: &SortTableColumn, _calculated_width: NonZeroU16,
|
&self, _column: &SortTableColumn, _calculated_width: NonZeroU16,
|
||||||
) -> Option<Cow<'static, str>> {
|
) -> Option<Cow<'static, str>> {
|
||||||
Some(self.clone())
|
Some(self.clone())
|
||||||
|
@ -40,7 +40,7 @@ impl TempWidgetData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DataToCell<TempWidgetColumn> for TempWidgetData {
|
impl DataToCell<TempWidgetColumn> for TempWidgetData {
|
||||||
fn to_cell(
|
fn to_cell_text(
|
||||||
&self, column: &TempWidgetColumn, _calculated_width: NonZeroU16,
|
&self, column: &TempWidgetColumn, _calculated_width: NonZeroU16,
|
||||||
) -> Option<Cow<'static, str>> {
|
) -> Option<Cow<'static, str>> {
|
||||||
Some(match column {
|
Some(match column {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user