diff --git a/Cargo.toml b/Cargo.toml index aed276a..e606e85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "superfreq" +description = "Modern CPU frequency and power management utility for Linux" version = "0.2.0" edition = "2024" +authors = ["NotAShelf "] +rust-version = "1.85" [dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index 0488b8f..c1aff74 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ Superfreq is a modern CPU frequency and power management utility for Linux systems. It provides intelligent control of CPU governors, frequencies, and power-saving features, helping optimize both performance and battery life. -It is greatly inspired by auto_cpufreq, but rewritten from ground up to provide +It is greatly inspired by auto-cpufreq, but rewritten from ground up to provide a smoother experience with a more efficient and more correct codebase. Some -features are omitted, and it is _not_ a drop-in replacement for auto_cpufreq, +features are omitted, and it is _not_ a drop-in replacement for auto-cpufreq, but most common usecases are already implemented. ## Features @@ -31,6 +31,8 @@ but most common usecases are already implemented. and turbo boost - **Intelligent Power Management**: Different profiles for AC and battery operation +- **Dynamic Turbo Boost Control**: Automatically enables/disables turbo based on + CPU load and temperature - **Fine-tuned Controls**: Adjust energy performance preferences, biases, and frequency limits - **Per-core Control**: Apply settings globally or to specific CPU cores @@ -150,6 +152,15 @@ variable. governor = "performance" # Turbo boost setting: "always", "auto", or "never" turbo = "auto" +# Enable or disable automatic turbo management (when turbo = "auto") +enable_auto_turbo = true +# Custom thresholds for auto turbo management +turbo_auto_settings = { + load_threshold_high = 70.0, + load_threshold_low = 30.0, + temp_threshold_high = 75.0, + initial_turbo_state = false, # whether turbo should be initially enabled (false = disabled) +} # Energy Performance Preference epp = "performance" # Energy Performance Bias (0-15 scale or named value) @@ -166,6 +177,14 @@ max_freq_mhz = 3500 [battery] governor = "powersave" turbo = "auto" +# More conservative auto turbo settings on battery +enable_auto_turbo = true +turbo_auto_settings = { + load_threshold_high = 80.0, + load_threshold_low = 40.0, + temp_threshold_high = 70.0, + initial_turbo_state = false, # start with turbo disabled on battery for power savings +} epp = "power" epb = "balance_power" platform_profile = "low-power" @@ -209,6 +228,45 @@ Those are the more advanced features of Superfreq that some users might be more inclined to use than others. If you have a use-case that is not covered, please create an issue. +### Dynamic Turbo Boost Management + +When using `turbo = "auto"` with `enable_auto_turbo = true`, Superfreq +dynamically controls CPU turbo boost based on: + +- **CPU Load Thresholds**: Enables turbo when load exceeds `load_threshold_high` + (default 70%), disables when below `load_threshold_low` (default 30%) +- **Temperature Protection**: Automatically disables turbo when CPU temperature + exceeds `temp_threshold_high` (default 75°C) +- **Hysteresis Control**: Prevents rapid toggling by maintaining previous state + when load is between thresholds +- **Configurable Initial State**: Sets the initial turbo state via + `initial_turbo_state` (default: disabled) before system load data is available +- **Profile-Specific Settings**: Configure different thresholds for battery vs. + AC power + +This feature optimizes performance and power consumption by providing maximum +performance for demanding tasks while conserving energy during light workloads. + +> [!TIP] +> You can disable this logic with `enable_auto_turbo = false` to let the system +> handle turbo boost natively when `turbo = "auto"`. + +#### Turbo Boost Behavior Table + +The table below explains how different combinations of `turbo` and +`enable_auto_turbo` settings affect CPU turbo behavior: + +| Setting | `enable_auto_turbo = true` | `enable_auto_turbo = false` | +| ------------------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `turbo = "always"` | **Always enabled**
Turbo is always active regardless of CPU load or temperature | **Always enabled**
Turbo is always active regardless of CPU load or temperature | +| `turbo = "never"` | **Always disabled**
Turbo is always disabled regardless of CPU load or temperature | **Always disabled**
Turbo is always disabled regardless of CPU load or temperature | +| `turbo = "auto"` | **Dynamically managed**
Superfreq enables/disables turbo based on CPU load and temperature thresholds | **System default**
Turbo is reset to system's default enabled state and is managed by the hardware/kernel | + +> [!NOTE] +> When `turbo = "auto"` and `enable_auto_turbo = false`, Superfreq ensures that +> any previous turbo state restrictions are removed, allowing the +> hardware/kernel to manage turbo behavior according to its default algorithms. + ### Adaptive Polling Superfreq includes a "sophisticated" (euphemism for complicated) adaptive @@ -268,14 +326,16 @@ While reporting issues, please attach the results from `superfreq debug`. Contributions to Superfreq are always welcome! Whether it's bug reports, feature requests, or code contributions, please feel free to contribute. -If you are looking to reimplement features from auto_cpufreq, please consider -opening an issue first and let us know what you have in mind. Certain features -(such as the system tray) are deliberately ignored, and might not be desired in -the codebase as they stand. +> [!NOTE] +> If you are looking to reimplement features from auto-cpufreq, please consider +> opening an issue first and let us know what you have in mind. Certain features +> (such as the system tray) are deliberately ignored, and might not be desired +> in the codebase as they stand. Please discuss those features with us first :) ### Setup -You will need Cargo and Rust installed on your system. Rust 1.80 or later is required. +You will need Cargo and Rust installed on your system. Rust 1.85 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 @@ -285,9 +345,9 @@ nix develop ``` Non-Nix users may get the appropriate Cargo and Rust versions from their package -manager. +manager, or using something like Rustup. -### Formatting +### Formatting & Lints 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. diff --git a/src/config/types.rs b/src/config/types.rs index 6446e3e..63dff9a 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -50,7 +50,10 @@ pub struct ProfileConfig { pub min_freq_mhz: Option, pub max_freq_mhz: Option, pub platform_profile: Option, - pub turbo_auto_settings: Option, + #[serde(default)] + pub turbo_auto_settings: TurboAutoSettings, + #[serde(default)] + pub enable_auto_turbo: bool, #[serde(skip_serializing_if = "Option::is_none")] pub battery_charge_thresholds: Option, } @@ -65,7 +68,8 @@ impl Default for ProfileConfig { min_freq_mhz: None, // no override max_freq_mhz: None, // no override platform_profile: None, // no override - turbo_auto_settings: Some(TurboAutoSettings::default()), + turbo_auto_settings: TurboAutoSettings::default(), + enable_auto_turbo: default_enable_auto_turbo(), battery_charge_thresholds: None, } } @@ -124,6 +128,9 @@ pub struct ProfileConfigToml { pub min_freq_mhz: Option, pub max_freq_mhz: Option, pub platform_profile: Option, + pub turbo_auto_settings: Option, + #[serde(default = "default_enable_auto_turbo")] + pub enable_auto_turbo: bool, #[serde(skip_serializing_if = "Option::is_none")] pub battery_charge_thresholds: Option, } @@ -151,6 +158,8 @@ impl Default for ProfileConfigToml { min_freq_mhz: None, max_freq_mhz: None, platform_profile: None, + turbo_auto_settings: None, + enable_auto_turbo: default_enable_auto_turbo(), battery_charge_thresholds: None, } } @@ -164,12 +173,18 @@ pub struct TurboAutoSettings { pub load_threshold_low: f32, #[serde(default = "default_temp_threshold_high")] pub temp_threshold_high: f32, + /// Initial turbo boost state when no previous state exists. + /// Set to `true` to start with turbo enabled, `false` to start with turbo disabled. + /// This is only used at first launch or after a reset. + #[serde(default = "default_initial_turbo_state")] + pub initial_turbo_state: bool, } // Default thresholds for Auto turbo mode pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is above this pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this +pub const DEFAULT_INITIAL_TURBO_STATE: bool = false; // by default, start with turbo disabled const fn default_load_threshold_high() -> f32 { DEFAULT_LOAD_THRESHOLD_HIGH @@ -180,6 +195,9 @@ const fn default_load_threshold_low() -> f32 { const fn default_temp_threshold_high() -> f32 { DEFAULT_TEMP_THRESHOLD_HIGH } +const fn default_initial_turbo_state() -> bool { + DEFAULT_INITIAL_TURBO_STATE +} impl Default for TurboAutoSettings { fn default() -> Self { @@ -187,6 +205,7 @@ impl Default for TurboAutoSettings { load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH, load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW, temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH, + initial_turbo_state: DEFAULT_INITIAL_TURBO_STATE, } } } @@ -208,7 +227,8 @@ impl From for ProfileConfig { min_freq_mhz: toml_config.min_freq_mhz, max_freq_mhz: toml_config.max_freq_mhz, platform_profile: toml_config.platform_profile, - turbo_auto_settings: Some(TurboAutoSettings::default()), + turbo_auto_settings: toml_config.turbo_auto_settings.unwrap_or_default(), + enable_auto_turbo: toml_config.enable_auto_turbo, battery_charge_thresholds: toml_config.battery_charge_thresholds, } } @@ -282,6 +302,10 @@ const fn default_stats_file_path() -> Option { None } +const fn default_enable_auto_turbo() -> bool { + true +} + #[derive(Deserialize, Serialize, Debug, Clone)] pub struct DaemonConfigToml { #[serde(default = "default_poll_interval_sec")] diff --git a/src/cpu.rs b/src/cpu.rs index eeb4dfa..cbd37f8 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,6 +1,7 @@ use crate::core::{GovernorOverrideMode, TurboSetting}; use crate::util::error::ControlError; use core::str; +use log::debug; use std::{fs, io, path::Path, string::ToString}; pub type Result = std::result::Result; @@ -216,12 +217,19 @@ pub fn set_turbo(setting: TurboSetting) -> Result<()> { let value_pstate = match setting { TurboSetting::Always => "0", // no_turbo = 0 means turbo is enabled TurboSetting::Never => "1", // no_turbo = 1 means turbo is disabled - TurboSetting::Auto => return Err(ControlError::InvalidValueError("Turbo Auto cannot be directly set via intel_pstate/no_turbo or cpufreq/boost. System default.".to_string())), + // Auto mode is handled at the engine level, not directly at the sysfs level + TurboSetting::Auto => { + debug!("Turbo Auto mode is managed by engine logic based on system conditions"); + return Ok(()); + } }; let value_boost = match setting { TurboSetting::Always => "1", // boost = 1 means turbo is enabled TurboSetting::Never => "0", // boost = 0 means turbo is disabled - TurboSetting::Auto => return Err(ControlError::InvalidValueError("Turbo Auto cannot be directly set via intel_pstate/no_turbo or cpufreq/boost. System default.".to_string())), + TurboSetting::Auto => { + debug!("Turbo Auto mode is managed by engine logic based on system conditions"); + return Ok(()); + } }; // AMD specific paths diff --git a/src/engine.rs b/src/engine.rs index 791fa5a..7438dbb 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,6 +4,116 @@ use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::cpu::{self}; use crate::util::error::{ControlError, EngineError}; use log::{debug, info, warn}; +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 store the initial state so that it's visible before initialized=true + self.previous_state.store(initial_state, Ordering::Release); + + // Try to atomically change initialized from false to true + // Now, 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 + initial_state + } + Err(_) => { + // Another thread already initialized it. + // Read the current state in bitter defeat + 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: @@ -37,7 +147,7 @@ where } /// Determines the appropriate CPU profile based on power status or forced mode, -/// and applies the settings using functions from the `cpu` module. +/// and applies the settings (via helpers defined in the `cpu` module) pub fn determine_and_apply_settings( report: &SystemReport, config: &AppConfig, @@ -56,6 +166,17 @@ pub fn determine_and_apply_settings( })?; } + // 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 { @@ -70,17 +191,7 @@ pub fn determine_and_apply_settings( } } } else { - // Determine AC/Battery status - // For desktops (no batteries), we should always use the AC power profile - // For laptops, we check if any battery is present and not connected to AC - let on_ac_power = if report.batteries.is_empty() { - // No batteries means desktop/server, always on AC - true - } else { - // Check if any battery reports AC connected - report.batteries.iter().any(|b| b.ac_connected) - }; - + // Use the previously computed on_ac_power value if on_ac_power { info!("On AC power, selecting Charger profile."); selected_profile_config = &config.charger; @@ -112,8 +223,19 @@ pub fn determine_and_apply_settings( info!("Setting turbo to '{turbo_setting:?}'"); match turbo_setting { TurboSetting::Auto => { - debug!("Managing turbo in auto mode based on system conditions"); - manage_auto_turbo(report, selected_profile_config)?; + if selected_profile_config.enable_auto_turbo { + debug!("Managing turbo in auto mode based on system conditions"); + manage_auto_turbo(report, selected_profile_config, on_ac_power)?; + } else { + debug!( + "Superfreq'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:?}"), || { @@ -172,12 +294,16 @@ pub fn determine_and_apply_settings( Ok(()) } -fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<(), EngineError> { - // Get the auto turbo settings from the config, or use defaults - let turbo_settings = config.turbo_auto_settings.clone().unwrap_or_default(); +fn manage_auto_turbo( + report: &SystemReport, + config: &ProfileConfig, + on_ac_power: bool, +) -> Result<(), EngineError> { + // 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)?; + validate_turbo_auto_settings(turbo_settings)?; // Get average CPU temperature and CPU load let cpu_temp = report.cpu_global.average_temperature_celsius; @@ -204,40 +330,96 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<() } }; - // Decision logic for enabling/disabling turbo - let enable_turbo = match (cpu_temp, avg_cpu_usage) { + // 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 => { + (Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => { 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 => { + (_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => { 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 => { + (_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => { info!( "Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)", usage, turbo_settings.load_threshold_low ); false } - // In intermediate load scenarios or if we can't determine, leave turbo in current state - // For now, we'll disable it as a safe default - _ => { - info!("Auto Turbo: Disabled (default for indeterminate state)"); - 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 => + { + 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) => { + 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) => { + 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) => { + 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); + } + // Apply the turbo setting let turbo_setting = if enable_turbo { TurboSetting::Always @@ -258,22 +440,21 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<() } fn validate_turbo_auto_settings(settings: &TurboAutoSettings) -> Result<(), EngineError> { - // Validate load thresholds - if settings.load_threshold_high <= settings.load_threshold_low { + if settings.load_threshold_high <= settings.load_threshold_low + || settings.load_threshold_high > 100.0 + || settings.load_threshold_low < 0.0 + || settings.load_threshold_low > 100.0 + { return Err(EngineError::ConfigurationError( - "Invalid turbo auto settings: high threshold must be greater than low threshold" + "Invalid turbo auto settings: load thresholds must be between 0 % and 100 % with high > low" .to_string(), )); } - // Validate range of load thresholds (should be 0-100%) - if settings.load_threshold_high > 100.0 || settings.load_threshold_low < 0.0 { - return Err(EngineError::ConfigurationError( - "Invalid turbo auto settings: load thresholds must be between 0% and 100%".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" diff --git a/src/main.rs b/src/main.rs index 6e26645..f6303dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -140,7 +140,7 @@ fn main() -> Result<(), AppError> { format_section("CPU Global Info"); println!( - "Current Governor: {}", + "Current Governor: {}", report .cpu_global .current_governor @@ -148,11 +148,11 @@ fn main() -> Result<(), AppError> { .unwrap_or("N/A") ); println!( - "Available Governors: {}", + "Available Governors: {}", // 21 length baseline report.cpu_global.available_governors.join(", ") ); println!( - "Turbo Status: {}", + "Turbo Status: {}", match report.cpu_global.turbo_status { Some(true) => "Enabled", Some(false) => "Disabled", @@ -161,15 +161,15 @@ fn main() -> Result<(), AppError> { ); println!( - "EPP: {}", + "EPP: {}", report.cpu_global.epp.as_deref().unwrap_or("N/A") ); println!( - "EPB: {}", + "EPB: {}", report.cpu_global.epb.as_deref().unwrap_or("N/A") ); println!( - "Platform Profile: {}", + "Platform Profile: {}", report .cpu_global .platform_profile @@ -177,7 +177,7 @@ fn main() -> Result<(), AppError> { .unwrap_or("N/A") ); println!( - "CPU Temperature: {}", + "CPU Temperature: {}", report.cpu_global.average_temperature_celsius.map_or_else( || "N/A (No sensor detected)".to_string(), |t| format!("{t:.1}°C")