diff --git a/build.rs b/build.rs index 5cc203d..7d03f98 100644 --- a/build.rs +++ b/build.rs @@ -6,6 +6,7 @@ const MULTICALL_NAMES: &[&str] = &["cpu", "power"]; fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=target"); let out_dir = PathBuf::from(env::var("OUT_DIR")?); let target = out_dir diff --git a/src/cpu.rs b/src/cpu.rs index e0e00e2..ef2eec6 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -123,7 +123,7 @@ impl Cpu { let cache = CpuRescanCache::default(); for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read CPU entries from '{PATH}'"))? + .context("failed to read CPU entries")? .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? { let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; diff --git a/src/engine.rs b/src/engine.rs deleted file mode 100644 index b9c7b01..0000000 --- a/src/engine.rs +++ /dev/null @@ -1,465 +0,0 @@ -use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; -use crate::core::{OperationalMode, SystemReport}; -use crate::cpu::{self}; -use crate::power_supply; -use std::sync::OnceLock; -use std::sync::atomic::{AtomicBool, Ordering}; - -/// Track turbo boost state for AC and battery power modes -struct TurboHysteresisStates { - /// State for when on AC power - charger: TurboHysteresis, - /// State for when on battery power - battery: TurboHysteresis, -} - -impl TurboHysteresisStates { - const fn new() -> Self { - Self { - charger: TurboHysteresis::new(), - battery: TurboHysteresis::new(), - } - } - - const fn get_for_power_state(&self, is_on_ac: bool) -> &TurboHysteresis { - if is_on_ac { - &self.charger - } else { - &self.battery - } - } -} - -static TURBO_STATES: OnceLock = OnceLock::new(); - -/// Get or initialize the global turbo states -fn get_turbo_states() -> &'static TurboHysteresisStates { - TURBO_STATES.get_or_init(TurboHysteresisStates::new) -} - -/// Manage turbo boost hysteresis state. -/// Contains the state needed to implement hysteresis -/// for the dynamic turbo management feature -struct TurboHysteresis { - /// Whether turbo was enabled in the previous cycle - previous_state: AtomicBool, - /// Whether the hysteresis state has been initialized - initialized: AtomicBool, -} - -impl TurboHysteresis { - const fn new() -> Self { - Self { - previous_state: AtomicBool::new(false), - initialized: AtomicBool::new(false), - } - } - - /// Get the previous turbo state, if initialized - fn get_previous_state(&self) -> Option { - if self.initialized.load(Ordering::Acquire) { - Some(self.previous_state.load(Ordering::Acquire)) - } else { - None - } - } - - /// Initialize the state with a specific value if not already initialized - /// Only one thread should be able to initialize the state - fn initialize_with(&self, initial_state: bool) -> bool { - // First, try to atomically change initialized from false to true - // Only one thread can win the initialization race - match self.initialized.compare_exchange( - false, // expected: not initialized - true, // desired: mark as initialized - Ordering::Release, // success: release for memory visibility - Ordering::Acquire, // failure: just need to acquire the current value - ) { - Ok(_) => { - // We won the race to initialize - // Now it's safe to set the initial state since we know we're the only - // thread that has successfully marked this as initialized - self.previous_state.store(initial_state, Ordering::Release); - initial_state - } - Err(_) => { - // Another thread already initialized it. - // Just read the current state value that was set by the winning thread - self.previous_state.load(Ordering::Acquire) - } - } - } - - /// Update the turbo state for hysteresis - fn update_state(&self, new_state: bool) { - // First store the new state, then mark as initialized - // With this, any thread seeing initialized=true will also see the correct state - self.previous_state.store(new_state, Ordering::Release); - - // Already initialized, no need for compare_exchange - if self.initialized.load(Ordering::Relaxed) { - return; - } - - // Otherwise, try to set initialized=true (but only if it was false) - self.initialized - .compare_exchange( - false, // expected: not initialized - true, // desired: mark as initialized - Ordering::Release, // success: release for memory visibility - Ordering::Relaxed, // failure: we don't care about the current value on failure - ) - .ok(); // Ignore the result. If it fails, it means another thread already initialized it - } -} - -/// Try applying a CPU feature and handle common error cases. Centralizes the where we -/// previously did: -/// 1. Try to apply a feature setting -/// 2. If not supported, log a warning and continue -/// 3. If other error, propagate the error -fn try_apply_feature anyhow::Result<()>, T>( - feature_name: &str, - value_description: &str, - apply_fn: F, -) -> anyhow::Result<()> { - log::info!("Setting {feature_name} to '{value_description}'"); - - apply_fn() -} - -/// Determines the appropriate CPU profile based on power status or forced mode, -/// and applies the settings (via helpers defined in the `cpu` module) -pub fn determine_and_apply_settings( - report: &SystemReport, - config: &AppConfig, - force_mode: Option, -) -> anyhow::Result<()> { - // // First, check if there's a governor override set - // if let Some(override_governor) = cpu::get_governor_override() { - // log::info!( - // "Governor override is active: '{}'. Setting governor.", - // override_governor.trim() - // ); - - // // Apply the override governor setting - // try_apply_feature("override governor", override_governor.trim(), || { - // cpu::set_governor(override_governor.trim(), None) - // })?; - // } - - // Determine AC/Battery status once, early in the function - // For desktops (no batteries), we should always use the AC power profile - // For laptops, we check if all batteries report connected to AC - let on_ac_power = if report.batteries.is_empty() { - // No batteries means desktop/server, always on AC - true - } else { - // Check if all batteries report AC connected - report.batteries.iter().all(|b| b.ac_connected) - }; - - let selected_profile_config: &ProfileConfig; - - if let Some(mode) = force_mode { - match mode { - OperationalMode::Powersave => { - log::info!("Forced Powersave mode selected. Applying 'battery' profile."); - selected_profile_config = &config.battery; - } - OperationalMode::Performance => { - log::info!("Forced Performance mode selected. Applying 'charger' profile."); - selected_profile_config = &config.charger; - } - } - } else { - // Use the previously computed on_ac_power value - if on_ac_power { - log::info!("On AC power, selecting Charger profile."); - selected_profile_config = &config.charger; - } else { - log::info!("On Battery power, selecting Battery profile."); - selected_profile_config = &config.battery; - } - } - - // Apply settings from selected_profile_config - if let Some(governor) = &selected_profile_config.governor { - log::info!("Setting governor to '{governor}'"); - for cpu in cpu::Cpu::all()? { - // Let set_governor handle the validation - if let Err(error) = cpu.set_governor(governor) { - // If the governor is not available, log a warning - log::warn!("{error}"); - } - } - } - - if let Some(turbo_setting) = selected_profile_config.turbo { - log::info!("Setting turbo to '{turbo_setting:?}'"); - match turbo_setting { - TurboSetting::Auto => { - if selected_profile_config.enable_auto_turbo { - log::debug!("Managing turbo in auto mode based on system conditions"); - manage_auto_turbo(report, selected_profile_config, on_ac_power)?; - } else { - log::debug!( - "Watt's dynamic turbo management is disabled by configuration. Ensuring system uses its default behavior for automatic turbo control." - ); - // Make sure the system is set to its default automatic turbo mode. - // This is important if turbo was previously forced off. - try_apply_feature("Turbo boost", "system default (Auto)", || { - cpu::set_turbo(TurboSetting::Auto) - })?; - } - } - _ => { - try_apply_feature("Turbo boost", &format!("{turbo_setting:?}"), || { - cpu::set_turbo(turbo_setting) - })?; - } - } - } - - if let Some(epp) = &selected_profile_config.epp { - try_apply_feature("EPP", epp, || cpu::set_epp(epp, None))?; - } - - if let Some(epb) = &selected_profile_config.epb { - try_apply_feature("EPB", epb, || cpu::set_epb(epb, None))?; - } - - if let Some(min_freq) = selected_profile_config.min_freq_mhz { - try_apply_feature("min frequency", &format!("{min_freq} MHz"), || { - cpu::set_frequency_minimum(min_freq, None) - })?; - } - - if let Some(max_freq) = selected_profile_config.max_freq_mhz { - try_apply_feature("max frequency", &format!("{max_freq} MHz"), || { - cpu::set_frequency_maximum(max_freq, None) - })?; - } - - if let Some(profile) = &selected_profile_config.platform_profile { - try_apply_feature("platform profile", profile, || { - cpu::set_platform_profile(profile) - })?; - } - - // Set battery charge thresholds if configured - if let Some(thresholds) = &selected_profile_config.battery_charge_thresholds { - let start_threshold = thresholds.start; - let stop_threshold = thresholds.stop; - - if start_threshold < stop_threshold && stop_threshold <= 100 { - log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); - match power_supply::set_battery_charge_thresholds(start_threshold, stop_threshold) { - Ok(()) => log::debug!("Battery charge thresholds set successfully"), - Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"), - } - } else { - log::warn!( - "Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}" - ); - } - } - - log::debug!("Profile settings applied successfully."); - - Ok(()) -} - -fn manage_auto_turbo( - report: &SystemReport, - config: &ProfileConfig, - on_ac_power: bool, -) -> anyhow::Result<()> { - // Get the auto turbo settings from the config - let turbo_settings = &config.turbo_auto_settings; - - // Validate the complete configuration to ensure it's usable - validate_turbo_auto_settings(turbo_settings)?; - - // Get average CPU temperature and CPU load - let cpu_temp = report.cpu_global.average_temperature_celsius; - - // Check if we have CPU usage data available - let avg_cpu_usage = if report.cpu_cores.is_empty() { - None - } else { - let sum: f32 = report - .cpu_cores - .iter() - .filter_map(|core| core.usage_percent) - .sum(); - let count = report - .cpu_cores - .iter() - .filter(|core| core.usage_percent.is_some()) - .count(); - - if count > 0 { - Some(sum / count as f32) - } else { - None - } - }; - - // Get the previous state or initialize with the configured initial state - let previous_turbo_enabled = { - let turbo_states = get_turbo_states(); - let hysteresis = turbo_states.get_for_power_state(on_ac_power); - if let Some(state) = hysteresis.get_previous_state() { - state - } else { - // Initialize with the configured initial state and return it - hysteresis.initialize_with(turbo_settings.initial_turbo_state) - } - }; - - // Decision logic for enabling/disabling turbo with hysteresis - let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) { - // If temperature is too high, disable turbo regardless of load - (Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => { - log::info!( - "Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)", - temp, - turbo_settings.temp_threshold_high - ); - false - } - - // If load is high enough, enable turbo (unless temp already caused it to disable) - (_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => { - log::info!( - "Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)", - usage, - turbo_settings.load_threshold_high - ); - true - } - - // If load is low, disable turbo - (_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => { - log::info!( - "Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)", - usage, - turbo_settings.load_threshold_low - ); - false - } - - // In intermediate load range, maintain previous state (hysteresis) - (_, Some(usage), prev_state) - if usage > turbo_settings.load_threshold_low - && usage < turbo_settings.load_threshold_high => - { - log::info!( - "Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)", - if prev_state { "enabled" } else { "disabled" }, - usage - ); - prev_state - } - - // When CPU load data is present but temperature is missing, use the same hysteresis logic - (None, Some(usage), prev_state) => { - log::info!( - "Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)", - if prev_state { "enabled" } else { "disabled" }, - usage - ); - prev_state - } - - // When all metrics are missing, maintain the previous state - (None, None, prev_state) => { - log::info!( - "Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics", - if prev_state { "enabled" } else { "disabled" } - ); - prev_state - } - - // Any other cases with partial metrics, maintain previous state for stability - (_, _, prev_state) => { - log::info!( - "Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics", - if prev_state { "enabled" } else { "disabled" } - ); - prev_state - } - }; - - // Save the current state for next time - { - let turbo_states = get_turbo_states(); - let hysteresis = turbo_states.get_for_power_state(on_ac_power); - hysteresis.update_state(enable_turbo); - } - - // Only apply the setting if the state has changed - let changed = previous_turbo_enabled != enable_turbo; - if changed { - let turbo_setting = if enable_turbo { - TurboSetting::Always - } else { - TurboSetting::Never - }; - - log::info!( - "Auto Turbo: Applying turbo change from {} to {}", - if previous_turbo_enabled { - "enabled" - } else { - "disabled" - }, - if enable_turbo { "enabled" } else { "disabled" } - ); - - match cpu::set_turbo(turbo_setting) { - Ok(()) => { - log::debug!( - "Auto Turbo: Successfully set turbo to {}", - if enable_turbo { "enabled" } else { "disabled" } - ); - Ok(()) - } - Err(e) => Err(EngineError::ControlError(e)), - } - } else { - log::debug!( - "Auto Turbo: Maintaining turbo state ({}) - no change needed", - if enable_turbo { "enabled" } else { "disabled" } - ); - Ok(()) - } -} - -fn validate_turbo_auto_settings(settings: &TurboAutoSettings) -> Result<(), EngineError> { - if settings.load_threshold_high <= settings.load_threshold_low - || settings.load_threshold_high > 100.0 - || settings.load_threshold_high < 0.0 - || settings.load_threshold_low < 0.0 - || settings.load_threshold_low > 100.0 - { - return Err(EngineError::ConfigurationError( - "Invalid turbo auto settings: load thresholds must be between 0 % and 100 % with high > low" - .to_string(), - )); - } - - // Validate temperature threshold (realistic range for CPU temps in Celsius) - // TODO: different CPUs have different temperature thresholds. While 110 is a good example - // "extreme" case, the upper barrier might be *lower* for some devices. We'll want to fix - // this eventually, or make it configurable. - if settings.temp_threshold_high <= 0.0 || settings.temp_threshold_high > 110.0 { - return Err(EngineError::ConfigurationError( - "Invalid turbo auto settings: temperature threshold must be between 0°C and 110°C" - .to_string(), - )); - } - - Ok(()) -} diff --git a/src/monitor.rs b/src/monitor.rs deleted file mode 100644 index e4ff659..0000000 --- a/src/monitor.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Try /sys/devices/platform paths for thermal zones as a last resort -// if temperature_celsius.is_none() { -// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { -// for entry in thermal_zones.flatten() { -// let zone_path = entry.path(); -// let name = entry.file_name().into_string().unwrap_or_default(); - -// if name.starts_with("thermal_zone") { -// // Try to match by type -// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) { -// if zone_type.contains("cpu") -// || zone_type.contains("x86") -// || zone_type.contains("core") -// { -// if let Ok(temp_mc) = read_sysfs_value::(zone_path.join("temp")) { -// temperature_celsius = Some(temp_mc as f32 / 1000.0); -// break; -// } -// } -// } -// } -// } -// } -// } diff --git a/src/power_supply.rs b/src/power_supply.rs index 2155c29..ac33e96 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -154,7 +154,7 @@ impl PowerSupply { let mut power_supplies = Vec::new(); for entry in fs::read_dir(POWER_SUPPLY_PATH) - .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? + .context("failed to read power supply entries")? .with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))? { let entry = match entry { diff --git a/src/system.rs b/src/system.rs index 322e0e4..f034a85 100644 --- a/src/system.rs +++ b/src/system.rs @@ -115,7 +115,7 @@ impl System { let mut temperatures = HashMap::new(); for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read hardware information from '{PATH}'"))? + .context("failed to read hardware information")? .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? { let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; @@ -147,6 +147,64 @@ impl System { } } + if temperatures.is_empty() { + const PATH: &str = "/sys/devices/virtual/thermal"; + + log::debug!( + "failed to get CPU temperature information by using hwmon, falling back to '{PATH}'" + ); + + let Some(thermal_zones) = + fs::read_dir(PATH).context("failed to read thermal information")? + else { + return Ok(()); + }; + + for entry in thermal_zones { + let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + + let entry_path = entry.path(); + + let entry_name = entry.file_name(); + let entry_name = entry_name.to_string_lossy(); + + if !entry_name.starts_with("thermal_zone") { + return Ok(()); + } + + let Some(entry_type) = fs::read(entry_path.join("type")).with_context(|| { + format!( + "failed to read type of zone at '{path}'", + path = entry_path.display(), + ) + })? + else { + return Ok(()); + }; + + if !entry_type.contains("cpu") + && !entry_type.contains("x86") + && !entry_type.contains("core") + { + return Ok(()); + } + + let Some(temperature_mc) = fs::read_n::(entry_path.join("temp")) + .with_context(|| { + format!( + "failed to read temperature of zone at '{path}'", + path = entry_path.display(), + ) + })? + else { + return Ok(()); + }; + + // Magic value to see that it is from the thermal zones. + temperatures.insert(777, temperature_mc as f64 / 1000.0); + } + } + self.cpu_temperatures = temperatures; Ok(()) @@ -185,7 +243,7 @@ impl System { else { continue; }; - log::debug!("label content: {number}"); + log::debug!("label content: {label}"); // Match various common label formats: // "Core X", "core X", "Core-X", "CPU Core X", etc.