feature: hide k-threads (#1772)

* hide k-threads init

comment

* add config for hide_k_threads

arg help

* update docs for hide_k_threads

* add test_toggle_k_thread for process_table

* update help text and schema

* add hide_k_threads to sample_configs default

* add Virt column to sample default config

* update sample demo config column name for schema ci

* force_rerender_and_update from toggle_k_threads for updates when frozen

* use is_kernel for ProcessType

* remove todo

* Apply suggestions from code review

Co-authored-by: Clement Tsang <34804052+ClementTsang@users.noreply.github.com>

---------

Co-authored-by: Clement Tsang <34804052+ClementTsang@users.noreply.github.com>
This commit is contained in:
Justin Martin 2025-09-07 22:56:56 +00:00 committed by GitHub
parent f1017d6f6f
commit b07f940970
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 144 additions and 8 deletions

View File

@ -35,6 +35,7 @@ see information on these options by running `btm -h`, or run `btm --help` to dis
| `--disable_advanced_kill` | Hides additional stopping options on Unix-like systems. |
| `--get_threads` | Also gather process thread information. |
| `-g, --group_processes` | Groups processes with the same name by default. No effect if `--tree` is set. |
| `--hide_k_threads` | Hide kernel threads by default. |
| `--process_memory_as_value` | Defaults to showing process memory usage by value. |
| `--process_command` | Shows the full command name instead of the process name by default. |
| `-R, --regex` | Enables regex by default while searching. |

View File

@ -52,3 +52,4 @@ each time:
| `network_legend` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the network widget. |
| `average_cpu_row` | Boolean | Moves the average CPU usage entry to its own row when using basic mode. |
| `tree_collapse` | Boolean | Collapse process tree by default. |
| `hide_k_threads` | Boolean | Hide kernel threads by default. |

View File

@ -233,6 +233,7 @@ Note that key bindings are generally case-sensitive.
| ++t++ , ++f5++ | Toggle tree mode |
| ++M++ | Sort by gpu memory usage, press again to reverse sorting order |
| ++C++ | Sort by gpu usage, press again to reverse sorting order |
| ++z++ | Toggle the hiding of kernel threads |
### Sort sub-widget

View File

@ -103,6 +103,9 @@
# Hides advanced options to stop a process on Unix-like systems.
#disable_advanced_kill = false
# Hides the kernel threads
#hide_k_threads = false
# Hide GPU(s) information
#disable_gpu = false
@ -123,7 +126,7 @@
#[processes]
# The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built):
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"]
#columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"]
# CPU widget configuration

View File

@ -388,6 +388,12 @@
"null"
]
},
"hide_k_threads": {
"type": [
"boolean",
"null"
]
},
"hide_table_gap": {
"type": [
"boolean",

View File

@ -57,6 +57,8 @@ pub struct AppConfigFields {
pub show_table_scroll_position: bool,
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
pub is_advanced_kill: bool,
#[cfg(target_os = "linux")]
pub hide_k_threads: bool,
pub memory_legend_position: Option<LegendPosition>,
// TODO: Remove these, move network details state-side.
pub network_unit_type: DataUnit,
@ -1231,6 +1233,19 @@ impl App {
}
'I' => self.invert_sort(),
'%' => self.toggle_percentages(),
#[cfg(target_os = "linux")]
'z' => {
if let BottomWidgetType::Proc = self.current_widget.widget_type {
if let Some(proc_widget_state) = self
.states
.proc_state
.widget_states
.get_mut(&self.current_widget.widget_id)
{
proc_widget_state.toggle_k_thread();
}
}
}
_ => {}
}

View File

@ -57,8 +57,6 @@ pub enum ProcessType {
Regular,
/// A kernel process.
///
/// TODO: Use <https://github.com/htop-dev/htop/commit/07496eafb0166aafd9c33a6a95e16bcbc64c34d4>?
Kernel,
/// A thread spawned by a regular user process.

View File

@ -169,6 +169,8 @@ fn read_proc(
let (parent_pid, process_type) = if let Some(thread_parent) = thread_parent {
(Some(thread_parent), ProcessType::ProcessThread)
} else if stat.is_kernel_thread {
(Some(stat.ppid), ProcessType::Kernel)
} else {
(Some(stat.ppid), ProcessType::Regular)
};

View File

@ -59,6 +59,9 @@ pub(crate) struct Stat {
/// The start time of the process, represented in clock ticks.
pub start_time: u64,
/// Kernel thread
pub is_kernel_thread: bool,
}
impl Stat {
@ -92,9 +95,15 @@ impl Stat {
.ok_or_else(|| anyhow!("missing state"))?;
let ppid: Pid = next_part(&mut rest)?.parse()?;
// Skip 9 fields until utime (pgrp, session, tty_nr, tpgid, flags, minflt,
// cminflt, majflt, cmajflt).
let mut rest = rest.skip(9);
// Skip 4 fields (pgrp, session, tty_nr, tpgid)
let mut rest = rest.skip(4);
// read flags for kernel thread (PF_KTHREAD from include/linux/sched.h)
let flags: u32 = next_part(&mut rest)?.parse()?;
let is_kernel_thread: bool = flags & 0x00200000 != 0;
// Skip 4 fields (minflt, cminflt, majflt, cmajflt)
let mut rest = rest.skip(4);
let utime: u64 = next_part(&mut rest)?.parse()?;
let stime: u64 = next_part(&mut rest)?.parse()?;
@ -115,6 +124,7 @@ impl Stat {
rss,
vsize,
start_time,
is_kernel_thread,
})
}

View File

@ -65,7 +65,7 @@ const CPU_HELP_TEXT: [&str; 2] = [
"Mouse scroll Scrolling over an CPU core/average shows only that entry on the chart",
];
const PROCESS_HELP_TEXT: [&str; 19] = [
const PROCESS_HELP_TEXT: [&str; 20] = [
"3 - Process widget",
"dd, F9, Delete Kill the selected process",
"c Sort by CPU usage, press again to reverse",
@ -85,6 +85,7 @@ const PROCESS_HELP_TEXT: [&str; 19] = [
"click on header Sorts the entries by that column, click again to invert the sort",
"C Sort by GPU usage, press again to reverse",
"M Sort by GPU memory usage, press again to reverse",
"z Toggle the display of kernel threads",
];
const SEARCH_HELP_TEXT: [&str; 51] = [
@ -352,6 +353,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
# Hides advanced options to stop a process on Unix-like systems.
#disable_advanced_kill = false
# Hides the kernel threads
#hide_k_threads = false
# Hide GPU(s) information
#disable_gpu = false
@ -372,7 +376,7 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
#[processes]
# The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built):
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"]
#columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"]
# CPU widget configuration

View File

@ -240,6 +240,9 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
let is_default_command = is_flag_enabled!(process_command, args.process, config);
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
let is_advanced_kill = !(is_flag_enabled!(disable_advanced_kill, args.process, config));
#[cfg(target_os = "linux")]
let hide_k_threads = is_flag_enabled!(hide_k_threads, args.process, config);
let process_memory_as_value = is_flag_enabled!(process_memory_as_value, args.process, config);
let is_default_tree_collapsed = is_flag_enabled!(tree_collapse, args.process, config);
@ -316,6 +319,8 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
),
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
is_advanced_kill,
#[cfg(target_os = "linux")]
hide_k_threads,
memory_legend_position,
network_legend_position,
network_scale_type,

View File

@ -324,6 +324,15 @@ pub struct ProcessArgs {
)]
pub disable_advanced_kill: bool,
#[cfg(target_os = "linux")]
#[arg(
long,
action = ArgAction::SetTrue,
help = "Hide kernel threads by default.",
alias = "hide-k-threads"
)]
pub hide_k_threads: bool,
#[arg(
long,
action = ArgAction::SetTrue,

View File

@ -40,6 +40,8 @@ pub(crate) struct GeneralConfig {
pub(crate) process_command: Option<bool>,
// #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
pub(crate) disable_advanced_kill: Option<bool>, // This does nothing on Windows, but we leave it enabled to make the config file consistent across platforms.
// #[cfg(target_os = "linux")]
pub(crate) hide_k_threads: Option<bool>,
pub(crate) network_use_bytes: Option<bool>,
pub(crate) network_use_log: Option<bool>,
pub(crate) network_use_binary_prefix: Option<bool>,

View File

@ -233,6 +233,8 @@ pub struct ProcWidgetState {
pub is_sort_open: bool,
pub force_rerender: bool,
pub force_update_data: bool,
#[cfg(target_os = "linux")]
pub hide_k_threads: bool,
}
impl ProcWidgetState {
@ -434,6 +436,8 @@ impl ProcWidgetState {
force_update_data: false,
default_sort_index,
default_sort_order,
#[cfg(target_os = "linux")]
hide_k_threads: config.hide_k_threads,
};
table.sort_table.set_data(table.column_text());
@ -514,6 +518,11 @@ impl ProcWidgetState {
.map(|q| q.check(process, is_using_command))
.unwrap_or(true)
{
#[cfg(target_os = "linux")]
if self.hide_k_threads && process.process_type.is_kernel() {
return None;
}
Some(*pid)
} else {
None
@ -759,6 +768,11 @@ impl ProcWidgetState {
let is_mem_percent = self.is_mem_percent();
let filtered_iter = process_harvest.values().filter(|process| {
#[cfg(target_os = "linux")]
if self.hide_k_threads && process.process_type.is_kernel() {
return false;
}
search_query
.as_ref()
.map(|query| query.check(process, is_using_command))
@ -892,6 +906,12 @@ impl ProcWidgetState {
self.force_update_data = true;
}
#[cfg(target_os = "linux")]
pub fn toggle_k_thread(&mut self) {
self.hide_k_threads = !self.hide_k_threads;
self.force_rerender_and_update();
}
/// Marks the selected column as hidden, and automatically resets the
/// selected column to the default sort index and order.
fn hide_column(&mut self, column: ProcWidgetColumn) {
@ -1150,6 +1170,9 @@ mod test {
use super::*;
use crate::widgets::MemUsage;
#[cfg(target_os = "linux")]
use crate::collection::processes::ProcessType;
#[test]
fn test_proc_sort() {
let a = ProcWidgetData {
@ -1660,4 +1683,60 @@ mod test {
assert!(!expanded_by_default.is_collapsed(1));
}
}
#[cfg(target_os = "linux")]
/// Sanity test to ensure kernel thread processes are toggled
#[test]
fn test_toggle_k_threads() {
let init_columns = [
ProcWidgetColumn::ProcNameOrCommand,
ProcWidgetColumn::PidOrCount,
ProcWidgetColumn::State,
ProcWidgetColumn::Mem,
ProcWidgetColumn::ProcNameOrCommand,
];
let mut state = init_default_state(&init_columns);
let process_harvest = ProcessHarvest {
pid: 1,
..Default::default()
};
let k_process_harvest = ProcessHarvest {
pid: 2,
process_type: ProcessType::Kernel,
..Default::default()
};
// test get_normal_data default is filtered by toggle_k_thread
let mut normal_proc_harvest: BTreeMap<Pid, ProcessHarvest> = BTreeMap::new();
normal_proc_harvest.insert(1, process_harvest.clone());
normal_proc_harvest.insert(2, k_process_harvest.clone());
let default_normal_results = state.get_normal_data(&normal_proc_harvest).len();
assert!(default_normal_results == 2);
state.toggle_k_thread();
let filtered_normal_results = state.get_normal_data(&normal_proc_harvest).len();
assert!(filtered_normal_results == 1);
// test that get_normal_data in grouped mode is still filtered
state.mode = ProcWidgetMode::Grouped;
let filtered_grouped_results = state.get_normal_data(&normal_proc_harvest).len();
assert!(filtered_grouped_results == 1);
// test that get_tree_data is filtered on toggle_k_thread
let tree_collapsed = TreeCollapsed::new(false);
state.mode = ProcWidgetMode::Tree(tree_collapsed.clone());
state.hide_k_threads = false;
let mut tree_proc_data = ProcessData::default();
tree_proc_data.process_harvest.insert(1, process_harvest);
tree_proc_data.process_harvest.insert(2, k_process_harvest);
tree_proc_data.orphan_pids = vec![1, 2];
let tree_stored_data = StoredData {
process_data: tree_proc_data,
..Default::default()
};
let default_tree_results = state
.get_tree_data(&tree_collapsed, &tree_stored_data)
.len();
assert!(default_tree_results == 2);
state.toggle_k_thread();
let filtered_tree_results = state
.get_tree_data(&tree_collapsed, &tree_stored_data)
.len();
assert!(filtered_tree_results == 1);
}
}