From 8c3a1848e158600aa4a88316e9693fc346f380fd Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 01:31:40 +0300 Subject: [PATCH] daemon: more sophisticated adaptive polling There are gains to be had. --- src/daemon.rs | 344 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 304 insertions(+), 40 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index dfa4ebb..9b03e18 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -10,6 +10,255 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; +/// Tracks historical system data for advanced adaptive polling +struct SystemHistory { + /// Last several CPU usage measurements + cpu_usage_history: Vec, + /// Last several temperature readings + temperature_history: Vec, + /// Time of last detected user activity + last_user_activity: Instant, + /// Previous battery percentage (to calculate discharge rate) + last_battery_percentage: Option, + /// Timestamp of last battery reading + last_battery_timestamp: Option, + /// Battery discharge rate (%/hour) + battery_discharge_rate: Option, + /// Time spent in each system state + state_durations: std::collections::HashMap, + /// Last time a state transition happened + last_state_change: Instant, + /// Current system state + current_state: SystemState, +} + +impl SystemHistory { + fn new() -> Self { + Self { + cpu_usage_history: Vec::with_capacity(5), + temperature_history: Vec::with_capacity(5), + last_user_activity: Instant::now(), + last_battery_percentage: None, + last_battery_timestamp: None, + battery_discharge_rate: None, + state_durations: std::collections::HashMap::new(), + last_state_change: Instant::now(), + current_state: SystemState::Unknown, + } + } + + /// Update system history with new report data + fn update(&mut self, report: &SystemReport) { + // Update CPU usage history + if !report.cpu_cores.is_empty() { + // Get average CPU usage across all cores + let total: f32 = report + .cpu_cores + .iter() + .filter_map(|core| core.usage_percent) + .sum(); + let count = report + .cpu_cores + .iter() + .filter(|c| c.usage_percent.is_some()) + .count(); + + if count > 0 { + let avg_usage = total / count as f32; + + // Keep only the last 5 measurements + if self.cpu_usage_history.len() >= 5 { + self.cpu_usage_history.remove(0); + } + self.cpu_usage_history.push(avg_usage); + + // Update last_user_activity if CPU usage indicates activity + // Consider significant CPU usage or sudden change as user activity + if avg_usage > 20.0 + || (self.cpu_usage_history.len() > 1 + && (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2]) + .abs() + > 15.0) + { + self.last_user_activity = Instant::now(); + debug!("User activity detected based on CPU usage"); + } + } + } + + // Update temperature history + if let Some(temp) = report.cpu_global.average_temperature_celsius { + if self.temperature_history.len() >= 5 { + self.temperature_history.remove(0); + } + self.temperature_history.push(temp); + + // Significant temperature increase can indicate user activity + if self.temperature_history.len() > 1 { + let temp_change = + temp - self.temperature_history[self.temperature_history.len() - 2]; + if temp_change > 5.0 { + // 5°C rise in temperature + self.last_user_activity = Instant::now(); + debug!("User activity detected based on temperature change"); + } + } + } + + // Update battery discharge rate + if let Some(battery) = report.batteries.first() { + // Reset when we are charging or have just connected AC + if battery.ac_connected { + self.battery_discharge_rate = None; + self.last_battery_percentage = None; + self.last_battery_timestamp = None; + return; + } + + if let Some(current_percentage) = battery.capacity_percent { + let current_percent = f32::from(current_percentage); + + if let (Some(last_percentage), Some(last_timestamp)) = + (self.last_battery_percentage, self.last_battery_timestamp) + { + let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0; + if elapsed_hours > 0.0 && !battery.ac_connected { + // Calculate discharge rate in percent per hour + let percent_change = last_percentage - current_percent; + if percent_change > 0.0 { + // Only if battery is discharging + let hourly_rate = percent_change / elapsed_hours; + self.battery_discharge_rate = Some(hourly_rate); + } + } + } + + self.last_battery_percentage = Some(current_percent); + self.last_battery_timestamp = Some(Instant::now()); + } + } + + // Update system state tracking + let new_state = determine_system_state(report); + if new_state != self.current_state { + // Record time spent in previous state + let time_in_state = self.last_state_change.elapsed(); + *self + .state_durations + .entry(self.current_state.clone()) + .or_insert(Duration::ZERO) += time_in_state; + + // State changes (except to Idle) likely indicate user activity + if new_state != SystemState::Idle && new_state != SystemState::LowLoad { + self.last_user_activity = Instant::now(); + debug!("User activity detected based on system state change to {new_state:?}"); + } + + // Update state + self.current_state = new_state; + self.last_state_change = Instant::now(); + } + + // Check for significant load changes + if report.system_load.load_avg_1min > 1.0 { + self.last_user_activity = Instant::now(); + debug!("User activity detected based on system load"); + } + } + + /// Calculate CPU usage volatility (how much it's changing) + fn get_cpu_volatility(&self) -> f32 { + if self.cpu_usage_history.len() < 2 { + return 0.0; + } + + let mut sum_of_changes = 0.0; + for i in 1..self.cpu_usage_history.len() { + sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs(); + } + + sum_of_changes / (self.cpu_usage_history.len() - 1) as f32 + } + + /// Calculate temperature volatility + fn get_temperature_volatility(&self) -> f32 { + if self.temperature_history.len() < 2 { + return 0.0; + } + + let mut sum_of_changes = 0.0; + for i in 1..self.temperature_history.len() { + sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs(); + } + + sum_of_changes / (self.temperature_history.len() - 1) as f32 + } + + /// Determine if the system appears to be idle + fn is_system_idle(&self) -> bool { + if self.cpu_usage_history.is_empty() { + return false; + } + + // System considered idle if the average CPU usage of last readings is below 10% + let recent_avg = + self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; + recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 + } + + /// Calculate optimal polling interval based on system conditions + fn calculate_optimal_interval(&self, config: &AppConfig, on_battery: bool) -> u64 { + let base_interval = config.daemon.poll_interval_sec; + let min_interval = config.daemon.min_poll_interval_sec; + let max_interval = config.daemon.max_poll_interval_sec; + + // Start with base interval + let mut adjusted_interval = base_interval; + + // If we're on battery, we want to be more aggressive about saving power + if on_battery { + // Apply a multiplier based on battery discharge rate + if let Some(discharge_rate) = self.battery_discharge_rate { + if discharge_rate > 20.0 { + // High discharge rate - increase polling interval significantly + adjusted_interval = (adjusted_interval as f32 * 3.0) as u64; + } else if discharge_rate > 10.0 { + // Moderate discharge - double polling interval + adjusted_interval *= 2; + } else { + // Low discharge rate - increase by 50% + adjusted_interval = (adjusted_interval as f32 * 1.5) as u64; + } + } else { + // If we don't know discharge rate, use a conservative multiplier + adjusted_interval *= 2; + } + } + + // Adjust for system idleness + if self.is_system_idle() { + // If the system has been idle for a while, increase interval + let idle_time = self.last_user_activity.elapsed().as_secs(); + if idle_time > 300 { + // 5 minutes + adjusted_interval = (adjusted_interval as f32 * 2.0) as u64; + } + } + + // Adjust for CPU/temperature volatility + let cpu_volatility = self.get_cpu_volatility(); + let temp_volatility = self.get_temperature_volatility(); + + // If either CPU usage or temperature is changing rapidly, decrease interval + if cpu_volatility > 10.0 || temp_volatility > 2.0 { + adjusted_interval = (adjusted_interval as f32 * 0.5) as u64; + } + + // Ensure interval stays within configured bounds + adjusted_interval.clamp(min_interval, max_interval) + } +} + /// Run the daemon pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box> { // Set effective log level based on config and verbose flag @@ -84,8 +333,7 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box Result<(), Box { error!("Error loading new configuration: {e}"); @@ -115,8 +363,11 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box { debug!("Collected system report, applying settings..."); - // Determine current system state - let current_state = determine_system_state(&report); + // Store the current state before updating history + let previous_state = system_history.current_state.clone(); + + // Update system history with new data + system_history.update(&report); // Update the stats file if configured if let Some(stats_path) = &config.daemon.stats_file_path { @@ -129,12 +380,12 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box { debug!("Successfully applied system settings"); - // If system state changed or settings were applied differently, record the time - if current_state != last_system_state { - last_settings_change = Instant::now(); - last_system_state = current_state.clone(); - - info!("System state changed to: {current_state:?}"); + // If system state changed, log the new state + if system_history.current_state != previous_state { + info!( + "System state changed to: {:?}", + system_history.current_state + ); } } Err(e) => { @@ -142,38 +393,39 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box 60 { - current_poll_interval = - (current_poll_interval * 2).min(config.daemon.max_poll_interval_sec); - - debug!("Adaptive polling: increasing interval to {current_poll_interval}s"); - } else if time_since_change < 10 { - // If we've had recent changes, decrease the interval (down to min) - current_poll_interval = - (current_poll_interval / 2).max(config.daemon.min_poll_interval_sec); - - debug!("Adaptive polling: decreasing interval to {current_poll_interval}s"); + // Don't change the interval too dramatically at once + if optimal_interval > current_poll_interval { + current_poll_interval = (current_poll_interval + optimal_interval) / 2; + } else if optimal_interval < current_poll_interval { + current_poll_interval = current_poll_interval + - ((current_poll_interval - optimal_interval) / 2).max(1); } + + debug!("Adaptive polling: set interval to {current_poll_interval}s"); } else { - // If not adaptive, use the configured poll interval - current_poll_interval = config.daemon.poll_interval_sec; - } + // If adaptive polling is disabled, still apply battery-saving adjustment + if config.daemon.throttle_on_battery && on_battery { + let battery_multiplier = 2; // Poll half as often on battery + current_poll_interval = (config.daemon.poll_interval_sec + * battery_multiplier) + .min(config.daemon.max_poll_interval_sec); - // If on battery and throttling is enabled, lengthen the poll interval to save power - if config.daemon.throttle_on_battery - && !report.batteries.is_empty() - && report.batteries.first().is_some_and(|b| !b.ac_connected) - { - let battery_multiplier = 2; // Poll half as often on battery - current_poll_interval = (current_poll_interval * battery_multiplier) - .min(config.daemon.max_poll_interval_sec); - - debug!("On battery power, increasing poll interval to save energy"); + debug!( + "On battery power, increased poll interval to {current_poll_interval}s" + ); + } else { + // Use the configured poll interval + current_poll_interval = config.daemon.poll_interval_sec; + } } } Err(e) => { @@ -227,7 +479,7 @@ fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Er } /// Simplified system state used for determining when to adjust polling interval -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] enum SystemState { Unknown, OnAC, @@ -235,6 +487,7 @@ enum SystemState { HighLoad, LowLoad, HighTemp, + Idle, } /// Determine the current system state for adaptive polling @@ -261,6 +514,17 @@ fn determine_system_state(report: &SystemReport) -> SystemState { } } + // Check idle state by checking very low CPU usage + let is_idle = report + .cpu_cores + .iter() + .filter_map(|c| c.usage_percent) + .all(|usage| usage < 5.0); + + if is_idle { + return SystemState::Idle; + } + // Check load let avg_load = report.system_load.load_avg_1min; if avg_load > 3.0 {