1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-08-02 03:47:46 +00:00

Compare commits

...

7 commits

10 changed files with 1159 additions and 1178 deletions

View file

@ -10,7 +10,7 @@ rust-version = "1.85"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
dirs = "6.0"
clap = { version = "4.0", features = ["derive"] }
clap = { version = "4.0", features = ["derive", "env"] }
num_cpus = "1.16"
ctrlc = "3.4"
log = "0.4"

296
src/config.rs Normal file
View file

@ -0,0 +1,296 @@
use std::{fs, path::Path};
use anyhow::{Context, bail};
use serde::{Deserialize, Serialize};
use crate::{cpu, power_supply};
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
}
#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
pub struct CpuDelta {
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
#[arg(short = 'c', long = "for")]
#[serde(rename = "for", skip_serializing_if = "is_default")]
pub for_: Option<Vec<u32>>,
/// Set the CPU governor.
#[arg(short = 'g', long)]
#[serde(skip_serializing_if = "is_default")]
pub governor: Option<String>, // TODO: Validate with clap for available governors.
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
#[arg(short = 'p', long, alias = "epp")]
#[serde(skip_serializing_if = "is_default")]
pub energy_performance_preference: Option<String>, // TODO: Validate with clap for available governors.
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
#[arg(short = 'b', long, alias = "epb")]
#[serde(skip_serializing_if = "is_default")]
pub energy_performance_bias: Option<String>, // TODO: Validate with clap for available governors.
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
#[serde(skip_serializing_if = "is_default")]
pub frequency_mhz_minimum: Option<u64>,
/// Set maximum CPU frequency in MHz. Short form: --freq-max.
#[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
#[serde(skip_serializing_if = "is_default")]
pub frequency_mhz_maximum: Option<u64>,
/// Set turbo boost behaviour. Has to be for all CPUs.
#[arg(short = 't', long, conflicts_with = "for_")]
#[serde(skip_serializing_if = "is_default")]
pub turbo: Option<bool>,
}
impl CpuDelta {
pub fn apply(&self) -> anyhow::Result<()> {
let cpus = match &self.for_ {
Some(numbers) => {
let mut cpus = Vec::with_capacity(numbers.len());
for &number in numbers {
cpus.push(cpu::Cpu::new(number)?);
}
cpus
}
None => cpu::Cpu::all().context("failed to get all CPUs and their information")?,
};
for cpu in cpus {
if let Some(governor) = self.governor.as_ref() {
cpu.set_governor(governor)?;
}
if let Some(epp) = self.energy_performance_preference.as_ref() {
cpu.set_epp(epp)?;
}
if let Some(epb) = self.energy_performance_bias.as_ref() {
cpu.set_epb(epb)?;
}
if let Some(mhz_minimum) = self.frequency_mhz_minimum {
cpu.set_frequency_minimum(mhz_minimum)?;
}
if let Some(mhz_maximum) = self.frequency_mhz_maximum {
cpu.set_frequency_maximum(mhz_maximum)?;
}
}
if let Some(turbo) = self.turbo {
cpu::Cpu::set_turbo(turbo)?;
}
Ok(())
}
}
#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
pub struct PowerDelta {
/// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies.
#[arg(short = 'p', long = "for")]
#[serde(rename = "for", skip_serializing_if = "is_default")]
pub for_: Option<Vec<String>>,
/// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start.
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
#[serde(skip_serializing_if = "is_default")]
pub charge_threshold_start: Option<u8>,
/// Set the percentage where charging will stop. Short form: --charge-end.
#[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))]
#[serde(skip_serializing_if = "is_default")]
pub charge_threshold_end: Option<u8>,
/// Set ACPI platform profile. Has to be for all power supplies.
#[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")]
#[serde(skip_serializing_if = "is_default")]
pub platform_profile: Option<String>,
}
impl PowerDelta {
pub fn apply(&self) -> anyhow::Result<()> {
let power_supplies = match &self.for_ {
Some(names) => {
let mut power_supplies = Vec::with_capacity(names.len());
for name in names {
power_supplies.push(power_supply::PowerSupply::from_name(name.clone())?);
}
power_supplies
}
None => power_supply::PowerSupply::all()?
.into_iter()
.filter(|power_supply| power_supply.threshold_config.is_some())
.collect(),
};
for power_supply in power_supplies {
if let Some(threshold_start) = self.charge_threshold_start {
power_supply.set_charge_threshold_start(threshold_start)?;
}
if let Some(threshold_end) = self.charge_threshold_end {
power_supply.set_charge_threshold_end(threshold_end)?;
}
}
if let Some(platform_profile) = self.platform_profile.as_ref() {
power_supply::PowerSupply::set_platform_profile(platform_profile)?;
}
Ok(())
}
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum Expression {
#[serde(rename = "%cpu-usage")]
CpuUsage,
#[serde(rename = "$cpu-usage-volatility")]
CpuUsageVolatility,
#[serde(rename = "$cpu-temperature")]
CpuTemperature,
#[serde(rename = "$cpu-temperature-volatility")]
CpuTemperatureVolatility,
#[serde(rename = "$cpu-idle-seconds")]
CpuIdleSeconds,
#[serde(rename = "%power-supply-charge")]
PowerSupplyCharge,
#[serde(rename = "%power-supply-discharge-rate")]
PowerSupplyDischargeRate,
#[serde(rename = "?charging")]
Charging,
#[serde(rename = "?on-battery")]
OnBattery,
#[serde(rename = "#false")]
False,
#[default]
#[serde(rename = "#true")]
True,
Number(f64),
Plus {
value: Box<Expression>,
plus: Box<Expression>,
},
Minus {
value: Box<Expression>,
minus: Box<Expression>,
},
Multiply {
value: Box<Expression>,
multiply: Box<Expression>,
},
Power {
value: Box<Expression>,
power: Box<Expression>,
},
Divide {
value: Box<Expression>,
divide: Box<Expression>,
},
LessThan {
value: Box<Expression>,
is_less_than: Box<Expression>,
},
MoreThan {
value: Box<Expression>,
is_more_than: Box<Expression>,
},
Equal {
value: Box<Expression>,
is_equal: Box<Expression>,
leeway: Box<Expression>,
},
And {
value: Box<Expression>,
and: Box<Expression>,
},
All {
all: Vec<Expression>,
},
Or {
value: Box<Expression>,
or: Box<Expression>,
},
Any {
any: Vec<Expression>,
},
Not {
not: Box<Expression>,
},
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Rule {
priority: u8,
#[serde(default, skip_serializing_if = "is_default")]
if_: Expression,
#[serde(default, skip_serializing_if = "is_default")]
cpu: CpuDelta,
#[serde(default, skip_serializing_if = "is_default")]
power: PowerDelta,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(transparent, default, rename_all = "kebab-case")]
pub struct DaemonConfig {
#[serde(rename = "rule")]
rules: Vec<Rule>,
}
impl DaemonConfig {
pub fn load_from(path: &Path) -> anyhow::Result<Self> {
let contents = fs::read_to_string(path).with_context(|| {
format!("failed to read config from '{path}'", path = path.display())
})?;
let config: Self = toml::from_str(&contents).context("failed to parse config file")?;
{
let mut priorities = Vec::with_capacity(config.rules.len());
for rule in &config.rules {
if priorities.contains(&rule.priority) {
bail!("each config rule must have a different priority")
}
priorities.push(rule.priority);
}
}
Ok(config)
}
}

View file

@ -1,128 +0,0 @@
// Configuration loading functionality
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Context as _;
use crate::config::types::{AppConfig, AppConfigToml, DaemonConfig, ProfileConfig};
/// The primary function to load application configuration from a specific path or from default locations.
///
/// # Arguments
///
/// * `specific_path` - If provided, only attempts to load from this path and errors if not found
///
/// # Returns
///
/// * `Ok(AppConfig)` - Successfully loaded configuration
/// * `Err(ConfigError)` - Error loading or parsing configuration
pub fn load_config() -> anyhow::Result<AppConfig> {
load_config_from_path(None)
}
/// Load configuration from a specific path or try default paths
pub fn load_config_from_path(specific_path: Option<&str>) -> anyhow::Result<AppConfig> {
// If a specific path is provided, only try that one
if let Some(path_str) = specific_path {
let path = Path::new(path_str);
if path.exists() {
return load_and_parse_config(path);
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Specified config file not found: {}", path.display()),
))?;
}
// Check for SUPERFREQ_CONFIG environment variable
if let Ok(env_path) = std::env::var("SUPERFREQ_CONFIG") {
let env_path = Path::new(&env_path);
if env_path.exists() {
println!(
"Loading config from SUPERFREQ_CONFIG: {}",
env_path.display()
);
return load_and_parse_config(env_path);
}
eprintln!(
"Warning: Config file specified by SUPERFREQ_CONFIG not found: {}",
env_path.display()
);
}
// System-wide paths
let config_paths = vec![
PathBuf::from("/etc/xdg/superfreq/config.toml"),
PathBuf::from("/etc/superfreq.toml"),
];
for path in config_paths {
if path.exists() {
println!("Loading config from: {}", path.display());
match load_and_parse_config(&path) {
Ok(config) => return Ok(config),
Err(e) => {
eprintln!("Error with config file {}: {}", path.display(), e);
// Continue trying other files
}
}
}
}
println!("No configuration file found or all failed to parse. Using default configuration.");
// Construct default AppConfig by converting default AppConfigToml
let default_toml_config = AppConfigToml::default();
Ok(AppConfig {
charger: ProfileConfig::from(default_toml_config.charger),
battery: ProfileConfig::from(default_toml_config.battery),
ignored_power_supplies: default_toml_config.ignored_power_supplies,
daemon: DaemonConfig::default(),
})
}
/// Load and parse a configuration file
fn load_and_parse_config(path: &Path) -> anyhow::Result<AppConfig> {
let contents = fs::read_to_string(path).with_context(|| {
format!(
"failed to read config file from '{path}'",
path = path.display(),
)
})?;
let toml_app_config =
toml::from_str::<AppConfigToml>(&contents).context("failed to parse config toml")?;
// Handle inheritance of values from global to profile configs
let mut charger_profile = toml_app_config.charger.clone();
let mut battery_profile = toml_app_config.battery.clone();
// Clone global battery_charge_thresholds once if it exists
if let Some(global_thresholds) = toml_app_config.battery_charge_thresholds {
// Apply to charger profile if not already set
if charger_profile.battery_charge_thresholds.is_none() {
charger_profile.battery_charge_thresholds = Some(global_thresholds.clone());
}
// Apply to battery profile if not already set
if battery_profile.battery_charge_thresholds.is_none() {
battery_profile.battery_charge_thresholds = Some(global_thresholds);
}
}
// Convert AppConfigToml to AppConfig
Ok(AppConfig {
charger: ProfileConfig::from(charger_profile),
battery: ProfileConfig::from(battery_profile),
ignored_power_supplies: toml_app_config.ignored_power_supplies,
daemon: DaemonConfig {
poll_interval_sec: toml_app_config.daemon.poll_interval_sec,
adaptive_interval: toml_app_config.daemon.adaptive_interval,
min_poll_interval_sec: toml_app_config.daemon.min_poll_interval_sec,
max_poll_interval_sec: toml_app_config.daemon.max_poll_interval_sec,
throttle_on_battery: toml_app_config.daemon.throttle_on_battery,
log_level: toml_app_config.daemon.log_level,
stats_file_path: toml_app_config.daemon.stats_file_path,
},
})
}

View file

@ -1,5 +0,0 @@
pub mod load;
pub mod types;
pub use load::*;
pub use types::*;

View file

@ -1,282 +0,0 @@
use anyhow::bail;
// Configuration types and structures for superfreq
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
/// Defines constant-returning functions used for default values.
/// This hopefully reduces repetition since we have way too many
/// default functions that just return constants.
macro_rules! default_const {
($($name:ident -> $type:ty = $value:expr;)*) => {
$(
const fn $name() -> $type {
$value
}
)*
};
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct PowerSupplyChargeThresholds {
pub start: u8,
pub stop: u8,
}
impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds {
type Error = anyhow::Error;
fn try_from((start, stop): (u8, u8)) -> anyhow::Result<Self> {
if stop == 0 {
bail!("stop threshold must be greater than 0%");
}
if start >= stop {
bail!("start threshold ({start}) must be less than stop threshold ({stop})");
}
if stop > 100 {
bail!("stop threshold ({stop}) cannot exceed 100%");
}
Ok(PowerSupplyChargeThresholds { start, stop })
}
}
// Structs for configuration using serde::Deserialize
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProfileConfig {
pub governor: Option<String>,
pub turbo: Option<bool>,
pub epp: Option<String>, // Energy Performance Preference (EPP)
pub epb: Option<String>, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs
pub min_freq_mhz: Option<u32>,
pub max_freq_mhz: Option<u32>,
pub platform_profile: Option<String>,
#[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<PowerSupplyChargeThresholds>,
}
impl Default for ProfileConfig {
fn default() -> Self {
Self {
governor: Some("schedutil".to_string()), // common sensible default (?)
turbo: None,
epp: None, // defaults depend on governor and system
epb: None, // defaults depend on governor and system
min_freq_mhz: None, // no override
max_freq_mhz: None, // no override
platform_profile: None, // no override
turbo_auto_settings: TurboAutoSettings::default(),
enable_auto_turbo: default_enable_auto_turbo(),
battery_charge_thresholds: None,
}
}
}
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
pub struct AppConfig {
#[serde(default)]
pub charger: ProfileConfig,
#[serde(default)]
pub battery: ProfileConfig,
pub ignored_power_supplies: Option<Vec<String>>,
#[serde(default)]
pub daemon: DaemonConfig,
}
// Intermediate structs for TOML parsing
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProfileConfigToml {
pub governor: Option<String>,
pub turbo: Option<String>, // "always", "auto", "never"
pub epp: Option<String>,
pub epb: Option<String>,
pub min_freq_mhz: Option<u32>,
pub max_freq_mhz: Option<u32>,
pub platform_profile: Option<String>,
pub turbo_auto_settings: Option<TurboAutoSettings>,
#[serde(default = "default_enable_auto_turbo")]
pub enable_auto_turbo: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct AppConfigToml {
#[serde(default)]
pub charger: ProfileConfigToml,
#[serde(default)]
pub battery: ProfileConfigToml,
#[serde(skip_serializing_if = "Option::is_none")]
pub battery_charge_thresholds: Option<PowerSupplyChargeThresholds>,
pub ignored_power_supplies: Option<Vec<String>>,
#[serde(default)]
pub daemon: DaemonConfigToml,
}
impl Default for ProfileConfigToml {
fn default() -> Self {
Self {
governor: Some("schedutil".to_string()),
turbo: Some("auto".to_string()),
epp: None,
epb: None,
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,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TurboAutoSettings {
#[serde(default = "default_load_threshold_high")]
pub load_threshold_high: f32,
#[serde(default = "default_load_threshold_low")]
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
default_const! {
default_load_threshold_high -> f32 = DEFAULT_LOAD_THRESHOLD_HIGH;
default_load_threshold_low -> f32 = DEFAULT_LOAD_THRESHOLD_LOW;
default_temp_threshold_high -> f32 = DEFAULT_TEMP_THRESHOLD_HIGH;
default_initial_turbo_state -> bool = DEFAULT_INITIAL_TURBO_STATE;
}
impl Default for TurboAutoSettings {
fn default() -> Self {
Self {
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,
}
}
}
impl From<ProfileConfigToml> for ProfileConfig {
fn from(toml_config: ProfileConfigToml) -> Self {
Self {
governor: toml_config.governor,
turbo: toml_config
.turbo
.and_then(|s| match s.to_lowercase().as_str() {
"always" => Some(true),
"auto" => None,
"never" => Some(false),
_ => panic!("invalid turbo value: {s}, must be one of: always, auto, never"),
}),
epp: toml_config.epp,
epb: toml_config.epb,
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: toml_config.turbo_auto_settings.unwrap_or_default(),
enable_auto_turbo: toml_config.enable_auto_turbo,
battery_charge_thresholds: toml_config.battery_charge_thresholds,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DaemonConfig {
#[serde(default = "default_poll_interval_sec")]
pub poll_interval_sec: u64,
#[serde(default = "default_adaptive_interval")]
pub adaptive_interval: bool,
#[serde(default = "default_min_poll_interval_sec")]
pub min_poll_interval_sec: u64,
#[serde(default = "default_max_poll_interval_sec")]
pub max_poll_interval_sec: u64,
#[serde(default = "default_throttle_on_battery")]
pub throttle_on_battery: bool,
#[serde(default = "default_log_level")]
pub log_level: LogLevel,
#[serde(default = "default_stats_file_path")]
pub stats_file_path: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Error,
Warning,
Info,
Debug,
}
impl Default for DaemonConfig {
fn default() -> Self {
Self {
poll_interval_sec: default_poll_interval_sec(),
adaptive_interval: default_adaptive_interval(),
min_poll_interval_sec: default_min_poll_interval_sec(),
max_poll_interval_sec: default_max_poll_interval_sec(),
throttle_on_battery: default_throttle_on_battery(),
log_level: default_log_level(),
stats_file_path: default_stats_file_path(),
}
}
}
default_const! {
default_poll_interval_sec -> u64 = 5;
default_adaptive_interval -> bool = false;
default_min_poll_interval_sec -> u64 = 1;
default_max_poll_interval_sec -> u64 = 30;
default_throttle_on_battery -> bool = true;
default_log_level -> LogLevel = LogLevel::Info;
default_stats_file_path -> Option<String> = None;
default_enable_auto_turbo -> bool = true;
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DaemonConfigToml {
#[serde(default = "default_poll_interval_sec")]
pub poll_interval_sec: u64,
#[serde(default = "default_adaptive_interval")]
pub adaptive_interval: bool,
#[serde(default = "default_min_poll_interval_sec")]
pub min_poll_interval_sec: u64,
#[serde(default = "default_max_poll_interval_sec")]
pub max_poll_interval_sec: u64,
#[serde(default = "default_throttle_on_battery")]
pub throttle_on_battery: bool,
#[serde(default = "default_log_level")]
pub log_level: LogLevel,
#[serde(default = "default_stats_file_path")]
pub stats_file_path: Option<String>,
}
impl Default for DaemonConfigToml {
fn default() -> Self {
Self {
poll_interval_sec: default_poll_interval_sec(),
adaptive_interval: default_adaptive_interval(),
min_poll_interval_sec: default_min_poll_interval_sec(),
max_poll_interval_sec: default_max_poll_interval_sec(),
throttle_on_battery: default_throttle_on_battery(),
log_level: default_log_level(),
stats_file_path: default_stats_file_path(),
}
}
}

View file

@ -1,4 +1,5 @@
use anyhow::{Context, bail};
use yansi::Paint as _;
use std::{fmt, fs, path::Path, string::ToString};
@ -14,7 +15,7 @@ fn read_u64(path: impl AsRef<Path>) -> anyhow::Result<u64> {
let content = fs::read_to_string(path)?;
Ok(content.trim().parse::<u64>()?)
Ok(content.trim().parse()?)
}
fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
@ -36,7 +37,8 @@ pub struct Cpu {
impl fmt::Display for Cpu {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { number, .. } = self;
let number = self.number.cyan();
write!(f, "CPU {number}")
}
}
@ -73,7 +75,7 @@ impl Cpu {
};
// Has to match "cpu{N}".
let Ok(number) = cpu_prefix_removed.parse::<u32>() else {
let Ok(number) = cpu_prefix_removed.parse() else {
continue;
};
@ -92,10 +94,13 @@ impl Cpu {
/// Rescan CPU, tuning local copy of settings.
pub fn rescan(&mut self) -> anyhow::Result<()> {
let has_cpufreq = exists(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq",
number = self.number,
));
let Self { number, .. } = self;
if !exists(format!("/sys/devices/system/cpu/cpu{number}")) {
bail!("{self} does not exist");
}
let has_cpufreq = exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
self.has_cpufreq = has_cpufreq;
@ -236,7 +241,7 @@ impl Cpu {
self.validate_frequency_minimum(frequency_mhz)?;
// We use u64 for the intermediate calculation to prevent overflow
let frequency_khz = u64::from(frequency_mhz) * 1000;
let frequency_khz = frequency_mhz * 1000;
let frequency_khz = frequency_khz.to_string();
write(
@ -258,7 +263,7 @@ impl Cpu {
return Ok(());
};
if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz {
if new_frequency_mhz * 1000 < minimum_frequency_khz {
bail!(
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for {self}",
minimum_frequency_khz / 1000,
@ -274,7 +279,7 @@ impl Cpu {
self.validate_frequency_maximum(frequency_mhz)?;
// We use u64 for the intermediate calculation to prevent overflow
let frequency_khz = u64::from(frequency_mhz) * 1000;
let frequency_khz = frequency_mhz * 1000;
let frequency_khz = frequency_khz.to_string();
write(
@ -344,7 +349,7 @@ impl Cpu {
let Cpu { number, .. } = cpu;
write(
&format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"),
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"),
value_boost,
)
.is_ok()

View file

@ -1,649 +1,222 @@
use anyhow::Context;
use anyhow::bail;
use std::{
collections::VecDeque,
ops,
time::{Duration, Instant},
};
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};
use crate::config;
/// 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
/// Calculate the idle time multiplier based on system idle time.
///
/// Returns a multiplier between 1.0 and 5.0 (capped):
/// Returns a multiplier between 1.0 and 5.0:
/// - 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)
fn idle_multiplier(idle_for: Duration) -> f64 {
let factor = match idle_for.as_secs() < 120 {
// Less than 2 minutes.
// 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;
true => (idle_for.as_secs() as f64) / 120.0,
// 2 minutes or more.
// Logarithmic scaling: 1.0 + log2(minutes)
1.0 + (idle_time_minutes as f32).log2().max(0.5)
false => {
let idle_minutes = idle_for.as_secs() as f64 / 60.0;
idle_minutes.log2()
}
};
// Cap the multiplier to avoid excessive intervals
idle_factor.min(5.0) // max factor of 5x
// Clamp the multiplier to avoid excessive intervals.
(1.0 + factor).clamp(1.0, 5.0)
}
/// Calculate optimal polling interval based on system conditions and history
///
/// Returns Ok with the calculated interval, or Err if the configuration is invalid
fn compute_new(params: &IntervalParams, system_history: &SystemHistory) -> anyhow::Result<u64> {
// Use the centralized validation function
validate_poll_intervals(params.min_interval, params.max_interval)?;
// 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);
log::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);
}
// Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval
let min_safe_interval = params.min_interval.max(1);
let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval);
// Blend the new interval with the cached value if available
let blended_interval = if let Some(cached) = system_history.last_computed_interval {
// Use a weighted average: 70% previous value, 30% new value
// This smooths out drastic changes in polling frequency
const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70%
const NEW_VALUE_WEIGHT: u128 = 3; // 30%
const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10
// XXX: Use u128 arithmetic to avoid overflow with large interval values
let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT
+ u128::from(new_interval) * NEW_VALUE_WEIGHT)
/ TOTAL_WEIGHT;
result as u64
} else {
new_interval
};
// Blended result still needs to respect the configured bounds
// Again enforce minimum of 1 second regardless of params.min_interval
Ok(blended_interval.clamp(min_safe_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
struct Daemon {
/// Last time when there was 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,
/// Last computed optimal polling interval
last_computed_interval: Option<u64>,
/// CPU usage and temperature log.
cpu_log: VecDeque<CpuLog>,
/// Power supply status log.
power_supply_log: VecDeque<PowerSupplyLog>,
charging: bool,
}
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(),
last_computed_interval: None,
}
}
struct CpuLog {
at: Instant,
/// CPU usage between 0-1, a percentage.
usage: f64,
/// CPU temperature in celcius.
temperature: f64,
}
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;
struct CpuVolatility {
at: ops::Range<Instant>,
for core in &report.cpu_cores {
if let Some(usage) = core.usage_percent {
total_usage += usage;
core_count += 1;
}
}
usage: f64,
if core_count > 0 {
let avg_usage = total_usage / core_count as f32;
temperature: f64,
}
// 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");
}
}
impl Daemon {
fn cpu_volatility(&self) -> Option<CpuVolatility> {
if self.cpu_log.len() < 2 {
return None;
}
// 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);
let change_count = self.cpu_log.len() - 1;
// 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");
}
}
let mut usage_change_sum = 0.0;
let mut temperature_change_sum = 0.0;
for index in 0..change_count {
let usage_change = self.cpu_log[index + 1].usage - self.cpu_log[index].usage;
usage_change_sum += usage_change.abs();
let temperature_change =
self.cpu_log[index + 1].temperature - self.cpu_log[index].temperature;
temperature_change_sum += temperature_change.abs();
}
// 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;
}
Some(CpuVolatility {
at: self.cpu_log.front().unwrap().at..self.cpu_log.back().unwrap().at,
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");
}
usage: usage_change_sum / change_count as f64,
temperature: temperature_change_sum / change_count as f64,
})
}
/// 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;
}
fn is_cpu_idle(&self) -> bool {
let recent_log_count = self
.cpu_log
.iter()
.rev()
.take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60))
.count();
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() {
if recent_log_count < 2 {
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
}
let recent_average = self
.cpu_log
.iter()
.rev()
.take(recent_log_count)
.map(|log| log.usage)
.sum::<f64>()
/ recent_log_count as f64;
/// Calculate optimal polling interval based on system conditions
fn calculate_optimal_interval(
&self,
config: &AppConfig,
on_battery: bool,
) -> anyhow::Result<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(&params, self)
recent_average < 0.1
&& self
.cpu_volatility()
.is_none_or(|volatility| volatility.usage < 0.05)
}
}
/// Validates that poll interval configuration is consistent
/// Returns Ok if configuration is valid, Err with a descriptive message if invalid
fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> anyhow::Result<()> {
if min_interval < 1 {
bail!("min_interval must be ≥ 1");
}
if max_interval < 1 {
bail!("max_interval must be ≥ 1");
}
if max_interval >= min_interval {
Ok(())
} else {
bail!(
"Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})"
);
struct PowerSupplyLog {
at: Instant,
/// Charge 0-1, as a percentage.
charge: f64,
}
impl Daemon {
/// Calculates the discharge rate, returns a number between 0 and 1.
///
/// The discharge rate is averaged per hour.
/// So a return value of Some(0.3) means the battery has been
/// discharging 30% per hour.
fn power_supply_discharge_rate(&self) -> Option<f64> {
let mut last_charge = None;
// A list of increasing charge percentages.
let discharging: Vec<&PowerSupplyLog> = self
.power_supply_log
.iter()
.rev()
.take_while(move |log| {
let Some(last_charge_value) = last_charge else {
last_charge = Some(log.charge);
return true;
};
last_charge = Some(log.charge);
log.charge > last_charge_value
})
.collect();
if discharging.len() < 2 {
return None;
}
// Start of discharging. Has the most charge.
let start = discharging.last().unwrap();
// End of discharging, very close to now. Has the least charge.
let end = discharging.first().unwrap();
let discharging_duration_seconds = (start.at - end.at).as_secs_f64();
let discharging_duration_hours = discharging_duration_seconds / 60.0 / 60.0;
let discharged = start.charge - end.charge;
Some(discharged / discharging_duration_hours)
}
}
/// Run the daemon
pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> {
log::info!("Starting superfreq daemon...");
impl Daemon {
fn polling_interval(&self) -> Duration {
let mut interval = Duration::from_secs(5);
// 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(&current_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"
);
// We are on battery, so we must be more conservative with our polling.
if !self.charging {
match self.power_supply_discharge_rate() {
Some(discharge_rate) => {
if discharge_rate > 0.2 {
interval *= 3;
} else if discharge_rate > 0.1 {
interval *= 2;
} 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"
);
}
// *= 1.5;
interval /= 2;
interval *= 3;
}
}
}
Err(e) => {
log::error!("Error collecting system report: {e}");
None => {
interval *= 2;
}
}
}
// 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);
if self.is_cpu_idle() {
let idle_for = self.last_user_activity.elapsed();
if idle_for > Duration::from_secs(30) {
let factor = idle_multiplier(idle_for);
log::debug!(
"system has been idle for {seconds} seconds (approx {minutes} minutes), applying idle factor: {factor:.2}x",
seconds = idle_for.as_secs(),
minutes = idle_for.as_secs() / 60,
);
interval = Duration::from_secs_f64(interval.as_secs_f64() * factor);
}
}
if let Some(volatility) = self.cpu_volatility() {
if volatility.usage > 0.1 || volatility.temperature > 0.02 {
interval = (interval / 2).max(Duration::from_secs(1));
}
}
todo!("implement rest from daemon_old.rs")
}
}
log::info!("Daemon stopped");
pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
Ok(())
}
/// Write current system stats to a file for --stats to read
fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> {
let mut file = File::create(path)?;
writeln!(file, "timestamp={:?}", report.timestamp)?;
// CPU info
writeln!(file, "governor={:?}", report.cpu_global.current_governor)?;
writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?;
if let Some(temp) = report.cpu_global.average_temperature_celsius {
writeln!(file, "cpu_temp={temp:.1}")?;
}
// Battery info
if !report.batteries.is_empty() {
let battery = &report.batteries[0];
writeln!(file, "ac_power={}", battery.ac_connected)?;
if let Some(cap) = battery.capacity_percent {
writeln!(file, "battery_percent={cap}")?;
}
}
// System load
writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?;
writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?;
writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?;
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
}

649
src/daemon_old.rs Normal file
View file

@ -0,0 +1,649 @@
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};
/// 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
///
/// Returns Ok with the calculated interval, or Err if the configuration is invalid
fn compute_new(params: &IntervalParams, system_history: &SystemHistory) -> anyhow::Result<u64> {
// Use the centralized validation function
validate_poll_intervals(params.min_interval, params.max_interval)?;
// 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);
log::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);
}
// Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval
let min_safe_interval = params.min_interval.max(1);
let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval);
// Blend the new interval with the cached value if available
let blended_interval = if let Some(cached) = system_history.last_computed_interval {
// Use a weighted average: 70% previous value, 30% new value
// This smooths out drastic changes in polling frequency
const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70%
const NEW_VALUE_WEIGHT: u128 = 3; // 30%
const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10
// XXX: Use u128 arithmetic to avoid overflow with large interval values
let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT
+ u128::from(new_interval) * NEW_VALUE_WEIGHT)
/ TOTAL_WEIGHT;
result as u64
} else {
new_interval
};
// Blended result still needs to respect the configured bounds
// Again enforce minimum of 1 second regardless of params.min_interval
Ok(blended_interval.clamp(min_safe_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,
/// Last computed optimal polling interval
last_computed_interval: Option<u64>,
}
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(),
last_computed_interval: None,
}
}
}
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::<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,
) -> anyhow::Result<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(&params, self)
}
}
/// Validates that poll interval configuration is consistent
/// Returns Ok if configuration is valid, Err with a descriptive message if invalid
fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> anyhow::Result<()> {
if min_interval < 1 {
bail!("min_interval must be ≥ 1");
}
if max_interval < 1 {
bail!("max_interval must be ≥ 1");
}
if max_interval >= min_interval {
Ok(())
} else {
bail!(
"Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})"
);
}
}
/// 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(&current_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(())
}
/// Write current system stats to a file for --stats to read
fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> {
let mut file = File::create(path)?;
writeln!(file, "timestamp={:?}", report.timestamp)?;
// CPU info
writeln!(file, "governor={:?}", report.cpu_global.current_governor)?;
writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?;
if let Some(temp) = report.cpu_global.average_temperature_celsius {
writeln!(file, "cpu_temp={temp:.1}")?;
}
// Battery info
if !report.batteries.is_empty() {
let battery = &report.batteries[0];
writeln!(file, "ac_power={}", battery.ac_connected)?;
if let Some(cap) = battery.capacity_percent {
writeln!(file, "battery_percent={cap}")?;
}
}
// System load
writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?;
writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?;
writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?;
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
}

View file

@ -1,15 +1,16 @@
mod config;
mod core;
// mod core;
mod cpu;
mod daemon;
mod engine;
mod monitor;
// mod engine;
// mod monitor;
mod power_supply;
use anyhow::Context;
use clap::Parser as _;
use std::fmt::Write as _;
use std::io::Write as _;
use std::path::PathBuf;
use std::{io, process};
use yansi::Paint as _;
@ -29,57 +30,17 @@ enum Command {
Info,
/// Start the daemon.
Start,
Start {
/// The daemon config path.
#[arg(long, env = "SUPERFREQ_CONFIG")]
config: PathBuf,
},
/// Modify CPU attributes.
CpuSet {
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
#[arg(short = 'c', long = "for")]
for_: Option<Vec<u32>>,
/// Set the CPU governor.
#[arg(short = 'g', long)]
governor: Option<String>, // TODO: Validate with clap for available governors.
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
#[arg(short = 'p', long, alias = "epp")]
energy_performance_preference: Option<String>,
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
#[arg(short = 'b', long, alias = "epb")]
energy_performance_bias: Option<String>,
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
frequency_mhz_minimum: Option<u64>,
/// Set maximum CPU frequency in MHz. Short form: --freq-max.
#[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))]
frequency_mhz_maximum: Option<u64>,
/// Set turbo boost behaviour. Has to be for all CPUs.
#[arg(short = 't', long, conflicts_with = "for_")]
turbo: Option<bool>,
},
CpuSet(config::CpuDelta),
/// Modify power supply attributes.
PowerSet {
/// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies.
#[arg(short = 'p', long = "for")]
for_: Option<Vec<String>>,
/// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start.
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
charge_threshold_start: Option<u8>,
/// Set the percentage where charging will stop. Short form: --charge-end.
#[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))]
charge_threshold_end: Option<u8>,
/// Set ACPI platform profile. Has to be for all power supplies.
#[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")]
platform_profile: Option<String>,
},
PowerSet(config::PowerDelta),
}
fn real_main() -> anyhow::Result<()> {
@ -91,106 +52,18 @@ fn real_main() -> anyhow::Result<()> {
.format_module_path(false)
.init();
let config = config::load_config().context("failed to load config")?;
match cli.command {
Command::Info => todo!(),
Command::Start => {
daemon::run_daemon(config)?;
Ok(())
Command::Start { config } => {
let config = config::DaemonConfig::load_from(&config)
.context("failed to load daemon config file")?;
daemon::run(config)
}
Command::CpuSet {
for_,
governor,
energy_performance_preference,
energy_performance_bias,
frequency_mhz_minimum,
frequency_mhz_maximum,
turbo,
} => {
let cpus = match for_ {
Some(numbers) => {
let mut cpus = Vec::with_capacity(numbers.len());
for number in numbers {
cpus.push(cpu::Cpu::new(number)?);
}
cpus
}
None => cpu::Cpu::all()?,
};
for cpu in cpus {
if let Some(governor) = governor.as_ref() {
cpu.set_governor(governor)?;
}
if let Some(epp) = energy_performance_preference.as_ref() {
cpu.set_epp(epp)?;
}
if let Some(epb) = energy_performance_bias.as_ref() {
cpu.set_epb(epb)?;
}
if let Some(mhz_minimum) = frequency_mhz_minimum {
cpu.set_frequency_minimum(mhz_minimum)?;
}
if let Some(mhz_maximum) = frequency_mhz_maximum {
cpu.set_frequency_maximum(mhz_maximum)?;
}
}
if let Some(turbo) = turbo {
cpu::Cpu::set_turbo(turbo)?;
}
Ok(())
}
Command::PowerSet {
for_,
charge_threshold_start,
charge_threshold_end,
platform_profile,
} => {
let power_supplies = match for_ {
Some(names) => {
let mut power_supplies = Vec::with_capacity(names.len());
for name in names {
power_supplies.push(power_supply::PowerSupply::from_name(name)?);
}
power_supplies
}
None => power_supply::PowerSupply::all()?
.into_iter()
.filter(|power_supply| power_supply.threshold_config.is_some())
.collect(),
};
for power_supply in power_supplies {
if let Some(threshold_start) = charge_threshold_start {
power_supply.set_charge_threshold_start(threshold_start)?;
}
if let Some(threshold_end) = charge_threshold_end {
power_supply.set_charge_threshold_end(threshold_end)?;
}
}
if let Some(platform_profile) = platform_profile.as_ref() {
power_supply::PowerSupply::set_platform_profile(platform_profile);
}
Ok(())
}
Command::CpuSet(delta) => delta.apply(),
Command::PowerSet(delta) => delta.apply(),
}
}

View file

@ -1,4 +1,5 @@
use anyhow::{Context, bail};
use yansi::Paint as _;
use std::{
fmt, fs,
@ -62,18 +63,13 @@ pub struct PowerSupply {
impl fmt::Display for PowerSupply {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"power supply '{name}' at '{path}'",
name = &self.name,
path = self.path.display(),
)?;
write!(f, "power supply '{name}'", name = self.name.yellow())?;
if let Some(config) = self.threshold_config.as_ref() {
write!(
f,
" from manufacturer '{manufacturer}'",
manufacturer = config.manufacturer,
manufacturer = config.manufacturer.green(),
)?;
}
@ -147,6 +143,10 @@ impl PowerSupply {
}
pub fn rescan(&mut self) -> anyhow::Result<()> {
if !self.path.exists() {
bail!("{self} does not exist");
}
let threshold_config = self
.get_type()
.with_context(|| format!("failed to determine what type of power supply '{self}' is"))?