From 118bb5e0611033b7d358066494d2eba55b895e53 Mon Sep 17 00:00:00 2001 From: Justin Martin Date: Sun, 12 Oct 2025 19:08:55 +0000 Subject: [PATCH] feature: free arc (#1812) * feature: free arc * freebsd clippy no-default-features * add alias to free_arc * add get_threads to default config * clippy * code review: combine zfs feature and target_os build cfgs --- .../configuration/command-line-options.md | 3 +- .../configuration/config-file/flags.md | 3 +- sample_configs/default_config.toml | 6 +++ schema/nightly/bottom.json | 6 +++ src/app.rs | 2 + src/collection.rs | 47 +++++++++++++++++-- src/collection/disks/freebsd.rs | 1 + src/collection/memory/arc.rs | 42 ++++++++++------- src/constants.rs | 6 +++ src/lib.rs | 4 ++ src/options.rs | 4 ++ src/options/args.rs | 9 ++++ src/options/config/flags.rs | 2 + 13 files changed, 111 insertions(+), 24 deletions(-) diff --git a/docs/content/configuration/command-line-options.md b/docs/content/configuration/command-line-options.md index 64cf1e38..b4b99b2d 100644 --- a/docs/content/configuration/command-line-options.md +++ b/docs/content/configuration/command-line-options.md @@ -35,7 +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. | +| `--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. | @@ -66,6 +66,7 @@ see information on these options by running `btm -h`, or run `btm --help` to dis | ---------------------------- | --------------------------------------------------------- | | `--memory_legend ` | Where to place the legend for the memory chart widget. | | `--enable_cache_memory` | Enable collecting and displaying cache and buffer memory. | +| `--free_arc` | Subtract freeable ARC from memory. | ## Network Options diff --git a/docs/content/configuration/config-file/flags.md b/docs/content/configuration/config-file/flags.md index 2a91e37c..3c9e056f 100644 --- a/docs/content/configuration/config-file/flags.md +++ b/docs/content/configuration/config-file/flags.md @@ -52,4 +52,5 @@ 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. | +| `hide_k_threads` | Boolean | Hide kernel threads by default. | +| `free_arc` | Boolean | Subtract freeable ARC from memory. | diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index 296676d6..d9ec2773 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -115,6 +115,9 @@ # Shows cache and buffer memory #enable_cache_memory = false +# Subtract freeable ARC from memory usage +#free_arc = false + # How much data is stored at once in terms of time. #retention = "10m" @@ -131,6 +134,9 @@ # PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU% #columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"] +# Gather process child thread information +#get_threads = false + # CPU widget configuration #[cpu] diff --git a/schema/nightly/bottom.json b/schema/nightly/bottom.json index 1963eb46..9cab73a9 100644 --- a/schema/nightly/bottom.json +++ b/schema/nightly/bottom.json @@ -376,6 +376,12 @@ "null" ] }, + "free_arc": { + "type": [ + "boolean", + "null" + ] + }, "group_processes": { "type": [ "boolean", diff --git a/src/app.rs b/src/app.rs index 20b27de0..2e3188a2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -59,6 +59,8 @@ pub struct AppConfigFields { pub is_advanced_kill: bool, #[cfg(target_os = "linux")] pub hide_k_threads: bool, + #[cfg(feature = "zfs")] + pub free_arc: bool, pub memory_legend_position: Option, // TODO: Remove these, move network details state-side. pub network_unit_type: DataUnit, diff --git a/src/collection.rs b/src/collection.rs index 93a16fa1..a39b049e 100644 --- a/src/collection.rs +++ b/src/collection.rs @@ -184,6 +184,8 @@ pub struct DataCollector { gpu_pids: Option>>, #[cfg(feature = "gpu")] gpus_total_mem: Option, + #[cfg(feature = "zfs")] + free_arc_mem: bool, } const LESS_ROUTINE_TASK_TIME: Duration = Duration::from_secs(60); @@ -222,6 +224,8 @@ impl DataCollector { gpu_pids: None, #[cfg(feature = "gpu")] gpus_total_mem: None, + #[cfg(feature = "zfs")] + free_arc_mem: false, last_list_collection_time: last_collection_time, should_run_less_routine_tasks: true, } @@ -267,6 +271,11 @@ impl DataCollector { self.get_process_threads = get_process_threads; } + #[cfg(feature = "zfs")] + pub fn set_free_arc_mem(&mut self, free_mem: bool) { + self.free_arc_mem = free_mem; + } + /// Refresh sysinfo data. We use sysinfo for the following data: /// - CPU usage /// - Memory usage @@ -463,17 +472,45 @@ impl DataCollector { if self.widgets_to_harvest.use_mem { self.data.memory = memory::get_ram_usage(&self.sys.system); + #[cfg(feature = "zfs")] + { + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + if let Some(arc) = memory::arc::get_arc_usage() { + if let Some(mem) = &mut self.data.memory { + if self.free_arc_mem { + if arc.0.used_bytes > arc.1 { + #[cfg(target_os = "linux")] + { + mem.used_bytes -= arc.0.used_bytes.saturating_sub(arc.1); // keep arc min like htop + } + #[cfg(target_os = "freebsd")] + { + mem.used_bytes += arc.1; // sysinfo subtracts arc_size on freebsd + } + } else { + #[cfg(target_os = "freebsd")] + { + mem.used_bytes += arc.0.used_bytes; + } + } + } else { + #[cfg(target_os = "freebsd")] + { + mem.used_bytes += arc.0.used_bytes; + } + } + } + + self.data.arc = Some(arc.0); + } + } + #[cfg(not(target_os = "windows"))] if self.widgets_to_harvest.use_cache { self.data.cache = memory::get_cache_usage(&self.sys.system); } self.data.swap = memory::get_swap_usage(&self.sys.system); - - #[cfg(feature = "zfs")] - { - self.data.arc = memory::arc::get_arc_usage(); - } } } diff --git a/src/collection/disks/freebsd.rs b/src/collection/disks/freebsd.rs index 90f2c13d..723ffe1f 100644 --- a/src/collection/disks/freebsd.rs +++ b/src/collection/disks/freebsd.rs @@ -27,6 +27,7 @@ struct FileSystem { pub fn get_io_usage() -> CollectionResult { // TODO: Should this (and other I/O collectors) fail fast? In general, should // collection ever fail fast? + #[cfg_attr(not(feature = "zfs"), expect(unused_mut))] let mut io_harvest: HashMap> = get_disk_info().map(|storage_system_information| { storage_system_information diff --git a/src/collection/memory/arc.rs b/src/collection/memory/arc.rs index 490f31cc..d09438e8 100644 --- a/src/collection/memory/arc.rs +++ b/src/collection/memory/arc.rs @@ -1,11 +1,11 @@ +#[cfg(all(feature = "zfs", any(target_os = "linux", target_os = "freebsd")))] use super::MemData; - +#[cfg(all(feature = "zfs", any(target_os = "linux", target_os = "freebsd")))] /// Return ARC usage. -#[cfg(feature = "zfs")] -pub(crate) fn get_arc_usage() -> Option { +pub(crate) fn get_arc_usage() -> Option<(MemData, u64)> { use std::num::NonZeroU64; - let (mem_total, mem_used) = { + let (mem_total, mem_used, mem_min) = { cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { // TODO: [OPT] is this efficient? @@ -13,14 +13,16 @@ pub(crate) fn get_arc_usage() -> Option { if let Ok(arc_stats) = read_to_string("/proc/spl/kstat/zfs/arcstats") { let mut mem_arc = 0; let mut mem_total = 0; + let mut mem_min = 0; let mut zfs_keys_read: u8 = 0; - const ZFS_KEYS_NEEDED: u8 = 2; + const ZFS_KEYS_NEEDED: u8 = 3; for line in arc_stats.lines() { if let Some((label, value)) = line.split_once(' ') { let to_write = match label { "size" => &mut mem_arc, "c_max" => &mut mem_total, + "c_min" => &mut mem_min, _ => { continue; } @@ -39,34 +41,40 @@ pub(crate) fn get_arc_usage() -> Option { } } } - (mem_total, mem_arc) + (mem_total, mem_arc, mem_min) } else { - (0, 0) + (0, 0, 0) } } else if #[cfg(target_os = "freebsd")] { use sysctl::Sysctl; - if let (Ok(mem_arc_value), Ok(mem_sys_value)) = ( + if let (Ok(mem_arc_value), Ok(mem_sys_value), Ok(mem_min_value)) = ( sysctl::Ctl::new("kstat.zfs.misc.arcstats.size"), sysctl::Ctl::new("kstat.zfs.misc.arcstats.c_max"), + sysctl::Ctl::new("kstat.zfs.misc.arcstats.c_min"), ) { - if let (Ok(sysctl::CtlValue::U64(arc)), Ok(sysctl::CtlValue::Ulong(mem))) = - (mem_arc_value.value(), mem_sys_value.value()) + if let (Ok(sysctl::CtlValue::U64(arc)), Ok(sysctl::CtlValue::U64(mem)), Ok(sysctl::CtlValue::U64(min))) = + (mem_arc_value.value(), mem_sys_value.value(), mem_min_value.value()) { - (mem, arc) + (mem, arc, min) } else { - (0, 0) + (0, 0, 0) } } else { - (0, 0) + (0, 0, 0) } } else { - (0, 0) + (0, 0, 0) } } }; - NonZeroU64::new(mem_total).map(|total_bytes| MemData { - total_bytes, - used_bytes: mem_used, + NonZeroU64::new(mem_total).map(|total_bytes| { + ( + MemData { + total_bytes, + used_bytes: mem_used, + }, + mem_min, + ) }) } diff --git a/src/constants.rs b/src/constants.rs index a7113d15..4554b952 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -365,6 +365,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott # Shows cache and buffer memory #enable_cache_memory = false +# Subtract freeable ARC from memory usage +#free_arc = false + # How much data is stored at once in terms of time. #retention = "10m" @@ -381,6 +384,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott # PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU% #columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"] +# Gather process child thread information +#get_threads = false + # CPU widget configuration #[cpu] diff --git a/src/lib.rs b/src/lib.rs index 9b48f11e..97cd2a8a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -228,6 +228,8 @@ fn create_collection_thread( 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; + #[cfg(feature = "zfs")] + let get_arc_free = app_config_fields.free_arc; thread::spawn(move || { let mut data_collector = collection::DataCollector::new(filters); @@ -237,6 +239,8 @@ fn create_collection_thread( 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); + #[cfg(feature = "zfs")] + data_collector.set_free_arc_mem(get_arc_free); data_collector.update_data(); data_collector.data = Data::default(); diff --git a/src/options.rs b/src/options.rs index 42f275db..702515d6 100644 --- a/src/options.rs +++ b/src/options.rs @@ -230,6 +230,8 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL let use_basic_mode = is_flag_enabled!(basic, args.general, config); let expanded = is_flag_enabled!(expanded, args.general, config); + #[cfg(feature = "zfs")] + let free_arc = is_flag_enabled!(free_arc, args.memory, config); // For processes let is_grouped = is_flag_enabled!(group_processes, args.process, config); @@ -329,6 +331,8 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL retention_ms, dedicated_average_row: get_dedicated_avg_row(config), default_tree_collapse: is_default_tree_collapsed, + #[cfg(feature = "zfs")] + free_arc, }; let table_config = ProcTableConfig { diff --git a/src/options/args.rs b/src/options/args.rs index cae2b382..8743d08d 100644 --- a/src/options/args.rs +++ b/src/options/args.rs @@ -518,6 +518,15 @@ pub struct MemoryArgs { alias = "enable-cache-memory" )] pub enable_cache_memory: bool, + + #[cfg(feature = "zfs")] + #[arg( + long, + action = ArgAction::SetTrue, + help = "Subtract reclaimable ARC from memory.", + alias = "free-arc" + )] + pub free_arc: bool, } /// Network arguments/config options. diff --git a/src/options/config/flags.rs b/src/options/config/flags.rs index 591d791a..375db3d2 100644 --- a/src/options/config/flags.rs +++ b/src/options/config/flags.rs @@ -42,6 +42,8 @@ pub(crate) struct GeneralConfig { pub(crate) disable_advanced_kill: Option, // 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, + // #[cfg(feature = "zfs")] + pub(crate) free_arc: Option, pub(crate) network_use_bytes: Option, pub(crate) network_use_log: Option, pub(crate) network_use_binary_prefix: Option,