//! This is the main file to house data collection functions. //! //! TODO: Rename this to intake? Collection? #[cfg(feature = "nvidia")] pub mod nvidia; #[cfg(all(target_os = "linux", feature = "gpu"))] pub mod amd; #[cfg(target_os = "linux")] mod linux { pub mod cgroups; pub mod utils; } #[cfg(feature = "battery")] pub mod batteries; pub mod cpu; pub mod disks; pub mod error; pub mod memory; pub mod network; pub mod processes; pub mod temperature; use std::time::{Duration, Instant}; #[cfg(any(not(target_os = "windows"), feature = "gpu"))] use processes::Pid; #[cfg(feature = "battery")] use starship_battery::{Battery, Manager}; use super::DataFilters; use crate::app::layout_manager::UsedWidgets; #[cfg(target_os = "linux")] use crate::collection::linux::cgroups::{CgroupCpuCollector, CgroupMemCollector}; #[cfg(any(target_os = "linux", feature = "gpu"))] use crate::utils::int_hash::IntHashMap; // TODO: We can possibly reuse an internal buffer for this to reduce allocs. #[derive(Clone, Debug)] pub struct Data { pub collection_time: Instant, pub cpu: Option, pub load_avg: Option, pub memory: Option, #[cfg(not(target_os = "windows"))] pub cache: Option, pub swap: Option, pub temperature_sensors: Option>, pub network: Option, pub list_of_processes: Option>, pub disks: Option>, pub io: Option, #[cfg(feature = "battery")] pub list_of_batteries: Option>, #[cfg(feature = "zfs")] pub arc: Option, #[cfg(feature = "gpu")] pub gpu: Option>, } impl Default for Data { fn default() -> Self { Data { collection_time: Instant::now(), cpu: None, load_avg: None, memory: None, #[cfg(not(target_os = "windows"))] cache: None, swap: None, temperature_sensors: None, list_of_processes: None, disks: None, io: None, network: None, #[cfg(feature = "battery")] list_of_batteries: None, #[cfg(feature = "zfs")] arc: None, #[cfg(feature = "gpu")] gpu: None, } } } impl Data { pub fn cleanup(&mut self) { self.io = None; self.temperature_sensors = None; self.list_of_processes = None; self.disks = None; self.memory = None; self.swap = None; self.cpu = None; self.load_avg = None; if let Some(network) = &mut self.network { network.first_run_cleanup(); } #[cfg(feature = "zfs")] { self.arc = None; } #[cfg(feature = "gpu")] { self.gpu = None; } } } /// A wrapper around the sysinfo data source. We use sysinfo for the following /// data: /// - CPU usage /// - Memory usage /// - Network usage /// - Processes (non-Linux) /// - Disk (anything outside of Linux, macOS, and FreeBSD) /// - Temperatures (non-Linux) #[derive(Debug)] pub struct SysinfoSource { /// Handles CPU, memory, and processes. pub(crate) system: sysinfo::System, pub(crate) network: sysinfo::Networks, #[cfg(not(target_os = "linux"))] pub(crate) temps: sysinfo::Components, #[cfg(not(any(target_os = "linux", target_os = "macos")))] pub(crate) disks: sysinfo::Disks, #[cfg(target_os = "windows")] pub(crate) users: sysinfo::Users, } impl Default for SysinfoSource { fn default() -> Self { use sysinfo::*; Self { system: System::new(), network: Networks::new(), #[cfg(not(target_os = "linux"))] temps: Components::new(), #[cfg(not(any(target_os = "linux", target_os = "macos")))] disks: Disks::new(), #[cfg(target_os = "windows")] users: Users::new(), } } } #[derive(Debug)] pub struct DataCollector { pub data: Data, sys: SysinfoSource, last_collection_time: Instant, widgets_to_harvest: UsedWidgets, filters: DataFilters, include_unmounted_disks: bool, total_rx: u64, total_tx: u64, total_rx_packets: u64, total_tx_packets: u64, unnormalized_cpu: bool, use_current_cpu_total: bool, show_average_cpu: bool, get_process_threads: bool, last_list_collection_time: Instant, should_run_less_routine_tasks: bool, #[cfg(target_os = "linux")] prev_process_details: IntHashMap, #[cfg(target_os = "linux")] prev_idle: f64, #[cfg(target_os = "linux")] prev_non_idle: f64, #[cfg(feature = "battery")] battery_manager: Option, #[cfg(feature = "battery")] battery_list: Option>, #[cfg(unix)] user_table: processes::UserTable, #[cfg(feature = "gpu")] gpu_pids: Option>>, #[cfg(feature = "gpu")] gpus_total_mem: Option, #[cfg(feature = "zfs")] free_arc_mem: bool, #[cfg(target_os = "linux")] cgroup_memory_data: CgroupMemCollector, #[cfg(target_os = "linux")] cgroup_cpu_data: CgroupCpuCollector, } 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(LESS_ROUTINE_TASK_TIME * 10).unwrap_or(now); DataCollector { data: Data::default(), sys: SysinfoSource::default(), #[cfg(target_os = "linux")] prev_process_details: IntHashMap::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, total_rx_packets: 0, total_tx_packets: 0, show_average_cpu: false, widgets_to_harvest: UsedWidgets::default(), #[cfg(feature = "battery")] battery_manager: None, #[cfg(feature = "battery")] battery_list: None, filters, #[cfg(unix)] user_table: Default::default(), #[cfg(feature = "gpu")] 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, #[cfg(target_os = "linux")] cgroup_memory_data: CgroupMemCollector::default(), #[cfg(target_os = "linux")] cgroup_cpu_data: CgroupCpuCollector::default(), include_unmounted_disks: false, } } /// 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] fn run_less_routine_tasks(&mut self) { if self .data .collection_time .duration_since(self.last_list_collection_time) > LESS_ROUTINE_TASK_TIME { self.should_run_less_routine_tasks = true; } if self.should_run_less_routine_tasks { self.last_list_collection_time = self.data.collection_time; } } pub fn set_collection(&mut self, used_widgets: UsedWidgets) { self.widgets_to_harvest = used_widgets; } pub fn set_use_current_cpu_total(&mut self, use_current_cpu_total: bool) { self.use_current_cpu_total = use_current_cpu_total; } pub fn set_unnormalized_cpu(&mut self, unnormalized_cpu: bool) { self.unnormalized_cpu = unnormalized_cpu; } pub fn set_show_average_cpu(&mut self, show_average_cpu: bool) { 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; } pub fn set_include_unmounted_disks(&mut self, include_unmounted_disks: bool) { self.include_unmounted_disks = include_unmounted_disks; } #[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 /// - Network usage /// - Processes (non-Linux) /// - Disk (Windows, FreeBSD) /// - Temperatures (non-Linux) fn refresh_sysinfo_data(&mut self) { // Refresh the list of objects once every minute. If it's too frequent it can // cause segfaults. if self.widgets_to_harvest.use_cpu || self.widgets_to_harvest.use_proc { self.sys.system.refresh_cpu_all(); } if self.widgets_to_harvest.use_mem || self.widgets_to_harvest.use_proc { self.sys.system.refresh_memory(); } if self.widgets_to_harvest.use_net { self.sys.network.refresh(true); } // sysinfo is used on non-Linux systems for the following: // - Processes (users list as well for Windows) // - Disks (Windows/FreeBSD) // - Temperatures and temperature components list. #[cfg(not(target_os = "linux"))] { if self.widgets_to_harvest.use_proc { self.sys.system.refresh_processes_specifics( sysinfo::ProcessesToUpdate::All, true, sysinfo::ProcessRefreshKind::everything() .without_environ() .without_cwd() .without_root(), ); // For Windows, sysinfo also handles the users list. #[cfg(target_os = "windows")] if self.should_run_less_routine_tasks { self.sys.users.refresh(); } } if self.widgets_to_harvest.use_temp || self.widgets_to_harvest.use_temp_graph { if self.should_run_less_routine_tasks { self.sys.temps.refresh(true); } for component in self.sys.temps.iter_mut() { component.refresh(); } } #[cfg(any(target_os = "windows", target_os = "freebsd"))] if self.widgets_to_harvest.use_disk { if self.should_run_less_routine_tasks { self.sys.disks.refresh(true); } for disk in self.sys.disks.iter_mut() { disk.refresh(); } } } } /// Update and refresh data. /// /// TODO: separate refresh steps and update steps pub fn update_data(&mut self) { self.data.collection_time = Instant::now(); self.run_less_routine_tasks(); self.refresh_sysinfo_data(); #[cfg(target_os = "linux")] { let total_memory = self.sys.system.total_memory(); let total_swap = self.sys.system.total_swap(); self.cgroup_memory_data.refresh(total_memory, total_swap); } #[cfg(target_os = "linux")] self.cgroup_cpu_data.refresh(); self.update_cpu_usage(); self.update_memory_usage(); self.update_temps(); #[cfg(feature = "battery")] self.update_batteries(); #[cfg(feature = "gpu")] self.update_gpus(); self.update_processes(); self.update_network_usage(); self.update_disks(); // 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; } /// Gets GPU data. Note this will usually append to other previously /// collected data fields at the moment. #[cfg(feature = "gpu")] #[inline] fn update_gpus(&mut self) { if self.widgets_to_harvest.use_gpu { let mut local_gpu: Vec<(String, memory::MemData)> = Vec::new(); let mut local_gpu_pids: Vec> = Vec::new(); let mut local_gpu_total_mem: u64 = 0; #[cfg(feature = "nvidia")] if let Some(data) = nvidia::get_nvidia_vecs( &self.filters.temp_filter, &self.filters.temp_graph_filter, &self.widgets_to_harvest, ) { if let Some(mut temp) = data.temperature { if let Some(sensors) = &mut self.data.temperature_sensors { sensors.append(&mut temp); } else { self.data.temperature_sensors = Some(temp); } } if let Some(mut mem) = data.memory { local_gpu.append(&mut mem); } if let Some(mut proc) = data.procs { local_gpu_pids.append(&mut proc.1); local_gpu_total_mem += proc.0; } } #[cfg(target_os = "linux")] if let Some(data) = amd::get_amd_vecs(&self.widgets_to_harvest, self.last_collection_time) { if let Some(mut mem) = data.memory { local_gpu.append(&mut mem); } if let Some(mut proc) = data.procs { local_gpu_pids.append(&mut proc.1); local_gpu_total_mem += proc.0; } } self.data.gpu = (!local_gpu.is_empty()).then_some(local_gpu); self.gpu_pids = (!local_gpu_pids.is_empty()).then_some(local_gpu_pids); self.gpus_total_mem = (local_gpu_total_mem > 0).then_some(local_gpu_total_mem); } } #[inline] fn update_cpu_usage(&mut self) { if self.widgets_to_harvest.use_cpu { self.data.cpu = cpu::get_cpu_data_list(self).ok(); #[cfg(unix)] { self.data.load_avg = Some(cpu::get_load_avg()); } } } #[inline] fn update_processes(&mut self) { if self.widgets_to_harvest.use_proc && let Ok(mut process_list) = self.get_processes() { // NB: To avoid duplicate sorts on rerenders/events, we sort the processes by // PID here. We also want to avoid re-sorting *again* later on // if we're sorting by PID, since we already did it here! process_list.sort_unstable_by_key(|p| p.pid); self.data.list_of_processes = Some(process_list); } } #[inline] fn update_temps(&mut self) { if self.widgets_to_harvest.use_temp || self.widgets_to_harvest.use_temp_graph { #[cfg(not(target_os = "linux"))] if let Ok(data) = temperature::get_temperature_data( &self.sys.temps, &self.filters.temp_filter, &self.filters.temp_graph_filter, ) { self.data.temperature_sensors = data; } #[cfg(target_os = "linux")] if let Ok(data) = temperature::get_temperature_data( &self.filters.temp_filter, &self.filters.temp_graph_filter, ) { self.data.temperature_sensors = data; } } } #[inline] fn update_memory_usage(&mut self) { if self.widgets_to_harvest.use_mem { self.data.memory = memory::get_ram_usage(self); #[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")] { // Keep arc min like htop; the subtraction below won't underflow because of // the above check. mem.used_bytes = mem.used_bytes.saturating_sub(arc.0.used_bytes - arc.1); } #[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); } } #[inline] fn update_network_usage(&mut self) { if self.widgets_to_harvest.use_net { let net_data = network::get_network_data( &self.sys.network, self.last_collection_time, &mut self.total_rx, &mut self.total_tx, &mut self.total_rx_packets, &mut self.total_tx_packets, self.data.collection_time, &self.filters.net_filter, ); self.total_rx = net_data.total_rx; self.total_tx = net_data.total_tx; self.total_rx_packets = net_data.total_rx_packets; self.total_tx_packets = net_data.total_tx_packets; self.data.network = Some(net_data); } } /// Update battery information. /// /// 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_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_run_less_routine_tasks { let battery_list = manager .batteries() .map(|batteries| batteries.filter_map(Result::ok).collect::>()); if let Ok(battery_list) = battery_list { if battery_list.is_empty() { self.battery_list = None; } else { self.battery_list = Some(battery_list); } } else { self.battery_list = None; } } manager } None => { if let Ok(manager) = Manager::new() { let Ok(batteries) = manager.batteries() else { return; }; let battery_list = batteries.filter_map(Result::ok).collect::>(); if battery_list.is_empty() { return; } self.battery_list = Some(battery_list); self.battery_manager.insert(manager) } else { return; } } }; self.data.list_of_batteries = self .battery_list .as_mut() .map(|battery_list| batteries::refresh_batteries(battery_manager, battery_list)); } #[inline] fn update_disks(&mut self) { if self.widgets_to_harvest.use_disk || self.widgets_to_harvest.use_disk_io_graph { self.data.disks = disks::get_disk_usage(self).ok(); self.data.io = disks::get_io_usage(self).ok(); } } /// Returns the total memory of the system. #[inline] fn total_memory(&self) -> u64 { if let Some(memory) = &self.data.memory { memory.total_bytes.get() } else { self.sys.system.total_memory() } } } #[cfg(target_os = "freebsd")] /// Deserialize [libxo](https://www.freebsd.org/cgi/man.cgi?query=libxo&apropos=0&sektion=0&manpath=FreeBSD+13.1-RELEASE+and+Ports&arch=default&format=html) JSON data fn deserialize_xo(key: &str, data: &[u8]) -> Result where T: serde::de::DeserializeOwned, { let mut value: serde_json::Value = serde_json::from_slice(data)?; value .as_object_mut() .and_then(|map| map.remove(key)) .ok_or_else(|| std::io::Error::other("key not found")) .and_then(|val| serde_json::from_value(val).map_err(|err| err.into())) } #[cfg(test)] mod tests { use super::*; #[test] fn test_data_collection() { let mut collector = DataCollector::new(DataFilters { disk_filter: None, mount_filter: None, temp_filter: None, temp_graph_filter: None, disk_io_graph_filter: None, net_filter: None, }); // #[cfg(feature = "battery")] // { // collector.widgets_to_harvest.use_battery = true; // } // #[cfg(feature = "zfs")] // { // collector.widgets_to_harvest.use_cache = true; // } // #[cfg(feature = "gpu")] // { // collector.widgets_to_harvest.use_gpu = true; // } collector.widgets_to_harvest.use_cpu = true; collector.widgets_to_harvest.use_disk = true; collector.widgets_to_harvest.use_mem = true; collector.widgets_to_harvest.use_net = true; collector.widgets_to_harvest.use_proc = true; collector.widgets_to_harvest.use_temp = true; collector.update_data(); let data = collector.data; assert!(!data.cpu.unwrap().is_empty()); assert!(!data.disks.unwrap().is_empty()); assert!(data.memory.is_some()); assert!(data.network.is_some()); assert!(!data.list_of_processes.unwrap().is_empty()); assert!(data.temperature_sensors.is_some()); } }