mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-27 17:07:44 +00:00
Merge pull request #24 from NotAShelf/better-polling
daemon: improve adapting polling; more precise math
This commit is contained in:
commit
506151ac79
2 changed files with 449 additions and 53 deletions
52
README.md
52
README.md
|
@ -10,7 +10,7 @@
|
|||
<br/>
|
||||
<a href="#what-is-superfreq">Synopsis</a><br/>
|
||||
<a href="#features">Features</a> | <a href="#usage">Usage</a><br/>
|
||||
<a href="#Contributing">Contributing</a>
|
||||
<a href="#contributing">Contributing</a>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
|
@ -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 or later 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
|
||||
|
||||
|
|
450
src/daemon.rs
450
src/daemon.rs
|
@ -4,12 +4,343 @@ 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;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// 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<f32>,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// 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
|
||||
let mut adjusted_interval = params.base_interval;
|
||||
|
||||
// If we're on battery, we want to be more aggressive about saving power
|
||||
if params.on_battery {
|
||||
// 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 (3x)
|
||||
adjusted_interval = adjusted_interval.saturating_mul(3);
|
||||
} else if discharge_rate > 10.0 {
|
||||
// Moderate discharge - double polling interval (2x)
|
||||
adjusted_interval = adjusted_interval.saturating_mul(2);
|
||||
} else {
|
||||
// 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 (2x)
|
||||
adjusted_interval = adjusted_interval.saturating_mul(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust for system idleness
|
||||
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
|
||||
if idle_time_seconds > 0 {
|
||||
let idle_factor = idle_multiplier(idle_time_seconds);
|
||||
|
||||
debug!(
|
||||
"System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x",
|
||||
idle_time_seconds,
|
||||
(idle_time_seconds as f32 / 60.0).round(),
|
||||
idle_factor
|
||||
);
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
adjusted_interval.clamp(params.min_interval, params.max_interval)
|
||||
}
|
||||
|
||||
/// Tracks historical system data for "advanced" adaptive polling
|
||||
#[derive(Debug)]
|
||||
struct SystemHistory {
|
||||
/// Last several CPU usage measurements
|
||||
cpu_usage_history: VecDeque<f32>,
|
||||
/// Last several temperature readings
|
||||
temperature_history: VecDeque<f32>,
|
||||
/// Time of last detected user activity
|
||||
last_user_activity: Instant,
|
||||
/// Previous battery percentage (to calculate discharge rate)
|
||||
last_battery_percentage: Option<f32>,
|
||||
/// Timestamp of last battery reading
|
||||
last_battery_timestamp: Option<Instant>,
|
||||
/// Battery discharge rate (%/hour)
|
||||
battery_discharge_rate: Option<f32>,
|
||||
/// Time spent in each system state
|
||||
state_durations: std::collections::HashMap<SystemState, Duration>,
|
||||
/// Last time a state transition happened
|
||||
last_state_change: Instant,
|
||||
/// Current system state
|
||||
current_state: SystemState,
|
||||
}
|
||||
|
||||
impl Default for SystemHistory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
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::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemHistory {
|
||||
/// Update system history with new report data
|
||||
fn update(&mut self, report: &SystemReport) {
|
||||
// Update CPU usage history
|
||||
if !report.cpu_cores.is_empty() {
|
||||
let mut total_usage: f32 = 0.0;
|
||||
let mut core_count: usize = 0;
|
||||
|
||||
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 {
|
||||
self.cpu_usage_history.pop_front();
|
||||
}
|
||||
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
|
||||
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.pop_front();
|
||||
}
|
||||
self.temperature_history.push_back(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 {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
// 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;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, self);
|
||||
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::<f32>() / 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 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the daemon
|
||||
pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Set effective log level based on config and verbose flag
|
||||
|
@ -83,9 +414,12 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn st
|
|||
};
|
||||
|
||||
// Variables for adaptive polling
|
||||
let mut current_poll_interval = config.daemon.poll_interval_sec;
|
||||
let mut last_settings_change = Instant::now();
|
||||
let mut last_system_state = SystemState::Unknown;
|
||||
// Make sure that the poll interval is *never* zero to prevent a busy loop
|
||||
let mut current_poll_interval = config.daemon.poll_interval_sec.max(1);
|
||||
if config.daemon.poll_interval_sec == 0 {
|
||||
warn!("Poll interval is set to zero in config, using 1s minimum to prevent a busy loop");
|
||||
}
|
||||
let mut system_history = SystemHistory::default();
|
||||
|
||||
// Main loop
|
||||
while running.load(Ordering::SeqCst) {
|
||||
|
@ -99,9 +433,14 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn st
|
|||
info!("Config file changed, updating configuration");
|
||||
config = new_config;
|
||||
// Reset polling interval after config change
|
||||
current_poll_interval = config.daemon.poll_interval_sec;
|
||||
// Record this as a settings change for adaptive polling purposes
|
||||
last_settings_change = Instant::now();
|
||||
current_poll_interval = config.daemon.poll_interval_sec.max(1);
|
||||
if config.daemon.poll_interval_sec == 0 {
|
||||
warn!(
|
||||
"Poll interval is set to zero in updated config, using 1s minimum to prevent a busy loop"
|
||||
);
|
||||
}
|
||||
// Mark this as a system event for adaptive polling
|
||||
system_history.last_user_activity = Instant::now();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error loading new configuration: {e}");
|
||||
|
@ -115,8 +454,11 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn st
|
|||
Ok(report) => {
|
||||
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 +471,12 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn st
|
|||
Ok(()) => {
|
||||
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 +484,51 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn st
|
|||
}
|
||||
}
|
||||
|
||||
// Adjust poll interval if adaptive polling is enabled
|
||||
// Check if we're on battery
|
||||
let on_battery = !report.batteries.is_empty()
|
||||
&& report.batteries.first().is_some_and(|b| !b.ac_connected);
|
||||
|
||||
// Calculate optimal polling interval if adaptive polling is enabled
|
||||
if config.daemon.adaptive_interval {
|
||||
let time_since_change = last_settings_change.elapsed().as_secs();
|
||||
let optimal_interval =
|
||||
system_history.calculate_optimal_interval(&config, on_battery);
|
||||
|
||||
// If we've been stable for a while, increase the interval (up to max)
|
||||
if time_since_change > 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);
|
||||
}
|
||||
|
||||
// Make sure that we respect the (user) configured min and max limits
|
||||
current_poll_interval = current_poll_interval.clamp(
|
||||
config.daemon.min_poll_interval_sec,
|
||||
config.daemon.max_poll_interval_sec,
|
||||
);
|
||||
|
||||
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
|
||||
|
||||
// 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);
|
||||
// We need to make sure `poll_interval_sec` is *at least* 1
|
||||
// before multiplying.
|
||||
let safe_interval = config.daemon.poll_interval_sec.max(1);
|
||||
current_poll_interval = (safe_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.max(1);
|
||||
if config.daemon.poll_interval_sec == 0 {
|
||||
debug!("Using minimum poll interval of 1s instead of configured 0s");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -227,18 +582,20 @@ 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, Default)]
|
||||
enum SystemState {
|
||||
#[default]
|
||||
Unknown,
|
||||
OnAC,
|
||||
OnBattery,
|
||||
HighLoad,
|
||||
LowLoad,
|
||||
HighTemp,
|
||||
Idle,
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
|
@ -261,11 +618,18 @@ fn determine_system_state(report: &SystemReport) -> SystemState {
|
|||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue