diff --git a/Cargo.lock b/Cargo.lock index f077741..bb5a94f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,27 +182,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys", -] - [[package]] name = "env_filter" version = "0.1.3" @@ -232,17 +211,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "hashbrown" version = "0.15.3" @@ -301,12 +269,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" dependencies = [ "jiff-static", - "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde", - "windows-sys", ] [[package]] @@ -320,37 +286,12 @@ dependencies = [ "syn", ] -[[package]] -name = "jiff-tzdb" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags", - "libc", -] - [[package]] name = "log" version = "0.4.27" @@ -391,12 +332,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "portable-atomic" version = "1.11.0" @@ -430,17 +365,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_users" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - [[package]] name = "regex" version = "1.11.1" @@ -505,26 +429,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "superfreq" -version = "0.3.2" -dependencies = [ - "anyhow", - "clap", - "clap-verbosity-flag", - "ctrlc", - "derive_more", - "dirs", - "env_logger", - "jiff", - "log", - "num_cpus", - "serde", - "thiserror", - "toml", - "yansi", -] - [[package]] name = "syn" version = "2.0.101" @@ -622,10 +526,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +name = "watt" +version = "0.4.0" +dependencies = [ + "anyhow", + "clap", + "clap-verbosity-flag", + "ctrlc", + "derive_more", + "env_logger", + "log", + "num_cpus", + "serde", + "thiserror", + "toml", + "yansi", +] [[package]] name = "windows-sys" diff --git a/Cargo.toml b/Cargo.toml index aeecd4b..ecc84ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,21 @@ [package] -name = "superfreq" +name = "watt" description = "Modern CPU frequency and power management utility for Linux" -version = "0.3.2" +version = "0.4.0" edition = "2024" -authors = ["NotAShelf "] +authors = ["NotAShelf ", "RGBCube "] rust-version = "1.85" [dependencies] -serde = { version = "1.0", features = ["derive"] } -toml = "0.8" -dirs = "6.0" -clap = { version = "4.0", features = ["derive", "env"] } -num_cpus = "1.16" -ctrlc = "3.4" -log = "0.4" -env_logger = "0.11" -thiserror = "2.0" anyhow = "1.0" -jiff = "0.2.13" +clap = { version = "4.0", features = ["derive", "env"] } clap-verbosity-flag = "3.0.2" -yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] } +ctrlc = "3.4" derive_more = { version = "2.0.1", features = ["full"] } +env_logger = "0.11" +log = "0.4" +num_cpus = "1.16" +serde = { version = "1.0", features = ["derive"] } +thiserror = "2.0" +toml = "0.8" +yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] } diff --git a/src/daemon_old.rs b/src/daemon_old.rs deleted file mode 100644 index 3a20cb4..0000000 --- a/src/daemon_old.rs +++ /dev/null @@ -1,426 +0,0 @@ -use anyhow::Context; -use anyhow::bail; - -use crate::config::AppConfig; -use crate::core::SystemReport; -use crate::engine; -use crate::monitor; -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}; - -/// Tracks historical system data for "advanced" adaptive polling -#[derive(Debug)] -struct SystemHistory { - /// Last several CPU usage measurements - cpu_usage_history: VecDeque, - /// Last several temperature readings - temperature_history: VecDeque, - /// 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, - /// Last computed optimal polling interval - last_computed_interval: Option, -} - -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(); - log::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(); - log::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(); - log::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(); - log::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 - } -} - -/// Run the daemon -pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { - log::info!("Starting superfreq daemon..."); - - // Validate critical configuration values before proceeding - validate_poll_intervals( - config.daemon.min_poll_interval_sec, - config.daemon.max_poll_interval_sec, - )?; - - // Create a flag that will be set to true when a signal is received - let running = Arc::new(AtomicBool::new(true)); - let r = running.clone(); - - // Set up signal handlers - ctrlc::set_handler(move || { - log::info!("Received shutdown signal, exiting..."); - r.store(false, Ordering::SeqCst); - }) - .context("failed to set Ctrl-C handler")?; - - log::info!( - "Daemon initialized with poll interval: {}s", - config.daemon.poll_interval_sec - ); - - // Set up stats file if configured - if let Some(stats_path) = &config.daemon.stats_file_path { - log::info!("Stats will be written to: {stats_path}"); - } - - // Variables for adaptive polling - // 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 { - log::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) { - let start_time = Instant::now(); - - match monitor::collect_system_report(&config) { - Ok(report) => { - log::debug!("Collected system report, applying settings..."); - - // 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 { - if let Err(e) = write_stats_file(stats_path, &report) { - log::error!("Failed to write stats file: {e}"); - } - } - - match engine::determine_and_apply_settings(&report, &config, None) { - Ok(()) => { - log::debug!("Successfully applied system settings"); - - // If system state changed, log the new state - if system_history.current_state != previous_state { - log::info!( - "System state changed to: {:?}", - system_history.current_state - ); - } - } - Err(e) => { - log::error!("Error applying system settings: {e}"); - } - } - - // 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 { - match system_history.calculate_optimal_interval(&config, on_battery) { - Ok(optimal_interval) => { - // Store the new interval - system_history.last_computed_interval = Some(optimal_interval); - - log::debug!("Recalculated optimal interval: {optimal_interval}s"); - - // Don't change the interval too dramatically at once - match optimal_interval.cmp(¤t_poll_interval) { - std::cmp::Ordering::Greater => { - current_poll_interval = - (current_poll_interval + optimal_interval) / 2; - } - std::cmp::Ordering::Less => { - current_poll_interval = current_poll_interval - - ((current_poll_interval - optimal_interval) / 2).max(1); - } - std::cmp::Ordering::Equal => { - // No change needed when they're equal - } - } - } - Err(e) => { - // Log the error and stop the daemon when an invalid configuration is detected - log::error!("Critical configuration error: {e}"); - running.store(false, Ordering::SeqCst); - break; - } - } - - // 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, - ); - - log::debug!("Adaptive polling: set interval to {current_poll_interval}s"); - } else { - // 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 - - // 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); - - log::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 { - log::debug!( - "Using minimum poll interval of 1s instead of configured 0s" - ); - } - } - } - } - Err(e) => { - log::error!("Error collecting system report: {e}"); - } - } - - // Sleep for the remaining time in the poll interval - let elapsed = start_time.elapsed(); - let poll_duration = Duration::from_secs(current_poll_interval); - if elapsed < poll_duration { - let sleep_time = poll_duration - elapsed; - log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); - std::thread::sleep(sleep_time); - } - } - - log::info!("Daemon stopped"); - Ok(()) -} - -/// Simplified system state used for determining when to adjust polling interval -#[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, history: &SystemHistory) -> SystemState { - // Check power state first - if !report.batteries.is_empty() { - if let Some(battery) = report.batteries.first() { - if battery.ac_connected { - return SystemState::OnAC; - } - return SystemState::OnBattery; - } - } - - // No batteries means desktop, so always AC - if report.batteries.is_empty() { - return SystemState::OnAC; - } - - // Check temperature - if let Some(temp) = report.cpu_global.average_temperature_celsius { - if temp > 80.0 { - return SystemState::HighTemp; - } - } - - // 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; - } - - // Default case - SystemState::Unknown -}