From 8c3a1848e158600aa4a88316e9693fc346f380fd Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 01:31:40 +0300 Subject: [PATCH 01/21] 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 { From 264cd6a4e93146f11d47c016f9755da0007cf764 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 01:52:35 +0300 Subject: [PATCH 02/21] daemon change CPU and temperature history to use `VecDeque ` --- src/daemon.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 9b03e18..3b90685 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -4,6 +4,7 @@ use crate::core::SystemReport; use crate::engine; use crate::monitor; use log::{LevelFilter, debug, error, info, warn}; +use std::collections::VecDeque; use std::fs::File; use std::io::Write; use std::sync::Arc; @@ -13,9 +14,9 @@ use std::time::{Duration, Instant}; /// Tracks historical system data for advanced adaptive polling struct SystemHistory { /// Last several CPU usage measurements - cpu_usage_history: Vec, + cpu_usage_history: VecDeque, /// Last several temperature readings - temperature_history: Vec, + temperature_history: VecDeque, /// Time of last detected user activity last_user_activity: Instant, /// Previous battery percentage (to calculate discharge rate) @@ -35,8 +36,8 @@ struct SystemHistory { impl SystemHistory { fn new() -> Self { Self { - cpu_usage_history: Vec::with_capacity(5), - temperature_history: Vec::with_capacity(5), + cpu_usage_history: VecDeque::with_capacity(5), + temperature_history: VecDeque::with_capacity(5), last_user_activity: Instant::now(), last_battery_percentage: None, last_battery_timestamp: None, @@ -68,9 +69,9 @@ impl SystemHistory { // Keep only the last 5 measurements if self.cpu_usage_history.len() >= 5 { - self.cpu_usage_history.remove(0); + self.cpu_usage_history.pop_front(); } - self.cpu_usage_history.push(avg_usage); + self.cpu_usage_history.push_back(avg_usage); // Update last_user_activity if CPU usage indicates activity // Consider significant CPU usage or sudden change as user activity @@ -89,9 +90,9 @@ impl SystemHistory { // 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.pop_front(); } - self.temperature_history.push(temp); + self.temperature_history.push_back(temp); // Significant temperature increase can indicate user activity if self.temperature_history.len() > 1 { From 7047f649840eb647ad8093488a3fb97717d049ff Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 02:08:21 +0300 Subject: [PATCH 03/21] daemon: enforce user-configured min and max polling interval limits --- src/daemon.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/daemon.rs b/src/daemon.rs index 3b90685..264bf74 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -411,6 +411,12 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box Date: Sat, 17 May 2025 02:12:54 +0300 Subject: [PATCH 04/21] daemon: reset battery discharge tracking on AC connection; preserve history --- src/daemon.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/daemon.rs b/src/daemon.rs index 264bf74..850a842 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -110,10 +110,11 @@ impl SystemHistory { if let Some(battery) = report.batteries.first() { // Reset when we are charging or have just connected AC if battery.ac_connected { + // Reset discharge tracking but continue updating the rest of + // the history so we still detect activity/load changes on AC. self.battery_discharge_rate = None; self.last_battery_percentage = None; self.last_battery_timestamp = None; - return; } if let Some(current_percentage) = battery.capacity_percent { From 7431c2282588e8cd7431e8c3de9f1540d98eaed2 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 02:26:51 +0300 Subject: [PATCH 05/21] daemon: refactor polling interval calculation into a separate function --- src/daemon.rs | 110 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 850a842..bb96d53 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -11,6 +11,61 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; +/// Calculate optimal polling interval based on system conditions and history +fn compute_new( + base_interval: u64, + min_interval: u64, + max_interval: u64, + cpu_volatility: f32, + temp_volatility: f32, + battery_discharge_rate: Option, + last_user_activity: Duration, + is_system_idle: bool, + on_battery: bool, +) -> u64 { + // 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) = 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 is_system_idle { + // If the system has been idle for a while, increase interval + let idle_time = last_user_activity.as_secs(); + if idle_time > 300 { + // 5 minutes + adjusted_interval = (adjusted_interval as f32 * 2.0) as u64; + } + } + + // Adjust for CPU/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) +} + /// Tracks historical system data for advanced adaptive polling struct SystemHistory { /// Last several CPU usage measurements @@ -214,50 +269,17 @@ impl SystemHistory { 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) + compute_new( + base_interval, + min_interval, + max_interval, + self.get_cpu_volatility(), + self.get_temperature_volatility(), + self.battery_discharge_rate, + self.last_user_activity.elapsed(), + self.is_system_idle(), + on_battery, + ) } } From f71534f7efdcc1a0dc3037b02ba5a18bcf365fb0 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 02:45:57 +0300 Subject: [PATCH 06/21] daemon: refactor polling interval calculation; use a struct --- src/daemon.rs | 58 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index bb96d53..a8486d3 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -11,25 +11,37 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -/// Calculate optimal polling interval based on system conditions and history -fn compute_new( +/// Parameters for computing optimal polling interval +struct IntervalParams { + /// Base polling interval in seconds base_interval: u64, + /// Minimum allowed polling interval in seconds min_interval: u64, + /// Maximum allowed polling interval in seconds max_interval: u64, + /// How rapidly CPU usage is changing cpu_volatility: f32, + /// How rapidly temperature is changing temp_volatility: f32, + /// Battery discharge rate in %/hour if available battery_discharge_rate: Option, + /// Time since last detected user activity last_user_activity: Duration, + /// Whether the system appears to be idle is_system_idle: bool, + /// Whether the system is running on battery power on_battery: bool, -) -> u64 { +} + +/// Calculate optimal polling interval based on system conditions and history +fn compute_new(params: &IntervalParams) -> u64 { // Start with base interval - let mut adjusted_interval = base_interval; + let mut adjusted_interval = params.base_interval; // If we're on battery, we want to be more aggressive about saving power - if on_battery { + if params.on_battery { // Apply a multiplier based on battery discharge rate - if let Some(discharge_rate) = battery_discharge_rate { + if let Some(discharge_rate) = params.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; @@ -47,9 +59,9 @@ fn compute_new( } // Adjust for system idleness - if is_system_idle { + if params.is_system_idle { // If the system has been idle for a while, increase interval - let idle_time = last_user_activity.as_secs(); + let idle_time = params.last_user_activity.as_secs(); if idle_time > 300 { // 5 minutes adjusted_interval = (adjusted_interval as f32 * 2.0) as u64; @@ -58,12 +70,12 @@ fn compute_new( // Adjust for CPU/temperature volatility // If either CPU usage or temperature is changing rapidly, decrease interval - if cpu_volatility > 10.0 || temp_volatility > 2.0 { + if params.cpu_volatility > 10.0 || params.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) + adjusted_interval.clamp(params.min_interval, params.max_interval) } /// Tracks historical system data for advanced adaptive polling @@ -265,21 +277,19 @@ impl SystemHistory { /// 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; - - compute_new( - base_interval, - min_interval, - max_interval, - self.get_cpu_volatility(), - self.get_temperature_volatility(), - self.battery_discharge_rate, - self.last_user_activity.elapsed(), - self.is_system_idle(), + let params = IntervalParams { + base_interval: config.daemon.poll_interval_sec, + min_interval: config.daemon.min_poll_interval_sec, + max_interval: config.daemon.max_poll_interval_sec, + cpu_volatility: self.get_cpu_volatility(), + temp_volatility: self.get_temperature_volatility(), + battery_discharge_rate: self.battery_discharge_rate, + last_user_activity: self.last_user_activity.elapsed(), + is_system_idle: self.is_system_idle(), on_battery, - ) + }; + + compute_new(¶ms) } } From 4fcfeb073d0b3e6df7b7fd4d66bfe34e72080a1b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 03:01:29 +0300 Subject: [PATCH 07/21] daemon: clamp battery discharge rate to a maximum of 100%/hour --- src/daemon.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index a8486d3..693d905 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -191,13 +191,17 @@ impl SystemHistory { (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 { + // Only calculate discharge rate if at least 30 seconds have passed + // and we're not on AC power + if elapsed_hours > 0.0083 && !battery.ac_connected { // 0.0083 hours = 30 seconds // 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); + // Clamp the discharge rate to a reasonable maximum value (100%/hour) + let clamped_rate = hourly_rate.min(100.0); + self.battery_discharge_rate = Some(clamped_rate); } } } From 6335f139f9f50a9c67bef1ff03152042c16c37bb Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 03:02:51 +0300 Subject: [PATCH 08/21] daemon: improve polling interval adjustment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Casting float ->→ u64 risks silent truncation & sub-second intervals. We now *round* the computed interval, and ensure a minimum value of 1 to avoid zero or overly-small polling intervals. --- src/daemon.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 693d905..307c98f 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -71,7 +71,7 @@ fn compute_new(params: &IntervalParams) -> u64 { // Adjust for CPU/temperature volatility // If either CPU usage or temperature is changing rapidly, decrease interval if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { - adjusted_interval = (adjusted_interval as f32 * 0.5) as u64; + adjusted_interval = ((adjusted_interval as f32 * 0.5).round()).max(1.0) as u64; } // Ensure interval stays within configured bounds @@ -193,7 +193,8 @@ impl SystemHistory { let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0; // Only calculate discharge rate if at least 30 seconds have passed // and we're not on AC power - if elapsed_hours > 0.0083 && !battery.ac_connected { // 0.0083 hours = 30 seconds + if elapsed_hours > 0.0083 && !battery.ac_connected { + // 0.0083 hours = 30 seconds // Calculate discharge rate in percent per hour let percent_change = last_percentage - current_percent; if percent_change > 0.0 { From eea1f52c263a4e84962ded8b0d4963311a385061 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 03:22:30 +0300 Subject: [PATCH 09/21] daemon: progressive logarithmic back-off for idle periods 1min -> 1.5x, 2min -> 2x, 4min -> 3x, etc. Addresses poll interval inflation issues caused by noise in battery measurements and hopefully improve power efficiency during extended idle periods. --- src/daemon.rs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 307c98f..930faf0 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -60,11 +60,24 @@ fn compute_new(params: &IntervalParams) -> u64 { // Adjust for system idleness if params.is_system_idle { - // If the system has been idle for a while, increase interval - let idle_time = params.last_user_activity.as_secs(); - if idle_time > 300 { - // 5 minutes - adjusted_interval = (adjusted_interval as f32 * 2.0) as u64; + // Progressive back-off based on idle time duration + let idle_time_minutes = params.last_user_activity.as_secs() / 60; + + if idle_time_minutes >= 1 { + // Logarithmic back-off starting after 1 minute of idleness + // Use log base 2 to double the interval for each power of 2 minutes of idle time + // Example: 1min->1.5x, 2min->2x, 4min->3x, 8min->4x, 16min->5x, etc. + let idle_factor = 1.0 + (idle_time_minutes as f32).log2().max(0.5); + + // Cap the multiplier to avoid excessive intervals + let capped_factor = idle_factor.min(5.0); + + debug!( + "System idle for {} minutes, applying idle factor: {:.1}x", + idle_time_minutes, capped_factor + ); + + adjusted_interval = (adjusted_interval as f32 * capped_factor) as u64; } } From f79d6385b40e135e9dca1577e78eba06111479df Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 03:33:26 +0300 Subject: [PATCH 10/21] docs: mention new adaptive polling features --- README.md | 50 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index eb110c8..d529a86 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,8 @@ max_freq_mhz = 2500 # Global battery charging thresholds (applied to both profiles unless overridden) # Start charging at 40%, stop at 80% - extends battery lifespan -# NOTE: Profile-specific thresholds (in [charger] or [battery] sections) take precedence over this global setting +# NOTE: Profile-specific thresholds (in [charger] or [battery] sections) +# take precedence over this global setting battery_charge_thresholds = [40, 80] # Daemon configuration @@ -210,11 +211,28 @@ create an issue. ### Adaptive Polling -The daemon mode uses adaptive polling to balance responsiveness with efficiency: +Superfreq includes a "sophisticated" (euphemism for complicated) adaptive +polling system to try and maximize power efficiency -- Increases polling frequency during system changes -- Decreases polling frequency during stable periods -- Reduces polling when on battery to save power +- **Battery Discharge Analysis** - Automatically adjusts polling frequency based + on the battery discharge rate, reducing system activity when battery is + draining quickly +- **System Activity Pattern Recognition** - Monitors CPU usage and temperature + patterns to identify system stability +- **Dynamic Interval Calculation** - Uses multiple factors to determine optimal + polling intervals - up to 3x longer on battery with minimal user impact +- **Idle Detection** - Significantly reduces polling frequency during extended + idle periods to minimize power consumption +- **Gradual Transition** - Smooth transitions between polling rates to avoid + performance spikes +- **Progressive Back-off** - Implements logarithmic back-off during idle periods + (1min -> 1.5x, 2min -> 2x, 4min -> 3x, 8min -> 4x, 16min -> 5x) +- **Battery Discharge Protection** - Includes safeguards against measurement + noise to prevent erratic polling behavior + +When enabled, this intelligent polling system provides substantial power savings +over conventional fixed-interval approaches, especially during low-activity or +idle periods, while maintaining responsiveness when needed. ### Power Supply Filtering @@ -257,11 +275,25 @@ the codebase as they stand. ### Setup -You will need Cargo and Rust installed on your system. For Nix users, using -Direnv is encouraged. +You will need Cargo and Rust installed on your system. Rust > 1.80 is required. -Non-Nix users may get the appropriate Cargo andn Rust versions from their -package manager. +A `.envrc` is provided, and it's usage is encouraged for Nix users. +Alternatively, you may use Nix for a reproducible developer environment + +```bash +nix develop +``` + +Non-Nix users may get the appropriate Cargo and Rust versions from their package +manager. + +### Formatting + +Please make sure to run _at least_ `cargo fmt` inside the repository to make +sure all of your code is properly formatted. For Nix code, please use Alejandra. + +Clippy lints are not _required_ as of now, but a good rule of thumb to run them +before committing to catch possible code smell early. ## License From 035d7b88489d60f83ddfde00eef5ef62983b5950 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 04:02:09 +0300 Subject: [PATCH 11/21] daemon: avoid potential `u64` overflow when scaling long poll intervals --- src/daemon.rs | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 930faf0..a40d33c 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -44,54 +44,71 @@ fn compute_new(params: &IntervalParams) -> u64 { if let Some(discharge_rate) = params.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; + let multiplied = adjusted_interval as f64 * 3.0; + adjusted_interval = if multiplied >= u64::MAX as f64 { + u64::MAX + } else { + multiplied.round() as u64 + }; } else if discharge_rate > 10.0 { // Moderate discharge - double polling interval - adjusted_interval *= 2; + adjusted_interval = adjusted_interval.saturating_mul(2); } else { // Low discharge rate - increase by 50% - adjusted_interval = (adjusted_interval as f32 * 1.5) as u64; + let multiplied = adjusted_interval as f64 * 1.5; + adjusted_interval = if multiplied >= u64::MAX as f64 { + u64::MAX + } else { + multiplied.round() as u64 + }; } } else { // If we don't know discharge rate, use a conservative multiplier - adjusted_interval *= 2; + adjusted_interval = adjusted_interval.saturating_mul(2); } } // Adjust for system idleness if params.is_system_idle { // Progressive back-off based on idle time duration - let idle_time_minutes = params.last_user_activity.as_secs() / 60; + let idle_time_mins = params.last_user_activity.as_secs() / 60; - if idle_time_minutes >= 1 { + if idle_time_mins >= 1 { // Logarithmic back-off starting after 1 minute of idleness // Use log base 2 to double the interval for each power of 2 minutes of idle time // Example: 1min->1.5x, 2min->2x, 4min->3x, 8min->4x, 16min->5x, etc. - let idle_factor = 1.0 + (idle_time_minutes as f32).log2().max(0.5); + let idle_factor = 1.0 + (idle_time_mins as f32).log2().max(0.5); // Cap the multiplier to avoid excessive intervals let capped_factor = idle_factor.min(5.0); debug!( - "System idle for {} minutes, applying idle factor: {:.1}x", - idle_time_minutes, capped_factor + "System idle for {idle_time_mins} minutes, applying idle factor: {capped_factor:.1}x" ); - adjusted_interval = (adjusted_interval as f32 * capped_factor) as u64; + let multiplied = adjusted_interval as f64 * f64::from(capped_factor); + adjusted_interval = if multiplied >= u64::MAX as f64 { + u64::MAX + } else { + multiplied.round() as u64 + }; } } // Adjust for CPU/temperature volatility // If either CPU usage or temperature is changing rapidly, decrease interval if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { - adjusted_interval = ((adjusted_interval as f32 * 0.5).round()).max(1.0) as u64; + // XXX: This operation reduces the interval, so overflow is not an issue. + // Using f64 for precision in multiplication before rounding. + // Max with 1 to prevent zero interval before final clamp. + adjusted_interval = ((adjusted_interval as f64 * 0.5).round() as u64).max(1); } // Ensure interval stays within configured bounds adjusted_interval.clamp(params.min_interval, params.max_interval) } -/// Tracks historical system data for advanced adaptive polling +/// Tracks historical system data for "advanced" adaptive polling struct SystemHistory { /// Last several CPU usage measurements cpu_usage_history: VecDeque, From 70a59cec827162ea99ae87bd03ef412081d6a5b1 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 04:31:05 +0300 Subject: [PATCH 12/21] daemon: cleaner idle time handling and progressive back-off adjustments --- src/daemon.rs | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index a40d33c..df310d9 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -70,20 +70,34 @@ fn compute_new(params: &IntervalParams) -> u64 { // Adjust for system idleness if params.is_system_idle { - // Progressive back-off based on idle time duration - let idle_time_mins = params.last_user_activity.as_secs() / 60; + let idle_time_seconds = params.last_user_activity.as_secs(); - if idle_time_mins >= 1 { - // Logarithmic back-off starting after 1 minute of idleness - // Use log base 2 to double the interval for each power of 2 minutes of idle time - // Example: 1min->1.5x, 2min->2x, 4min->3x, 8min->4x, 16min->5x, etc. - let idle_factor = 1.0 + (idle_time_mins as f32).log2().max(0.5); + // Apply adjustment only if the system has been idle for a non-zero duration. + // The factor starts at 1.0 for 0 seconds idle time and increases. + if idle_time_seconds > 0 { + let idle_factor = if idle_time_seconds < 120 { + // Less than 2 minutes (0 to 119 seconds) + // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s). + // Value at 60s (1 min) = 1.0 + 60.0/120.0 = 1.5. + // This should provide a smooth transition from no multiplier (or 1.0x) + // up to the point where the logarithmic scale takes over at 2 minutes. + 1.0 + (idle_time_seconds as f32) / 120.0 + } else { + // 2 minutes (120 seconds) or more + let idle_time_minutes = idle_time_seconds / 60; + // At 2 minutes (120s), (2_f32).log2() = 1.0. So, factor = 1.0 + 1.0 = 2.0. + 1.0 + (idle_time_minutes as f32).log2().max(0.5) + }; // Cap the multiplier to avoid excessive intervals - let capped_factor = idle_factor.min(5.0); + let capped_factor = idle_factor.min(5.0); // max factor of 5x debug!( - "System idle for {idle_time_mins} minutes, applying idle factor: {capped_factor:.1}x" + "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x (raw: {:.2}x)", + idle_time_seconds, + (idle_time_seconds as f32 / 60.0).round(), + capped_factor, + idle_factor ); let multiplied = adjusted_interval as f64 * f64::from(capped_factor); @@ -93,10 +107,10 @@ fn compute_new(params: &IntervalParams) -> u64 { multiplied.round() as u64 }; } + // If idle_time_seconds is 0, no factor is applied by this block, effectively 1.0x. } // Adjust for CPU/temperature volatility - // If either CPU usage or temperature is changing rapidly, decrease interval if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { // XXX: This operation reduces the interval, so overflow is not an issue. // Using f64 for precision in multiplication before rounding. From ff2f305d9ec025f369492374f9786e8c03602fc2 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 04:40:30 +0300 Subject: [PATCH 13/21] daemon: optimize CPU usage averaging in `SystemHistory` updates --- src/daemon.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index df310d9..3308a50 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -163,20 +163,18 @@ impl SystemHistory { 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(); + let mut total_usage: f32 = 0.0; + let mut core_count: usize = 0; - if count > 0 { - let avg_usage = total / count as f32; + for core in &report.cpu_cores { + if let Some(usage) = core.usage_percent { + total_usage += usage; + core_count += 1; + } + } + + if core_count > 0 { + let avg_usage = total_usage / core_count as f32; // Keep only the last 5 measurements if self.cpu_usage_history.len() >= 5 { From 0ecb02499a9752247fe672d05855bed745608bb5 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 04:41:12 +0300 Subject: [PATCH 14/21] docs: fix link casing and clarify Rust version requirement in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d529a86..0488b8f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@
Synopsis
Features | Usage
- Contributing + Contributing
@@ -275,7 +275,7 @@ the codebase as they stand. ### Setup -You will need Cargo and Rust installed on your system. Rust > 1.80 is required. +You will need Cargo and Rust installed on your system. Rust 1.80 or later is required. A `.envrc` is provided, and it's usage is encouraged for Nix users. Alternatively, you may use Nix for a reproducible developer environment From 4aea53e060b8bf1c0730a7165a5bfd3bd96561c3 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 04:59:39 +0300 Subject: [PATCH 15/21] daemon: refactor idle time multiplier calculation for adaptive polling --- src/daemon.rs | 52 +++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 3308a50..27c86d7 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -33,6 +33,31 @@ struct IntervalParams { on_battery: bool, } +/// Calculate the idle time multiplier based on system idle duration +/// +/// Returns a multiplier between 1.0 and 5.0 (capped): +/// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 +/// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) +fn idle_multiplier(idle_secs: u64) -> f32 { + if idle_secs == 0 { + return 1.0; // No idle time, no multiplier effect + } + + let idle_factor = if idle_secs < 120 { + // Less than 2 minutes (0 to 119 seconds) + // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) + 1.0 + (idle_secs as f32) / 120.0 + } else { + // 2 minutes (120 seconds) or more + let idle_time_minutes = idle_secs / 60; + // Logarithmic scaling: 1.0 + log2(minutes) + 1.0 + (idle_time_minutes as f32).log2().max(0.5) + }; + + // Cap the multiplier to avoid excessive intervals + idle_factor.min(5.0) // max factor of 5x +} + /// Calculate optimal polling interval based on system conditions and history fn compute_new(params: &IntervalParams) -> u64 { // Start with base interval @@ -72,42 +97,25 @@ fn compute_new(params: &IntervalParams) -> u64 { if params.is_system_idle { let idle_time_seconds = params.last_user_activity.as_secs(); - // Apply adjustment only if the system has been idle for a non-zero duration. - // The factor starts at 1.0 for 0 seconds idle time and increases. + // Apply adjustment only if the system has been idle for a non-zero duration if idle_time_seconds > 0 { - let idle_factor = if idle_time_seconds < 120 { - // Less than 2 minutes (0 to 119 seconds) - // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s). - // Value at 60s (1 min) = 1.0 + 60.0/120.0 = 1.5. - // This should provide a smooth transition from no multiplier (or 1.0x) - // up to the point where the logarithmic scale takes over at 2 minutes. - 1.0 + (idle_time_seconds as f32) / 120.0 - } else { - // 2 minutes (120 seconds) or more - let idle_time_minutes = idle_time_seconds / 60; - // At 2 minutes (120s), (2_f32).log2() = 1.0. So, factor = 1.0 + 1.0 = 2.0. - 1.0 + (idle_time_minutes as f32).log2().max(0.5) - }; - - // Cap the multiplier to avoid excessive intervals - let capped_factor = idle_factor.min(5.0); // max factor of 5x + let idle_factor = idle_multiplier(idle_time_seconds); debug!( - "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x (raw: {:.2}x)", + "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", idle_time_seconds, (idle_time_seconds as f32 / 60.0).round(), - capped_factor, idle_factor ); - let multiplied = adjusted_interval as f64 * f64::from(capped_factor); + let multiplied = adjusted_interval as f64 * f64::from(idle_factor); adjusted_interval = if multiplied >= u64::MAX as f64 { u64::MAX } else { multiplied.round() as u64 }; } - // If idle_time_seconds is 0, no factor is applied by this block, effectively 1.0x. + // If idle_time_seconds is 0, no factor is applied by this block } // Adjust for CPU/temperature volatility From 59602b0e3ef5c05a2de29035f50b6da4f28b90a8 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 05:19:32 +0300 Subject: [PATCH 16/21] daemon: refactor `SystemHistory` init; derive `Default` for `SystemState` --- src/daemon.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 27c86d7..13d3925 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -131,6 +131,7 @@ fn compute_new(params: &IntervalParams) -> u64 { } /// Tracks historical system data for "advanced" adaptive polling +#[derive(Debug)] struct SystemHistory { /// Last several CPU usage measurements cpu_usage_history: VecDeque, @@ -152,21 +153,23 @@ struct SystemHistory { current_state: SystemState, } -impl SystemHistory { - fn new() -> Self { +impl Default for SystemHistory { + fn default() -> Self { Self { - cpu_usage_history: VecDeque::with_capacity(5), - temperature_history: VecDeque::with_capacity(5), + cpu_usage_history: VecDeque::new(), + temperature_history: VecDeque::new(), 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, + current_state: SystemState::default(), } } +} +impl SystemHistory { /// Update system history with new report data fn update(&mut self, report: &SystemReport) { // Update CPU usage history @@ -422,7 +425,7 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box Result<(), std::io::Er } /// Simplified system state used for determining when to adjust polling interval -#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] enum SystemState { + #[default] Unknown, OnAC, OnBattery, From 139746069ad3412b1c0bd81acab242ff1f444666 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 05:39:01 +0300 Subject: [PATCH 17/21] daemon: cross-platform safe arithmetic --- src/daemon.rs | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 13d3925..305e51f 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -68,27 +68,17 @@ fn compute_new(params: &IntervalParams) -> u64 { // Apply a multiplier based on battery discharge rate if let Some(discharge_rate) = params.battery_discharge_rate { if discharge_rate > 20.0 { - // High discharge rate - increase polling interval significantly - let multiplied = adjusted_interval as f64 * 3.0; - adjusted_interval = if multiplied >= u64::MAX as f64 { - u64::MAX - } else { - multiplied.round() as u64 - }; + // High discharge rate - increase polling interval significantly (3x) + adjusted_interval = adjusted_interval.saturating_mul(3); } else if discharge_rate > 10.0 { - // Moderate discharge - double polling interval + // Moderate discharge - double polling interval (2x) adjusted_interval = adjusted_interval.saturating_mul(2); } else { - // Low discharge rate - increase by 50% - let multiplied = adjusted_interval as f64 * 1.5; - adjusted_interval = if multiplied >= u64::MAX as f64 { - u64::MAX - } else { - multiplied.round() as u64 - }; + // Low discharge rate - increase by 50% (multiply by 3/2) + adjusted_interval = adjusted_interval.saturating_mul(3).saturating_div(2); } } else { - // If we don't know discharge rate, use a conservative multiplier + // If we don't know discharge rate, use a conservative multiplier (2x) adjusted_interval = adjusted_interval.saturating_mul(2); } } @@ -108,22 +98,22 @@ fn compute_new(params: &IntervalParams) -> u64 { idle_factor ); - let multiplied = adjusted_interval as f64 * f64::from(idle_factor); - adjusted_interval = if multiplied >= u64::MAX as f64 { - u64::MAX - } else { - multiplied.round() as u64 - }; + // Convert f32 multiplier to integer-safe math + // Multiply by a large number first, then divide to maintain precision + // Use 1000 as the scaling factor to preserve up to 3 decimal places + let scaling_factor = 1000; + let scaled_factor = (idle_factor * scaling_factor as f32) as u64; + adjusted_interval = adjusted_interval + .saturating_mul(scaled_factor) + .saturating_div(scaling_factor); } // If idle_time_seconds is 0, no factor is applied by this block } // Adjust for CPU/temperature volatility if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { - // XXX: This operation reduces the interval, so overflow is not an issue. - // Using f64 for precision in multiplication before rounding. - // Max with 1 to prevent zero interval before final clamp. - adjusted_interval = ((adjusted_interval as f64 * 0.5).round() as u64).max(1); + // For division by 2 (halving the interval), we can safely use integer division + adjusted_interval = (adjusted_interval / 2).max(1); } // Ensure interval stays within configured bounds From ff4e6e69c83f3a68becf1327862c940c96431528 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 05:47:39 +0300 Subject: [PATCH 18/21] daemon: deduplicate idle state check logic --- src/daemon.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 305e51f..c3894ea 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -256,7 +256,7 @@ impl SystemHistory { } // Update system state tracking - let new_state = determine_system_state(report); + let new_state = determine_system_state(report, self); if new_state != self.current_state { // Record time spent in previous state let time_in_state = self.last_state_change.elapsed(); @@ -580,7 +580,7 @@ enum SystemState { } /// Determine the current system state for adaptive polling -fn determine_system_state(report: &SystemReport) -> SystemState { +fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState { // Check power state first if !report.batteries.is_empty() { if let Some(battery) = report.batteries.first() { @@ -603,14 +603,8 @@ 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 { + // Check idle state + if history.is_system_idle() { return SystemState::Idle; } From f0932c64d9e5febcdde1acf3646ad9b4a9f5f797 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 06:01:00 +0300 Subject: [PATCH 19/21] daemon: prevent busy loops via minimum polling interval --- src/daemon.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index c3894ea..5421d82 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -414,7 +414,11 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Date: Sat, 17 May 2025 06:20:26 +0300 Subject: [PATCH 20/21] daemon: prioritize load checks over idle state in system state determination --- src/daemon.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 5421d82..202dbfb 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -618,16 +618,18 @@ fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> Sys } } - // Check idle state - if history.is_system_idle() { - return SystemState::Idle; - } - - // Check load + // Check load first, as high load should take precedence over idle state let avg_load = report.system_load.load_avg_1min; if avg_load > 3.0 { return SystemState::HighLoad; } + + // Check idle state only if we don't have high load + if history.is_system_idle() { + return SystemState::Idle; + } + + // Check for low load if avg_load < 0.5 { return SystemState::LowLoad; } From 862e090bc6f228a81260644d05502d6cca2b81ea Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 May 2025 06:22:56 +0300 Subject: [PATCH 21/21] daemon: fix typo in comment --- src/daemon.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 202dbfb..82349cb 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -414,7 +414,7 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box Sys if avg_load > 3.0 { return SystemState::HighLoad; } - + // Check idle state only if we don't have high load if history.is_system_idle() { return SystemState::Idle;