mirror of
https://github.com/RGBCube/watt
synced 2025-08-02 10:57:47 +00:00
Compare commits
No commits in common. "e9e1df90e6d3f263143168ae8d7400ca83469631" and "ce83ba3c91ee6b355971f0b33cc387aefcb1cff6" have entirely different histories.
e9e1df90e6
...
ce83ba3c91
14 changed files with 2629 additions and 2476 deletions
|
@ -1,30 +0,0 @@
|
||||||
# Taken from https://github.com/cull-os/carcass.
|
|
||||||
# Modified to have 2 space indents and 80 line width.
|
|
||||||
|
|
||||||
# float_literal_trailing_zero = "Always" # TODO: Warning for some reason?
|
|
||||||
condense_wildcard_suffixes = true
|
|
||||||
doc_comment_code_block_width = 80
|
|
||||||
edition = "2024" # Keep in sync with Cargo.toml.
|
|
||||||
enum_discrim_align_threshold = 60
|
|
||||||
force_explicit_abi = false
|
|
||||||
force_multiline_blocks = true
|
|
||||||
format_code_in_doc_comments = true
|
|
||||||
format_macro_matchers = true
|
|
||||||
format_strings = true
|
|
||||||
group_imports = "StdExternalCrate"
|
|
||||||
hex_literal_case = "Upper"
|
|
||||||
imports_granularity = "Crate"
|
|
||||||
imports_layout = "Vertical"
|
|
||||||
inline_attribute_width = 60
|
|
||||||
match_block_trailing_comma = true
|
|
||||||
max_width = 80
|
|
||||||
newline_style = "Unix"
|
|
||||||
normalize_comments = true
|
|
||||||
normalize_doc_attributes = true
|
|
||||||
overflow_delimited_expr = true
|
|
||||||
struct_field_align_threshold = 60
|
|
||||||
tab_spaces = 2
|
|
||||||
unstable_features = true
|
|
||||||
use_field_init_shorthand = true
|
|
||||||
use_try_shorthand = true
|
|
||||||
wrap_comments = true
|
|
15
.taplo.toml
15
.taplo.toml
|
@ -1,15 +0,0 @@
|
||||||
# Taken from https://github.com/cull-os/carcass.
|
|
||||||
|
|
||||||
[formatting]
|
|
||||||
align_entries = true
|
|
||||||
column_width = 100
|
|
||||||
compact_arrays = false
|
|
||||||
reorder_inline_tables = true
|
|
||||||
reorder_keys = true
|
|
||||||
|
|
||||||
[[rule]]
|
|
||||||
include = [ "**/Cargo.toml" ]
|
|
||||||
keys = [ "package" ]
|
|
||||||
|
|
||||||
[rule.formatting]
|
|
||||||
reorder_keys = false
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -3,19 +3,19 @@ name = "watt"
|
||||||
description = "Modern CPU frequency and power management utility for Linux"
|
description = "Modern CPU frequency and power management utility for Linux"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = [ "NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>" ]
|
authors = ["NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>"]
|
||||||
rust-version = "1.85"
|
rust-version = "1.85"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
clap = { version = "4.0", features = [ "derive", "env" ] }
|
clap = { version = "4.0", features = ["derive", "env"] }
|
||||||
clap-verbosity-flag = "3.0.2"
|
clap-verbosity-flag = "3.0.2"
|
||||||
ctrlc = "3.4"
|
ctrlc = "3.4"
|
||||||
derive_more = { version = "2.0.1", features = [ "full" ] }
|
derive_more = { version = "2.0.1", features = ["full"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
yansi = { version = "1.0.1", features = [ "detect-env", "detect-tty" ] }
|
yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] }
|
||||||
|
|
16
build.rs
16
build.rs
|
@ -1,14 +1,11 @@
|
||||||
use std::{
|
use std::env;
|
||||||
env,
|
use std::fs;
|
||||||
fs,
|
use std::path::PathBuf;
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MULTICALL_NAMES: &[&str] = &["cpu", "power"];
|
const MULTICALL_NAMES: &[&str] = &["cpu", "power"];
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
println!("cargo:rerun-if-changed=target");
|
|
||||||
|
|
||||||
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||||
let target = out_dir
|
let target = out_dir
|
||||||
|
@ -45,12 +42,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
if errored {
|
if errored {
|
||||||
println!(
|
println!(
|
||||||
"cargo:warning=this often happens because the target binary isn't built \
|
"cargo:warning=this often happens because the target binary isn't built yet, try running `cargo build` again"
|
||||||
yet, try running `cargo build` again"
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"cargo:warning=keep in mind that this is for development purposes only"
|
|
||||||
);
|
);
|
||||||
|
println!("cargo:warning=keep in mind that this is for development purposes only");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
74
config.toml
74
config.toml
|
@ -5,101 +5,107 @@
|
||||||
|
|
||||||
# Emergency thermal protection (highest priority).
|
# Emergency thermal protection (highest priority).
|
||||||
[[rule]]
|
[[rule]]
|
||||||
|
priority = 100
|
||||||
|
if = { value = "$cpu-temperature", is-more-than = 85.0 }
|
||||||
|
cpu.governor = "powersave"
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 2000
|
cpu.frequency-mhz-maximum = 2000
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.turbo = false
|
cpu.turbo = false
|
||||||
if = { value = "$cpu-temperature", is-more-than = 85.0 }
|
|
||||||
priority = 100
|
|
||||||
|
|
||||||
# Critical battery preservation.
|
# Critical battery preservation.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
|
priority = 90
|
||||||
|
if.all = [
|
||||||
|
"?discharging",
|
||||||
|
{ value = "%power-supply-charge", is-less-than = 0.3 },
|
||||||
|
]
|
||||||
|
cpu.governor = "powersave"
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 800 # More aggressive below critical threshold.
|
cpu.frequency-mhz-maximum = 800 # More aggressive below critical threshold.
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.turbo = false
|
cpu.turbo = false
|
||||||
if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.3 } ]
|
|
||||||
power.platform-profile = "low-power"
|
power.platform-profile = "low-power"
|
||||||
priority = 90
|
|
||||||
|
|
||||||
# High performance mode for sustained high load.
|
# High performance mode for sustained high load.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
cpu.energy-performance-preference = "performance"
|
priority = 80
|
||||||
cpu.governor = "performance"
|
|
||||||
cpu.turbo = true
|
|
||||||
if.all = [
|
if.all = [
|
||||||
{ value = "%cpu-usage", is-more-than = 0.8 },
|
{ value = "%cpu-usage", is-more-than = 0.8 },
|
||||||
{ value = "$cpu-idle-seconds", is-less-than = 30.0 },
|
{ value = "$cpu-idle-seconds", is-less-than = 30.0 },
|
||||||
{ value = "$cpu-temperature", is-less-than = 75.0 },
|
{ value = "$cpu-temperature", is-less-than = 75.0 },
|
||||||
]
|
]
|
||||||
priority = 80
|
cpu.governor = "performance"
|
||||||
|
cpu.energy-performance-preference = "performance"
|
||||||
|
cpu.turbo = true
|
||||||
|
|
||||||
# Performance mode when not discharging.
|
# Performance mode when not discharging.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
cpu.energy-performance-bias = "balance_performance"
|
priority = 70
|
||||||
cpu.energy-performance-preference = "performance"
|
|
||||||
cpu.governor = "performance"
|
|
||||||
cpu.turbo = true
|
|
||||||
if.all = [
|
if.all = [
|
||||||
{ not = "?discharging" },
|
{ not = "?discharging" },
|
||||||
{ value = "%cpu-usage", is-more-than = 0.1 },
|
{ value = "%cpu-usage", is-more-than = 0.1 },
|
||||||
{ value = "$cpu-temperature", is-less-than = 80.0 },
|
{ value = "$cpu-temperature", is-less-than = 80.0 },
|
||||||
]
|
]
|
||||||
priority = 70
|
cpu.governor = "performance"
|
||||||
|
cpu.energy-performance-preference = "performance"
|
||||||
|
cpu.energy-performance-bias = "balance_performance"
|
||||||
|
cpu.turbo = true
|
||||||
|
|
||||||
# Moderate performance for medium load.
|
# Moderate performance for medium load.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
cpu.energy-performance-preference = "balance_performance"
|
priority = 60
|
||||||
cpu.governor = "schedutil"
|
|
||||||
if.all = [
|
if.all = [
|
||||||
{ value = "%cpu-usage", is-more-than = 0.4 },
|
{ value = "%cpu-usage", is-more-than = 0.4 },
|
||||||
{ value = "%cpu-usage", is-less-than = 0.8 },
|
{ value = "%cpu-usage", is-less-than = 0.8 },
|
||||||
]
|
]
|
||||||
priority = 60
|
cpu.governor = "schedutil"
|
||||||
|
cpu.energy-performance-preference = "balance_performance"
|
||||||
|
|
||||||
# Power saving during low activity.
|
# Power saving during low activity.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
cpu.energy-performance-preference = "power"
|
priority = 50
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.turbo = false
|
|
||||||
if.all = [
|
if.all = [
|
||||||
{ value = "%cpu-usage", is-less-than = 0.2 },
|
{ value = "%cpu-usage", is-less-than = 0.2 },
|
||||||
{ value = "$cpu-idle-seconds", is-more-than = 60.0 },
|
{ value = "$cpu-idle-seconds", is-more-than = 60.0 },
|
||||||
]
|
]
|
||||||
priority = 50
|
cpu.governor = "powersave"
|
||||||
|
cpu.energy-performance-preference = "power"
|
||||||
|
cpu.turbo = false
|
||||||
|
|
||||||
# Extended idle power optimization.
|
# Extended idle power optimization.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
|
priority = 40
|
||||||
|
if = { value = "$cpu-idle-seconds", is-more-than = 300.0 }
|
||||||
|
cpu.governor = "powersave"
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 1600
|
cpu.frequency-mhz-maximum = 1600
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.turbo = false
|
cpu.turbo = false
|
||||||
if = { value = "$cpu-idle-seconds", is-more-than = 300.0 }
|
|
||||||
priority = 40
|
|
||||||
|
|
||||||
# Battery conservation when discharging.
|
# Battery conservation when discharging.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
|
priority = 30
|
||||||
|
if.all = [
|
||||||
|
"?discharging",
|
||||||
|
{ value = "%power-supply-charge", is-less-than = 0.5 },
|
||||||
|
]
|
||||||
|
cpu.governor = "powersave"
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 2000
|
cpu.frequency-mhz-maximum = 2000
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.turbo = false
|
cpu.turbo = false
|
||||||
if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.5 } ]
|
|
||||||
power.platform-profile = "low-power"
|
power.platform-profile = "low-power"
|
||||||
priority = 30
|
|
||||||
|
|
||||||
# General battery mode.
|
# General battery mode.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
cpu.energy-performance-bias = "balance_power"
|
priority = 20
|
||||||
|
if = "?discharging"
|
||||||
|
cpu.governor = "powersave"
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
|
cpu.energy-performance-bias = "balance_power"
|
||||||
cpu.frequency-mhz-maximum = 1800
|
cpu.frequency-mhz-maximum = 1800
|
||||||
cpu.frequency-mhz-minimum = 200
|
cpu.frequency-mhz-minimum = 200
|
||||||
cpu.governor = "powersave"
|
|
||||||
cpu.turbo = false
|
cpu.turbo = false
|
||||||
if = "?discharging"
|
|
||||||
priority = 20
|
|
||||||
|
|
||||||
# Balanced performance for general use. Default fallback rule.
|
# Balanced performance for general use. Default fallback rule.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
cpu.energy-performance-preference = "balance_performance"
|
|
||||||
cpu.governor = "schedutil"
|
|
||||||
priority = 0
|
priority = 0
|
||||||
|
cpu.governor = "schedutil"
|
||||||
|
cpu.energy-performance-preference = "balance_performance"
|
||||||
|
|
130
src/config.rs
130
src/config.rs
|
@ -1,33 +1,18 @@
|
||||||
use std::{
|
use std::{fs, path::Path};
|
||||||
fs,
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{
|
use anyhow::{Context, bail};
|
||||||
Context,
|
use serde::{Deserialize, Serialize};
|
||||||
bail,
|
|
||||||
};
|
|
||||||
use serde::{
|
|
||||||
Deserialize,
|
|
||||||
Serialize,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{cpu, power_supply};
|
||||||
cpu,
|
|
||||||
power_supply,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
||||||
*value == T::default()
|
*value == T::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)]
|
||||||
Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq,
|
|
||||||
)]
|
|
||||||
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
|
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
|
||||||
pub struct CpuDelta {
|
pub struct CpuDelta {
|
||||||
/// The CPUs to apply the changes to. When unspecified, will be applied to
|
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs.
|
||||||
/// all CPUs.
|
|
||||||
#[arg(short = 'c', long = "for")]
|
#[arg(short = 'c', long = "for")]
|
||||||
#[serde(rename = "for", skip_serializing_if = "is_default")]
|
#[serde(rename = "for", skip_serializing_if = "is_default")]
|
||||||
pub for_: Option<Vec<u32>>,
|
pub for_: Option<Vec<u32>>,
|
||||||
|
@ -35,20 +20,17 @@ pub struct CpuDelta {
|
||||||
/// Set the CPU governor.
|
/// Set the CPU governor.
|
||||||
#[arg(short = 'g', long)]
|
#[arg(short = 'g', long)]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub governor: Option<String>, /* TODO: Validate with clap for available
|
pub governor: Option<String>, // TODO: Validate with clap for available governors.
|
||||||
* governors. */
|
|
||||||
|
|
||||||
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
|
/// Set CPU Energy Performance Preference (EPP). Short form: --epp.
|
||||||
#[arg(short = 'p', long, alias = "epp")]
|
#[arg(short = 'p', long, alias = "epp")]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub energy_performance_preference: Option<String>, /* TODO: Validate with
|
pub energy_performance_preference: Option<String>, // TODO: Validate with clap for available governors.
|
||||||
* clap for available
|
|
||||||
* governors. */
|
|
||||||
|
|
||||||
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
|
/// Set CPU Energy Performance Bias (EPB). Short form: --epb.
|
||||||
#[arg(short = 'b', long, alias = "epb")]
|
#[arg(short = 'b', long, alias = "epb")]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub energy_performance_bias: Option<String>, /* TODO: Validate with clap for available governors. */
|
pub energy_performance_bias: Option<String>, // TODO: Validate with clap for available governors.
|
||||||
|
|
||||||
/// Set minimum CPU frequency in MHz. Short form: --freq-min.
|
/// 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))]
|
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
|
||||||
|
@ -78,11 +60,8 @@ impl CpuDelta {
|
||||||
}
|
}
|
||||||
|
|
||||||
cpus
|
cpus
|
||||||
},
|
}
|
||||||
None => {
|
None => cpu::Cpu::all().context("failed to get all CPUs and their information")?,
|
||||||
cpu::Cpu::all()
|
|
||||||
.context("failed to get all CPUs and their information")?
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for cpu in &mut cpus {
|
for cpu in &mut cpus {
|
||||||
|
@ -115,19 +94,15 @@ impl CpuDelta {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)]
|
||||||
Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq,
|
|
||||||
)]
|
|
||||||
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
|
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
|
||||||
pub struct PowerDelta {
|
pub struct PowerDelta {
|
||||||
/// The power supplies to apply the changes to. When unspecified, will be
|
/// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies.
|
||||||
/// applied to all power supplies.
|
|
||||||
#[arg(short = 'p', long = "for")]
|
#[arg(short = 'p', long = "for")]
|
||||||
#[serde(rename = "for", skip_serializing_if = "is_default")]
|
#[serde(rename = "for", skip_serializing_if = "is_default")]
|
||||||
pub for_: Option<Vec<String>>,
|
pub for_: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Set the percentage that the power supply has to drop under for charging
|
/// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start.
|
||||||
/// to start. Short form: --charge-start.
|
|
||||||
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
|
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||||
#[serde(skip_serializing_if = "is_default")]
|
#[serde(skip_serializing_if = "is_default")]
|
||||||
pub charge_threshold_start: Option<u8>,
|
pub charge_threshold_start: Option<u8>,
|
||||||
|
@ -150,25 +125,21 @@ impl PowerDelta {
|
||||||
let mut power_supplies = Vec::with_capacity(names.len());
|
let mut power_supplies = Vec::with_capacity(names.len());
|
||||||
|
|
||||||
for name in names {
|
for name in names {
|
||||||
power_supplies
|
power_supplies.push(power_supply::PowerSupply::from_name(name.clone())?);
|
||||||
.push(power_supply::PowerSupply::from_name(name.clone())?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
power_supplies
|
power_supplies
|
||||||
},
|
}
|
||||||
|
|
||||||
None => {
|
None => power_supply::PowerSupply::all()?
|
||||||
power_supply::PowerSupply::all()?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|power_supply| power_supply.threshold_config.is_some())
|
.filter(|power_supply| power_supply.threshold_config.is_some())
|
||||||
.collect()
|
.collect(),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for power_supply in &mut power_supplies {
|
for power_supply in &mut power_supplies {
|
||||||
if let Some(threshold_start) = self.charge_threshold_start {
|
if let Some(threshold_start) = self.charge_threshold_start {
|
||||||
power_supply
|
power_supply.set_charge_threshold_start(threshold_start as f64 / 100.0)?;
|
||||||
.set_charge_threshold_start(threshold_start as f64 / 100.0)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(threshold_end) = self.charge_threshold_end {
|
if let Some(threshold_end) = self.charge_threshold_end {
|
||||||
|
@ -187,9 +158,7 @@ impl PowerDelta {
|
||||||
macro_rules! named {
|
macro_rules! named {
|
||||||
($variant:ident => $value:literal) => {
|
($variant:ident => $value:literal) => {
|
||||||
pub mod $variant {
|
pub mod $variant {
|
||||||
pub fn serialize<S: serde::Serializer>(
|
pub fn serialize<S: serde::Serializer>(serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error> {
|
|
||||||
serializer.serialize_str($value)
|
serializer.serialize_str($value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,22 +170,13 @@ macro_rules! named {
|
||||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||||
type Value = ();
|
type Value = ();
|
||||||
|
|
||||||
fn expecting(
|
fn expecting(&self, writer: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
&self,
|
|
||||||
writer: &mut std::fmt::Formatter,
|
|
||||||
) -> std::fmt::Result {
|
|
||||||
writer.write_str(concat!("\"", $value, "\""))
|
writer.write_str(concat!("\"", $value, "\""))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_str<E: serde::de::Error>(
|
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||||
self,
|
|
||||||
value: &str,
|
|
||||||
) -> Result<Self::Value, E> {
|
|
||||||
if value != $value {
|
if value != $value {
|
||||||
return Err(E::invalid_value(
|
return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self));
|
||||||
serde::de::Unexpected::Str(value),
|
|
||||||
&self,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -414,22 +374,18 @@ impl Expression {
|
||||||
// We also want to be strict, instead of lazy in binary operations, because
|
// We also want to be strict, instead of lazy in binary operations, because
|
||||||
// we want to catch type errors immediately.
|
// we want to catch type errors immediately.
|
||||||
//
|
//
|
||||||
// FIXME: We currently cannot catch errors that will happen when propagating
|
// FIXME: We currently cannot catch errors that will happen when propagating None.
|
||||||
// None. You can have a type error go uncaught on first startup by using
|
// You can have a type error go uncaught on first startup by using $cpu-usage-volatility
|
||||||
// $cpu-usage-volatility incorrectly, for example.
|
// incorrectly, for example.
|
||||||
Ok(Some(match self {
|
Ok(Some(match self {
|
||||||
CpuUsage => Number(state.cpu_usage),
|
CpuUsage => Number(state.cpu_usage),
|
||||||
CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)),
|
CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)),
|
||||||
CpuTemperature => Number(state.cpu_temperature),
|
CpuTemperature => Number(state.cpu_temperature),
|
||||||
CpuTemperatureVolatility => {
|
CpuTemperatureVolatility => Number(try_ok!(state.cpu_temperature_volatility)),
|
||||||
Number(try_ok!(state.cpu_temperature_volatility))
|
|
||||||
},
|
|
||||||
CpuIdleSeconds => Number(state.cpu_idle_seconds),
|
CpuIdleSeconds => Number(state.cpu_idle_seconds),
|
||||||
|
|
||||||
PowerSupplyCharge => Number(state.cpu_idle_seconds),
|
PowerSupplyCharge => Number(state.cpu_idle_seconds),
|
||||||
PowerSupplyDischargeRate => {
|
PowerSupplyDischargeRate => Number(try_ok!(state.power_supply_discharge_rate)),
|
||||||
Number(try_ok!(state.power_supply_discharge_rate))
|
|
||||||
},
|
|
||||||
|
|
||||||
Discharging => Boolean(state.discharging),
|
Discharging => Boolean(state.discharging),
|
||||||
|
|
||||||
|
@ -437,20 +393,12 @@ impl Expression {
|
||||||
|
|
||||||
Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?),
|
Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?),
|
||||||
Minus { a, b } => Number(eval!(a).as_number()? - eval!(b).as_number()?),
|
Minus { a, b } => Number(eval!(a).as_number()? - eval!(b).as_number()?),
|
||||||
Multiply { a, b } => {
|
Multiply { a, b } => Number(eval!(a).as_number()? * eval!(b).as_number()?),
|
||||||
Number(eval!(a).as_number()? * eval!(b).as_number()?)
|
Power { a, b } => Number(eval!(a).as_number()?.powf(eval!(b).as_number()?)),
|
||||||
},
|
|
||||||
Power { a, b } => {
|
|
||||||
Number(eval!(a).as_number()?.powf(eval!(b).as_number()?))
|
|
||||||
},
|
|
||||||
Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?),
|
Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?),
|
||||||
|
|
||||||
LessThan { a, b } => {
|
LessThan { a, b } => Boolean(eval!(a).as_number()? < eval!(b).as_number()?),
|
||||||
Boolean(eval!(a).as_number()? < eval!(b).as_number()?)
|
MoreThan { a, b } => Boolean(eval!(a).as_number()? > eval!(b).as_number()?),
|
||||||
},
|
|
||||||
MoreThan { a, b } => {
|
|
||||||
Boolean(eval!(a).as_number()? > eval!(b).as_number()?)
|
|
||||||
},
|
|
||||||
Equal { a, b, leeway } => {
|
Equal { a, b, leeway } => {
|
||||||
let a = eval!(a).as_number()?;
|
let a = eval!(a).as_number()?;
|
||||||
let b = eval!(b).as_number()?;
|
let b = eval!(b).as_number()?;
|
||||||
|
@ -460,14 +408,14 @@ impl Expression {
|
||||||
let maximum = a + leeway;
|
let maximum = a + leeway;
|
||||||
|
|
||||||
Boolean(minimum < b && b < maximum)
|
Boolean(minimum < b && b < maximum)
|
||||||
},
|
}
|
||||||
|
|
||||||
And { a, b } => {
|
And { a, b } => {
|
||||||
let a = eval!(a).as_boolean()?;
|
let a = eval!(a).as_boolean()?;
|
||||||
let b = eval!(b).as_boolean()?;
|
let b = eval!(b).as_boolean()?;
|
||||||
|
|
||||||
Boolean(a && b)
|
Boolean(a && b)
|
||||||
},
|
}
|
||||||
All { all } => {
|
All { all } => {
|
||||||
let mut result = true;
|
let mut result = true;
|
||||||
|
|
||||||
|
@ -478,13 +426,13 @@ impl Expression {
|
||||||
}
|
}
|
||||||
|
|
||||||
Boolean(result)
|
Boolean(result)
|
||||||
},
|
}
|
||||||
Or { a, b } => {
|
Or { a, b } => {
|
||||||
let a = eval!(a).as_boolean()?;
|
let a = eval!(a).as_boolean()?;
|
||||||
let b = eval!(b).as_boolean()?;
|
let b = eval!(b).as_boolean()?;
|
||||||
|
|
||||||
Boolean(a || b)
|
Boolean(a || b)
|
||||||
},
|
}
|
||||||
Any { any } => {
|
Any { any } => {
|
||||||
let mut result = false;
|
let mut result = false;
|
||||||
|
|
||||||
|
@ -495,7 +443,7 @@ impl Expression {
|
||||||
}
|
}
|
||||||
|
|
||||||
Boolean(result)
|
Boolean(result)
|
||||||
},
|
}
|
||||||
Not { not } => Boolean(!eval!(not).as_boolean()?),
|
Not { not } => Boolean(!eval!(not).as_boolean()?),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -563,9 +511,7 @@ impl DaemonConfig {
|
||||||
// This is just for debug traces.
|
// This is just for debug traces.
|
||||||
if log::max_level() >= log::LevelFilter::Debug {
|
if log::max_level() >= log::LevelFilter::Debug {
|
||||||
if config.rules.is_sorted_by_key(|rule| rule.priority) {
|
if config.rules.is_sorted_by_key(|rule| rule.priority) {
|
||||||
log::debug!(
|
log::debug!("config rules are sorted by increasing priority, not doing anything");
|
||||||
"config rules are sorted by increasing priority, not doing anything"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
log::debug!("config rules aren't sorted by priority, sorting");
|
log::debug!("config rules aren't sorted by priority, sorting");
|
||||||
}
|
}
|
||||||
|
|
120
src/cpu.rs
120
src/cpu.rs
|
@ -1,18 +1,8 @@
|
||||||
use std::{
|
use anyhow::{Context, bail};
|
||||||
cell::OnceCell,
|
|
||||||
collections::HashMap,
|
|
||||||
fmt,
|
|
||||||
mem,
|
|
||||||
rc::Rc,
|
|
||||||
string::ToString,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{
|
|
||||||
Context,
|
|
||||||
bail,
|
|
||||||
};
|
|
||||||
use yansi::Paint as _;
|
use yansi::Paint as _;
|
||||||
|
|
||||||
|
use std::{cell::OnceCell, collections::HashMap, fmt, mem, rc::Rc, string::ToString};
|
||||||
|
|
||||||
use crate::fs;
|
use crate::fs;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq)]
|
#[derive(Default, Debug, Clone, PartialEq)]
|
||||||
|
@ -133,11 +123,10 @@ impl Cpu {
|
||||||
let cache = CpuRescanCache::default();
|
let cache = CpuRescanCache::default();
|
||||||
|
|
||||||
for entry in fs::read_dir(PATH)
|
for entry in fs::read_dir(PATH)
|
||||||
.context("failed to read CPU entries")?
|
.with_context(|| format!("failed to read CPU entries from '{PATH}'"))?
|
||||||
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
|
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
|
||||||
{
|
{
|
||||||
let entry =
|
let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
|
||||||
entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
|
|
||||||
|
|
||||||
let entry_file_name = entry.file_name();
|
let entry_file_name = entry.file_name();
|
||||||
|
|
||||||
|
@ -175,8 +164,7 @@ impl Cpu {
|
||||||
bail!("{self} does not exist");
|
bail!("{self} does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.has_cpufreq =
|
self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
|
||||||
fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
|
|
||||||
|
|
||||||
if self.has_cpufreq {
|
if self.has_cpufreq {
|
||||||
self.rescan_governor()?;
|
self.rescan_governor()?;
|
||||||
|
@ -196,8 +184,7 @@ impl Cpu {
|
||||||
|
|
||||||
self.available_governors = 'available_governors: {
|
self.available_governors = 'available_governors: {
|
||||||
let Some(content) = fs::read(format!(
|
let Some(content) = fs::read(format!(
|
||||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors"
|
||||||
scaling_available_governors"
|
|
||||||
))
|
))
|
||||||
.with_context(|| format!("failed to read {self} available governors"))?
|
.with_context(|| format!("failed to read {self} available governors"))?
|
||||||
else {
|
else {
|
||||||
|
@ -252,11 +239,8 @@ impl Cpu {
|
||||||
|
|
||||||
self.available_epps = 'available_epps: {
|
self.available_epps = 'available_epps: {
|
||||||
let Some(content) = fs::read(format!(
|
let Some(content) = fs::read(format!(
|
||||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences"
|
||||||
energy_performance_available_preferences"
|
)).with_context(|| format!("failed to read {self} available EPPs"))? else {
|
||||||
))
|
|
||||||
.with_context(|| format!("failed to read {self} available EPPs"))?
|
|
||||||
else {
|
|
||||||
break 'available_epps Vec::new();
|
break 'available_epps Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -268,8 +252,7 @@ impl Cpu {
|
||||||
|
|
||||||
self.epp = Some(
|
self.epp = Some(
|
||||||
fs::read(format!(
|
fs::read(format!(
|
||||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"
|
||||||
energy_performance_preference"
|
|
||||||
))
|
))
|
||||||
.with_context(|| format!("failed to read {self} EPP"))?
|
.with_context(|| format!("failed to read {self} EPP"))?
|
||||||
.with_context(|| format!("failed to find {self} EPP"))?,
|
.with_context(|| format!("failed to find {self} EPP"))?,
|
||||||
|
@ -355,7 +338,7 @@ impl Cpu {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
cache.stat.get().unwrap()
|
cache.stat.get().unwrap()
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.stat = stat
|
self.stat = stat
|
||||||
|
@ -409,7 +392,7 @@ impl Cpu {
|
||||||
|
|
||||||
cache.info.set(info).unwrap();
|
cache.info.set(info).unwrap();
|
||||||
cache.info.get().unwrap()
|
cache.info.get().unwrap()
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.info = info.get(&self.number).cloned();
|
self.info = info.get(&self.number).cloned();
|
||||||
|
@ -429,8 +412,7 @@ impl Cpu {
|
||||||
.any(|avail_governor| avail_governor == governor)
|
.any(|avail_governor| avail_governor == governor)
|
||||||
{
|
{
|
||||||
bail!(
|
bail!(
|
||||||
"governor '{governor}' is not available for {self}. available \
|
"governor '{governor}' is not available for {self}. available governors: {governors}",
|
||||||
governors: {governors}",
|
|
||||||
governors = governors.join(", "),
|
governors = governors.join(", "),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -441,8 +423,7 @@ impl Cpu {
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"this probably means that {self} doesn't exist or doesn't support \
|
"this probably means that {self} doesn't exist or doesn't support changing governors"
|
||||||
changing governors"
|
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -460,24 +441,17 @@ impl Cpu {
|
||||||
|
|
||||||
if !epps.iter().any(|avail_epp| avail_epp == epp) {
|
if !epps.iter().any(|avail_epp| avail_epp == epp) {
|
||||||
bail!(
|
bail!(
|
||||||
"EPP value '{epp}' is not available for {self}. available EPP values: \
|
"EPP value '{epp}' is not available for {self}. available EPP values: {epps}",
|
||||||
{epps}",
|
|
||||||
epps = epps.join(", "),
|
epps = epps.join(", "),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::write(
|
fs::write(
|
||||||
format!(
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"),
|
||||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
|
|
||||||
energy_performance_preference"
|
|
||||||
),
|
|
||||||
epp,
|
epp,
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!("this probably means that {self} doesn't exist or doesn't support changing EPP")
|
||||||
"this probably means that {self} doesn't exist or doesn't support \
|
|
||||||
changing EPP"
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.epp = Some(epp.to_owned());
|
self.epp = Some(epp.to_owned());
|
||||||
|
@ -494,23 +468,17 @@ impl Cpu {
|
||||||
|
|
||||||
if !epbs.iter().any(|avail_epb| avail_epb == epb) {
|
if !epbs.iter().any(|avail_epb| avail_epb == epb) {
|
||||||
bail!(
|
bail!(
|
||||||
"EPB value '{epb}' is not available for {self}. available EPB values: \
|
"EPB value '{epb}' is not available for {self}. available EPB values: {valid}",
|
||||||
{valid}",
|
|
||||||
valid = epbs.join(", "),
|
valid = epbs.join(", "),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::write(
|
fs::write(
|
||||||
format!(
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"),
|
||||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"
|
|
||||||
),
|
|
||||||
epb,
|
epb,
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!("this probably means that {self} doesn't exist or doesn't support changing EPB")
|
||||||
"this probably means that {self} doesn't exist or doesn't support \
|
|
||||||
changing EPB"
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.epb = Some(epb.to_owned());
|
self.epb = Some(epb.to_owned());
|
||||||
|
@ -518,10 +486,7 @@ impl Cpu {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_frequency_mhz_minimum(
|
pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
&mut self,
|
|
||||||
frequency_mhz: u64,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let Self { number, .. } = *self;
|
let Self { number, .. } = *self;
|
||||||
|
|
||||||
self.validate_frequency_mhz_minimum(frequency_mhz)?;
|
self.validate_frequency_mhz_minimum(frequency_mhz)?;
|
||||||
|
@ -535,10 +500,7 @@ impl Cpu {
|
||||||
&frequency_khz,
|
&frequency_khz,
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency")
|
||||||
"this probably means that {self} doesn't exist or doesn't support \
|
|
||||||
changing minimum frequency"
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.frequency_mhz_minimum = Some(frequency_mhz);
|
self.frequency_mhz_minimum = Some(frequency_mhz);
|
||||||
|
@ -546,10 +508,7 @@ impl Cpu {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_frequency_mhz_minimum(
|
fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
&self,
|
|
||||||
new_frequency_mhz: u64,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let Self { number, .. } = self;
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
let Some(minimum_frequency_khz) = fs::read_n::<u64>(format!(
|
let Some(minimum_frequency_khz) = fs::read_n::<u64>(format!(
|
||||||
|
@ -563,8 +522,7 @@ impl Cpu {
|
||||||
|
|
||||||
if new_frequency_mhz * 1000 < minimum_frequency_khz {
|
if new_frequency_mhz * 1000 < minimum_frequency_khz {
|
||||||
bail!(
|
bail!(
|
||||||
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than \
|
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for {self}",
|
||||||
the minimum frequency ({} MHz) for {self}",
|
|
||||||
minimum_frequency_khz / 1000,
|
minimum_frequency_khz / 1000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -572,10 +530,7 @@ impl Cpu {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_frequency_mhz_maximum(
|
pub fn set_frequency_mhz_maximum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
&mut self,
|
|
||||||
frequency_mhz: u64,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let Self { number, .. } = *self;
|
let Self { number, .. } = *self;
|
||||||
|
|
||||||
self.validate_frequency_mhz_maximum(frequency_mhz)?;
|
self.validate_frequency_mhz_maximum(frequency_mhz)?;
|
||||||
|
@ -589,10 +544,7 @@ impl Cpu {
|
||||||
&frequency_khz,
|
&frequency_khz,
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency")
|
||||||
"this probably means that {self} doesn't exist or doesn't support \
|
|
||||||
changing maximum frequency"
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.frequency_mhz_maximum = Some(frequency_mhz);
|
self.frequency_mhz_maximum = Some(frequency_mhz);
|
||||||
|
@ -600,10 +552,7 @@ impl Cpu {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_frequency_mhz_maximum(
|
fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
&self,
|
|
||||||
new_frequency_mhz: u64,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let Self { number, .. } = self;
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
let Some(maximum_frequency_khz) = fs::read_n::<u64>(format!(
|
let Some(maximum_frequency_khz) = fs::read_n::<u64>(format!(
|
||||||
|
@ -617,8 +566,7 @@ impl Cpu {
|
||||||
|
|
||||||
if new_frequency_mhz * 1000 > maximum_frequency_khz {
|
if new_frequency_mhz * 1000 > maximum_frequency_khz {
|
||||||
bail!(
|
bail!(
|
||||||
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher \
|
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for {self}",
|
||||||
than the maximum frequency ({} MHz) for {self}",
|
|
||||||
maximum_frequency_khz / 1000,
|
maximum_frequency_khz / 1000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -639,12 +587,10 @@ impl Cpu {
|
||||||
|
|
||||||
// AMD specific paths
|
// AMD specific paths
|
||||||
let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost";
|
let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost";
|
||||||
let msr_boost_path =
|
let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost";
|
||||||
"/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost";
|
|
||||||
|
|
||||||
// Path priority (from most to least specific)
|
// Path priority (from most to least specific)
|
||||||
let intel_boost_path_negated =
|
let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo";
|
||||||
"/sys/devices/system/cpu/intel_pstate/no_turbo";
|
|
||||||
let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost";
|
let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost";
|
||||||
|
|
||||||
// Try each boost control path in order of specificity
|
// Try each boost control path in order of specificity
|
||||||
|
@ -678,15 +624,13 @@ impl Cpu {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn turbo() -> anyhow::Result<Option<bool>> {
|
pub fn turbo() -> anyhow::Result<Option<bool>> {
|
||||||
if let Some(content) =
|
if let Some(content) = fs::read_n::<u64>("/sys/devices/system/cpu/intel_pstate/no_turbo")
|
||||||
fs::read_n::<u64>("/sys/devices/system/cpu/intel_pstate/no_turbo")
|
|
||||||
.context("failed to read CPU turbo boost status")?
|
.context("failed to read CPU turbo boost status")?
|
||||||
{
|
{
|
||||||
return Ok(Some(content == 0));
|
return Ok(Some(content == 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(content) =
|
if let Some(content) = fs::read_n::<u64>("/sys/devices/system/cpu/cpufreq/boost")
|
||||||
fs::read_n::<u64>("/sys/devices/system/cpu/cpufreq/boost")
|
|
||||||
.context("failed to read CPU turbo boost status")?
|
.context("failed to read CPU turbo boost status")?
|
||||||
{
|
{
|
||||||
return Ok(Some(content == 1));
|
return Ok(Some(content == 1));
|
||||||
|
|
|
@ -1,29 +1,17 @@
|
||||||
use std::{
|
use std::{
|
||||||
cell::LazyCell,
|
cell::LazyCell,
|
||||||
collections::{
|
collections::{HashMap, VecDeque},
|
||||||
HashMap,
|
|
||||||
VecDeque,
|
|
||||||
},
|
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{
|
atomic::{AtomicBool, Ordering},
|
||||||
AtomicBool,
|
|
||||||
Ordering,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
time::{
|
time::{Duration, Instant},
|
||||||
Duration,
|
|
||||||
Instant,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{config, system};
|
||||||
config,
|
|
||||||
system,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Calculate the idle time multiplier based on system idle time.
|
/// Calculate the idle time multiplier based on system idle time.
|
||||||
///
|
///
|
||||||
|
@ -41,7 +29,7 @@ fn idle_multiplier(idle_for: Duration) -> f64 {
|
||||||
false => {
|
false => {
|
||||||
let idle_minutes = idle_for.as_secs() as f64 / 60.0;
|
let idle_minutes = idle_for.as_secs() as f64 / 60.0;
|
||||||
idle_minutes.log2()
|
idle_minutes.log2()
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clamp the multiplier to avoid excessive delays.
|
// Clamp the multiplier to avoid excessive delays.
|
||||||
|
@ -166,8 +154,7 @@ impl Daemon {
|
||||||
let mut temperature_change_sum = 0.0;
|
let mut temperature_change_sum = 0.0;
|
||||||
|
|
||||||
for index in 0..change_count {
|
for index in 0..change_count {
|
||||||
let usage_change =
|
let usage_change = self.cpu_log[index + 1].usage - self.cpu_log[index].usage;
|
||||||
self.cpu_log[index + 1].usage - self.cpu_log[index].usage;
|
|
||||||
usage_change_sum += usage_change.abs();
|
usage_change_sum += usage_change.abs();
|
||||||
|
|
||||||
let temperature_change =
|
let temperature_change =
|
||||||
|
@ -219,9 +206,10 @@ struct PowerSupplyLog {
|
||||||
|
|
||||||
impl Daemon {
|
impl Daemon {
|
||||||
fn discharging(&self) -> bool {
|
fn discharging(&self) -> bool {
|
||||||
self.system.power_supplies.iter().any(|power_supply| {
|
self.system
|
||||||
power_supply.charge_state.as_deref() == Some("Discharging")
|
.power_supplies
|
||||||
})
|
.iter()
|
||||||
|
.any(|power_supply| power_supply.charge_state.as_deref() == Some("Discharging"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the discharge rate, returns a number between 0 and 1.
|
/// Calculates the discharge rate, returns a number between 0 and 1.
|
||||||
|
@ -283,13 +271,13 @@ impl Daemon {
|
||||||
delay /= 2;
|
delay /= 2;
|
||||||
delay *= 3;
|
delay *= 3;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
// If we can't determine the discharge rate, that means that
|
// If we can't determine the discharge rate, that means that
|
||||||
// we were very recently started. Which is user activity.
|
// we were very recently started. Which is user activity.
|
||||||
None => {
|
None => {
|
||||||
delay *= 2;
|
delay *= 2;
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,8 +288,7 @@ impl Daemon {
|
||||||
let factor = idle_multiplier(idle_for);
|
let factor = idle_multiplier(idle_for);
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"system has been idle for {seconds} seconds (approx {minutes} \
|
"system has been idle for {seconds} seconds (approx {minutes} minutes), applying idle factor: {factor:.2}x",
|
||||||
minutes), applying idle factor: {factor:.2}x",
|
|
||||||
seconds = idle_for.as_secs(),
|
seconds = idle_for.as_secs(),
|
||||||
minutes = idle_for.as_secs() / 60,
|
minutes = idle_for.as_secs() / 60,
|
||||||
);
|
);
|
||||||
|
@ -317,12 +304,10 @@ impl Daemon {
|
||||||
}
|
}
|
||||||
|
|
||||||
let delay = match self.last_polling_delay {
|
let delay = match self.last_polling_delay {
|
||||||
Some(last_delay) => {
|
Some(last_delay) => Duration::from_secs_f64(
|
||||||
Duration::from_secs_f64(
|
|
||||||
// 30% of current computed delay, 70% of last delay.
|
// 30% of current computed delay, 70% of last delay.
|
||||||
delay.as_secs_f64() * 0.3 + last_delay.as_secs_f64() * 0.7,
|
delay.as_secs_f64() * 0.3 + last_delay.as_secs_f64() * 0.7,
|
||||||
)
|
),
|
||||||
},
|
|
||||||
|
|
||||||
None => delay,
|
None => delay,
|
||||||
};
|
};
|
||||||
|
@ -366,8 +351,7 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
|
||||||
|
|
||||||
let delay = daemon.polling_delay();
|
let delay = daemon.polling_delay();
|
||||||
log::info!(
|
log::info!(
|
||||||
"next poll will be in {seconds} seconds or {minutes} minutes, possibly \
|
"next poll will be in {seconds} seconds or {minutes} minutes, possibly delayed if application of rules takes more than the polling delay",
|
||||||
delayed if application of rules takes more than the polling delay",
|
|
||||||
seconds = delay.as_secs_f64(),
|
seconds = delay.as_secs_f64(),
|
||||||
minutes = delay.as_secs_f64() / 60.0,
|
minutes = delay.as_secs_f64() / 60.0,
|
||||||
);
|
);
|
||||||
|
@ -380,25 +364,15 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
|
||||||
cpu_usage: daemon.cpu_log.back().unwrap().usage,
|
cpu_usage: daemon.cpu_log.back().unwrap().usage,
|
||||||
cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage),
|
cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage),
|
||||||
cpu_temperature: daemon.cpu_log.back().unwrap().temperature,
|
cpu_temperature: daemon.cpu_log.back().unwrap().temperature,
|
||||||
cpu_temperature_volatility: daemon
|
cpu_temperature_volatility: daemon.cpu_volatility().map(|vol| vol.temperature),
|
||||||
.cpu_volatility()
|
cpu_idle_seconds: daemon.last_user_activity.elapsed().as_secs_f64(),
|
||||||
.map(|vol| vol.temperature),
|
power_supply_charge: daemon.power_supply_log.back().unwrap().charge,
|
||||||
cpu_idle_seconds: daemon
|
|
||||||
.last_user_activity
|
|
||||||
.elapsed()
|
|
||||||
.as_secs_f64(),
|
|
||||||
power_supply_charge: daemon
|
|
||||||
.power_supply_log
|
|
||||||
.back()
|
|
||||||
.unwrap()
|
|
||||||
.charge,
|
|
||||||
power_supply_discharge_rate: daemon.power_supply_discharge_rate(),
|
power_supply_discharge_rate: daemon.power_supply_discharge_rate(),
|
||||||
discharging: daemon.discharging(),
|
discharging: daemon.discharging(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cpu_delta_for = HashMap::<u32, config::CpuDelta>::new();
|
let mut cpu_delta_for = HashMap::<u32, config::CpuDelta>::new();
|
||||||
let all_cpus =
|
let all_cpus = LazyCell::new(|| (0..num_cpus::get() as u32).collect::<Vec<_>>());
|
||||||
LazyCell::new(|| (0..num_cpus::get() as u32).collect::<Vec<_>>());
|
|
||||||
|
|
||||||
for rule in &config.rules {
|
for rule in &config.rules {
|
||||||
let Some(condition) = rule.condition.eval(&state)? else {
|
let Some(condition) = rule.condition.eval(&state)? else {
|
||||||
|
|
465
src/engine.rs
Normal file
465
src/engine.rs
Normal file
|
@ -0,0 +1,465 @@
|
||||||
|
use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings};
|
||||||
|
use crate::core::{OperationalMode, SystemReport};
|
||||||
|
use crate::cpu::{self};
|
||||||
|
use crate::power_supply;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
/// Track turbo boost state for AC and battery power modes
|
||||||
|
struct TurboHysteresisStates {
|
||||||
|
/// State for when on AC power
|
||||||
|
charger: TurboHysteresis,
|
||||||
|
/// State for when on battery power
|
||||||
|
battery: TurboHysteresis,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TurboHysteresisStates {
|
||||||
|
const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
charger: TurboHysteresis::new(),
|
||||||
|
battery: TurboHysteresis::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn get_for_power_state(&self, is_on_ac: bool) -> &TurboHysteresis {
|
||||||
|
if is_on_ac {
|
||||||
|
&self.charger
|
||||||
|
} else {
|
||||||
|
&self.battery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static TURBO_STATES: OnceLock<TurboHysteresisStates> = 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<bool> {
|
||||||
|
if self.initialized.load(Ordering::Acquire) {
|
||||||
|
Some(self.previous_state.load(Ordering::Acquire))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the state with a specific value if not already initialized
|
||||||
|
/// Only one thread should be able to initialize the state
|
||||||
|
fn initialize_with(&self, initial_state: bool) -> bool {
|
||||||
|
// First, try to atomically change initialized from false to true
|
||||||
|
// Only one thread can win the initialization race
|
||||||
|
match self.initialized.compare_exchange(
|
||||||
|
false, // expected: not initialized
|
||||||
|
true, // desired: mark as initialized
|
||||||
|
Ordering::Release, // success: release for memory visibility
|
||||||
|
Ordering::Acquire, // failure: just need to acquire the current value
|
||||||
|
) {
|
||||||
|
Ok(_) => {
|
||||||
|
// We won the race to initialize
|
||||||
|
// Now it's safe to set the initial state since we know we're the only
|
||||||
|
// thread that has successfully marked this as initialized
|
||||||
|
self.previous_state.store(initial_state, Ordering::Release);
|
||||||
|
initial_state
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Another thread already initialized it.
|
||||||
|
// Just read the current state value that was set by the winning thread
|
||||||
|
self.previous_state.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the turbo state for hysteresis
|
||||||
|
fn update_state(&self, new_state: bool) {
|
||||||
|
// First store the new state, then mark as initialized
|
||||||
|
// With this, any thread seeing initialized=true will also see the correct state
|
||||||
|
self.previous_state.store(new_state, Ordering::Release);
|
||||||
|
|
||||||
|
// Already initialized, no need for compare_exchange
|
||||||
|
if self.initialized.load(Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, try to set initialized=true (but only if it was false)
|
||||||
|
self.initialized
|
||||||
|
.compare_exchange(
|
||||||
|
false, // expected: not initialized
|
||||||
|
true, // desired: mark as initialized
|
||||||
|
Ordering::Release, // success: release for memory visibility
|
||||||
|
Ordering::Relaxed, // failure: we don't care about the current value on failure
|
||||||
|
)
|
||||||
|
.ok(); // Ignore the result. If it fails, it means another thread already initialized it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try applying a CPU feature and handle common error cases. Centralizes the where we
|
||||||
|
/// previously did:
|
||||||
|
/// 1. Try to apply a feature setting
|
||||||
|
/// 2. If not supported, log a warning and continue
|
||||||
|
/// 3. If other error, propagate the error
|
||||||
|
fn try_apply_feature<F: FnOnce() -> anyhow::Result<()>, T>(
|
||||||
|
feature_name: &str,
|
||||||
|
value_description: &str,
|
||||||
|
apply_fn: F,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
log::info!("Setting {feature_name} to '{value_description}'");
|
||||||
|
|
||||||
|
apply_fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the appropriate CPU profile based on power status or forced mode,
|
||||||
|
/// and applies the settings (via helpers defined in the `cpu` module)
|
||||||
|
pub fn determine_and_apply_settings(
|
||||||
|
report: &SystemReport,
|
||||||
|
config: &AppConfig,
|
||||||
|
force_mode: Option<OperationalMode>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// // First, check if there's a governor override set
|
||||||
|
// if let Some(override_governor) = cpu::get_governor_override() {
|
||||||
|
// log::info!(
|
||||||
|
// "Governor override is active: '{}'. Setting governor.",
|
||||||
|
// override_governor.trim()
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Apply the override governor setting
|
||||||
|
// try_apply_feature("override governor", override_governor.trim(), || {
|
||||||
|
// cpu::set_governor(override_governor.trim(), None)
|
||||||
|
// })?;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Determine AC/Battery status once, early in the function
|
||||||
|
// For desktops (no batteries), we should always use the AC power profile
|
||||||
|
// For laptops, we check if all batteries report connected to AC
|
||||||
|
let on_ac_power = if report.batteries.is_empty() {
|
||||||
|
// No batteries means desktop/server, always on AC
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
// Check if all batteries report AC connected
|
||||||
|
report.batteries.iter().all(|b| b.ac_connected)
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected_profile_config: &ProfileConfig;
|
||||||
|
|
||||||
|
if let Some(mode) = force_mode {
|
||||||
|
match mode {
|
||||||
|
OperationalMode::Powersave => {
|
||||||
|
log::info!("Forced Powersave mode selected. Applying 'battery' profile.");
|
||||||
|
selected_profile_config = &config.battery;
|
||||||
|
}
|
||||||
|
OperationalMode::Performance => {
|
||||||
|
log::info!("Forced Performance mode selected. Applying 'charger' profile.");
|
||||||
|
selected_profile_config = &config.charger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use the previously computed on_ac_power value
|
||||||
|
if on_ac_power {
|
||||||
|
log::info!("On AC power, selecting Charger profile.");
|
||||||
|
selected_profile_config = &config.charger;
|
||||||
|
} else {
|
||||||
|
log::info!("On Battery power, selecting Battery profile.");
|
||||||
|
selected_profile_config = &config.battery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply settings from selected_profile_config
|
||||||
|
if let Some(governor) = &selected_profile_config.governor {
|
||||||
|
log::info!("Setting governor to '{governor}'");
|
||||||
|
for cpu in cpu::Cpu::all()? {
|
||||||
|
// Let set_governor handle the validation
|
||||||
|
if let Err(error) = cpu.set_governor(governor) {
|
||||||
|
// If the governor is not available, log a warning
|
||||||
|
log::warn!("{error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(turbo_setting) = selected_profile_config.turbo {
|
||||||
|
log::info!("Setting turbo to '{turbo_setting:?}'");
|
||||||
|
match turbo_setting {
|
||||||
|
TurboSetting::Auto => {
|
||||||
|
if selected_profile_config.enable_auto_turbo {
|
||||||
|
log::debug!("Managing turbo in auto mode based on system conditions");
|
||||||
|
manage_auto_turbo(report, selected_profile_config, on_ac_power)?;
|
||||||
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"Watt's dynamic turbo management is disabled by configuration. Ensuring system uses its default behavior for automatic turbo control."
|
||||||
|
);
|
||||||
|
// Make sure the system is set to its default automatic turbo mode.
|
||||||
|
// This is important if turbo was previously forced off.
|
||||||
|
try_apply_feature("Turbo boost", "system default (Auto)", || {
|
||||||
|
cpu::set_turbo(TurboSetting::Auto)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
try_apply_feature("Turbo boost", &format!("{turbo_setting:?}"), || {
|
||||||
|
cpu::set_turbo(turbo_setting)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(epp) = &selected_profile_config.epp {
|
||||||
|
try_apply_feature("EPP", epp, || cpu::set_epp(epp, None))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(epb) = &selected_profile_config.epb {
|
||||||
|
try_apply_feature("EPB", epb, || cpu::set_epb(epb, None))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(min_freq) = selected_profile_config.min_freq_mhz {
|
||||||
|
try_apply_feature("min frequency", &format!("{min_freq} MHz"), || {
|
||||||
|
cpu::set_frequency_minimum(min_freq, None)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(max_freq) = selected_profile_config.max_freq_mhz {
|
||||||
|
try_apply_feature("max frequency", &format!("{max_freq} MHz"), || {
|
||||||
|
cpu::set_frequency_maximum(max_freq, None)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(profile) = &selected_profile_config.platform_profile {
|
||||||
|
try_apply_feature("platform profile", profile, || {
|
||||||
|
cpu::set_platform_profile(profile)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set battery charge thresholds if configured
|
||||||
|
if let Some(thresholds) = &selected_profile_config.battery_charge_thresholds {
|
||||||
|
let start_threshold = thresholds.start;
|
||||||
|
let stop_threshold = thresholds.stop;
|
||||||
|
|
||||||
|
if start_threshold < stop_threshold && stop_threshold <= 100 {
|
||||||
|
log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%");
|
||||||
|
match power_supply::set_battery_charge_thresholds(start_threshold, stop_threshold) {
|
||||||
|
Ok(()) => log::debug!("Battery charge thresholds set successfully"),
|
||||||
|
Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("Profile settings applied successfully.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn manage_auto_turbo(
|
||||||
|
report: &SystemReport,
|
||||||
|
config: &ProfileConfig,
|
||||||
|
on_ac_power: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// Get the auto turbo settings from the config
|
||||||
|
let turbo_settings = &config.turbo_auto_settings;
|
||||||
|
|
||||||
|
// Validate the complete configuration to ensure it's usable
|
||||||
|
validate_turbo_auto_settings(turbo_settings)?;
|
||||||
|
|
||||||
|
// Get average CPU temperature and CPU load
|
||||||
|
let cpu_temp = report.cpu_global.average_temperature_celsius;
|
||||||
|
|
||||||
|
// Check if we have CPU usage data available
|
||||||
|
let avg_cpu_usage = if report.cpu_cores.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let sum: f32 = report
|
||||||
|
.cpu_cores
|
||||||
|
.iter()
|
||||||
|
.filter_map(|core| core.usage_percent)
|
||||||
|
.sum();
|
||||||
|
let count = report
|
||||||
|
.cpu_cores
|
||||||
|
.iter()
|
||||||
|
.filter(|core| core.usage_percent.is_some())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
Some(sum / count as f32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the previous state or initialize with the configured initial state
|
||||||
|
let previous_turbo_enabled = {
|
||||||
|
let turbo_states = get_turbo_states();
|
||||||
|
let hysteresis = turbo_states.get_for_power_state(on_ac_power);
|
||||||
|
if let Some(state) = hysteresis.get_previous_state() {
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
// Initialize with the configured initial state and return it
|
||||||
|
hysteresis.initialize_with(turbo_settings.initial_turbo_state)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decision logic for enabling/disabling turbo with hysteresis
|
||||||
|
let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) {
|
||||||
|
// If temperature is too high, disable turbo regardless of load
|
||||||
|
(Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => {
|
||||||
|
log::info!(
|
||||||
|
"Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)",
|
||||||
|
temp,
|
||||||
|
turbo_settings.temp_threshold_high
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If load is high enough, enable turbo (unless temp already caused it to disable)
|
||||||
|
(_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => {
|
||||||
|
log::info!(
|
||||||
|
"Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)",
|
||||||
|
usage,
|
||||||
|
turbo_settings.load_threshold_high
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If load is low, disable turbo
|
||||||
|
(_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => {
|
||||||
|
log::info!(
|
||||||
|
"Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)",
|
||||||
|
usage,
|
||||||
|
turbo_settings.load_threshold_low
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// In intermediate load range, maintain previous state (hysteresis)
|
||||||
|
(_, Some(usage), prev_state)
|
||||||
|
if usage > turbo_settings.load_threshold_low
|
||||||
|
&& usage < turbo_settings.load_threshold_high =>
|
||||||
|
{
|
||||||
|
log::info!(
|
||||||
|
"Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)",
|
||||||
|
if prev_state { "enabled" } else { "disabled" },
|
||||||
|
usage
|
||||||
|
);
|
||||||
|
prev_state
|
||||||
|
}
|
||||||
|
|
||||||
|
// When CPU load data is present but temperature is missing, use the same hysteresis logic
|
||||||
|
(None, Some(usage), prev_state) => {
|
||||||
|
log::info!(
|
||||||
|
"Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)",
|
||||||
|
if prev_state { "enabled" } else { "disabled" },
|
||||||
|
usage
|
||||||
|
);
|
||||||
|
prev_state
|
||||||
|
}
|
||||||
|
|
||||||
|
// When all metrics are missing, maintain the previous state
|
||||||
|
(None, None, prev_state) => {
|
||||||
|
log::info!(
|
||||||
|
"Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics",
|
||||||
|
if prev_state { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
prev_state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other cases with partial metrics, maintain previous state for stability
|
||||||
|
(_, _, prev_state) => {
|
||||||
|
log::info!(
|
||||||
|
"Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics",
|
||||||
|
if prev_state { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
prev_state
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the current state for next time
|
||||||
|
{
|
||||||
|
let turbo_states = get_turbo_states();
|
||||||
|
let hysteresis = turbo_states.get_for_power_state(on_ac_power);
|
||||||
|
hysteresis.update_state(enable_turbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only apply the setting if the state has changed
|
||||||
|
let changed = previous_turbo_enabled != enable_turbo;
|
||||||
|
if changed {
|
||||||
|
let turbo_setting = if enable_turbo {
|
||||||
|
TurboSetting::Always
|
||||||
|
} else {
|
||||||
|
TurboSetting::Never
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Auto Turbo: Applying turbo change from {} to {}",
|
||||||
|
if previous_turbo_enabled {
|
||||||
|
"enabled"
|
||||||
|
} else {
|
||||||
|
"disabled"
|
||||||
|
},
|
||||||
|
if enable_turbo { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
|
||||||
|
match cpu::set_turbo(turbo_setting) {
|
||||||
|
Ok(()) => {
|
||||||
|
log::debug!(
|
||||||
|
"Auto Turbo: Successfully set turbo to {}",
|
||||||
|
if enable_turbo { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(EngineError::ControlError(e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!(
|
||||||
|
"Auto Turbo: Maintaining turbo state ({}) - no change needed",
|
||||||
|
if enable_turbo { "enabled" } else { "disabled" }
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_turbo_auto_settings(settings: &TurboAutoSettings) -> Result<(), EngineError> {
|
||||||
|
if settings.load_threshold_high <= settings.load_threshold_low
|
||||||
|
|| settings.load_threshold_high > 100.0
|
||||||
|
|| settings.load_threshold_high < 0.0
|
||||||
|
|| settings.load_threshold_low < 0.0
|
||||||
|
|| settings.load_threshold_low > 100.0
|
||||||
|
{
|
||||||
|
return Err(EngineError::ConfigurationError(
|
||||||
|
"Invalid turbo auto settings: load thresholds must be between 0 % and 100 % with high > low"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate temperature threshold (realistic range for CPU temps in Celsius)
|
||||||
|
// TODO: different CPUs have different temperature thresholds. While 110 is a good example
|
||||||
|
// "extreme" case, the upper barrier might be *lower* for some devices. We'll want to fix
|
||||||
|
// this eventually, or make it configurable.
|
||||||
|
if settings.temp_threshold_high <= 0.0 || settings.temp_threshold_high > 110.0 {
|
||||||
|
return Err(EngineError::ConfigurationError(
|
||||||
|
"Invalid turbo auto settings: temperature threshold must be between 0°C and 110°C"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
29
src/fs.rs
29
src/fs.rs
|
@ -1,10 +1,4 @@
|
||||||
use std::{
|
use std::{error, fs, io, path::Path, str};
|
||||||
error,
|
|
||||||
fs,
|
|
||||||
io,
|
|
||||||
path::Path,
|
|
||||||
str,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
|
@ -22,12 +16,10 @@ pub fn read_dir(path: impl AsRef<Path>) -> anyhow::Result<Option<fs::ReadDir>> {
|
||||||
|
|
||||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||||
|
|
||||||
Err(error) => {
|
Err(error) => Err(error).context(format!(
|
||||||
Err(error).context(format!(
|
|
||||||
"failed to read directory '{path}'",
|
"failed to read directory '{path}'",
|
||||||
path = path.display()
|
path = path.display()
|
||||||
))
|
)),
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,30 +31,23 @@ pub fn read(path: impl AsRef<Path>) -> anyhow::Result<Option<String>> {
|
||||||
|
|
||||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||||
|
|
||||||
Err(error) => {
|
Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())),
|
||||||
Err(error)
|
|
||||||
.context(format!("failed to read '{path}", path = path.display()))
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_n<N: str::FromStr>(
|
pub fn read_n<N: str::FromStr>(path: impl AsRef<Path>) -> anyhow::Result<Option<N>>
|
||||||
path: impl AsRef<Path>,
|
|
||||||
) -> anyhow::Result<Option<N>>
|
|
||||||
where
|
where
|
||||||
N::Err: error::Error + Send + Sync + 'static,
|
N::Err: error::Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
match read(path)? {
|
match read(path)? {
|
||||||
Some(content) => {
|
Some(content) => Ok(Some(content.trim().parse().with_context(|| {
|
||||||
Ok(Some(content.trim().parse().with_context(|| {
|
|
||||||
format!(
|
format!(
|
||||||
"failed to parse contents of '{path}' as a unsigned number",
|
"failed to parse contents of '{path}' as a unsigned number",
|
||||||
path = path.display(),
|
path = path.display(),
|
||||||
)
|
)
|
||||||
})?))
|
})?)),
|
||||||
},
|
|
||||||
|
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -10,16 +10,12 @@ mod daemon;
|
||||||
// mod engine;
|
// mod engine;
|
||||||
// mod monitor;
|
// mod monitor;
|
||||||
|
|
||||||
use std::{
|
|
||||||
fmt::Write as _,
|
|
||||||
io,
|
|
||||||
io::Write as _,
|
|
||||||
path::PathBuf,
|
|
||||||
process,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser as _;
|
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 _;
|
use yansi::Paint as _;
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
|
@ -94,7 +90,7 @@ fn real_main() -> anyhow::Result<()> {
|
||||||
.context("failed to load daemon config")?;
|
.context("failed to load daemon config")?;
|
||||||
|
|
||||||
daemon::run(config)
|
daemon::run(config)
|
||||||
},
|
}
|
||||||
|
|
||||||
Command::Cpu {
|
Command::Cpu {
|
||||||
command: CpuCommand::Set(delta),
|
command: CpuCommand::Set(delta),
|
||||||
|
@ -137,20 +133,18 @@ fn main() {
|
||||||
let mut chars = message.char_indices();
|
let mut chars = message.char_indices();
|
||||||
|
|
||||||
let _ = match (chars.next(), chars.next()) {
|
let _ = match (chars.next(), chars.next()) {
|
||||||
(Some((_, first)), Some((second_start, second)))
|
(Some((_, first)), Some((second_start, second))) if second.is_lowercase() => {
|
||||||
if second.is_lowercase() =>
|
|
||||||
{
|
|
||||||
writeln!(
|
writeln!(
|
||||||
err,
|
err,
|
||||||
"{first_lowercase}{rest}",
|
"{first_lowercase}{rest}",
|
||||||
first_lowercase = first.to_lowercase(),
|
first_lowercase = first.to_lowercase(),
|
||||||
rest = &message[second_start..],
|
rest = &message[second_start..],
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
writeln!(err, "{message}")
|
writeln!(err, "{message}")
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
24
src/monitor.rs
Normal file
24
src/monitor.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Try /sys/devices/platform paths for thermal zones as a last resort
|
||||||
|
// if temperature_celsius.is_none() {
|
||||||
|
// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") {
|
||||||
|
// for entry in thermal_zones.flatten() {
|
||||||
|
// let zone_path = entry.path();
|
||||||
|
// let name = entry.file_name().into_string().unwrap_or_default();
|
||||||
|
|
||||||
|
// if name.starts_with("thermal_zone") {
|
||||||
|
// // Try to match by type
|
||||||
|
// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) {
|
||||||
|
// if zone_type.contains("cpu")
|
||||||
|
// || zone_type.contains("x86")
|
||||||
|
// || zone_type.contains("core")
|
||||||
|
// {
|
||||||
|
// if let Ok(temp_mc) = read_sysfs_value::<i32>(zone_path.join("temp")) {
|
||||||
|
// temperature_celsius = Some(temp_mc as f32 / 1000.0);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -1,18 +1,11 @@
|
||||||
|
use anyhow::{Context, anyhow, bail};
|
||||||
|
use yansi::Paint as _;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
path::{
|
path::{Path, PathBuf},
|
||||||
Path,
|
|
||||||
PathBuf,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{
|
|
||||||
Context,
|
|
||||||
anyhow,
|
|
||||||
bail,
|
|
||||||
};
|
|
||||||
use yansi::Paint as _;
|
|
||||||
|
|
||||||
use crate::fs;
|
use crate::fs;
|
||||||
|
|
||||||
/// Represents a pattern of path suffixes used to control charge thresholds
|
/// Represents a pattern of path suffixes used to control charge thresholds
|
||||||
|
@ -161,10 +154,8 @@ impl PowerSupply {
|
||||||
let mut power_supplies = Vec::new();
|
let mut power_supplies = Vec::new();
|
||||||
|
|
||||||
for entry in fs::read_dir(POWER_SUPPLY_PATH)
|
for entry in fs::read_dir(POWER_SUPPLY_PATH)
|
||||||
.context("failed to read power supply entries")?
|
.with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))?
|
||||||
.with_context(|| {
|
.with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))?
|
||||||
format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?")
|
|
||||||
})?
|
|
||||||
{
|
{
|
||||||
let entry = match entry {
|
let entry = match entry {
|
||||||
Ok(entry) => entry,
|
Ok(entry) => entry,
|
||||||
|
@ -172,7 +163,7 @@ impl PowerSupply {
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::warn!("failed to read power supply entry: {error}");
|
log::warn!("failed to read power supply entry: {error}");
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
power_supplies.push(PowerSupply::from_path(entry.path())?);
|
power_supplies.push(PowerSupply::from_path(entry.path())?);
|
||||||
|
@ -190,12 +181,8 @@ impl PowerSupply {
|
||||||
let type_path = self.path.join("type");
|
let type_path = self.path.join("type");
|
||||||
|
|
||||||
fs::read(&type_path)
|
fs::read(&type_path)
|
||||||
.with_context(|| {
|
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))?
|
||||||
format!("failed to read '{path}'", path = type_path.display())
|
.with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))?
|
||||||
})?
|
|
||||||
.with_context(|| {
|
|
||||||
format!("'{path}' doesn't exist", path = type_path.display())
|
|
||||||
})?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.is_from_peripheral = 'is_from_peripheral: {
|
self.is_from_peripheral = 'is_from_peripheral: {
|
||||||
|
@ -214,10 +201,8 @@ impl PowerSupply {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small capacity batteries are likely not laptop batteries.
|
// Small capacity batteries are likely not laptop batteries.
|
||||||
if let Some(energy_full) =
|
if let Some(energy_full) = fs::read_n::<u64>(self.path.join("energy_full"))
|
||||||
fs::read_n::<u64>(self.path.join("energy_full")).with_context(|| {
|
.with_context(|| format!("failed to read the max charge {self} can hold"))?
|
||||||
format!("failed to read the max charge {self} can hold")
|
|
||||||
})?
|
|
||||||
{
|
{
|
||||||
// Most laptop batteries are at least 20,000,000 µWh (20 Wh).
|
// Most laptop batteries are at least 20,000,000 µWh (20 Wh).
|
||||||
// Peripheral batteries are typically much smaller.
|
// Peripheral batteries are typically much smaller.
|
||||||
|
@ -248,31 +233,24 @@ impl PowerSupply {
|
||||||
|
|
||||||
self.charge_threshold_start =
|
self.charge_threshold_start =
|
||||||
fs::read_n::<u64>(self.path.join("charge_control_start_threshold"))
|
fs::read_n::<u64>(self.path.join("charge_control_start_threshold"))
|
||||||
.with_context(|| {
|
.with_context(|| format!("failed to read {self} charge threshold start"))?
|
||||||
format!("failed to read {self} charge threshold start")
|
|
||||||
})?
|
|
||||||
.map_or(0.0, |percent| percent as f64 / 100.0);
|
.map_or(0.0, |percent| percent as f64 / 100.0);
|
||||||
|
|
||||||
self.charge_threshold_end =
|
self.charge_threshold_end =
|
||||||
fs::read_n::<u64>(self.path.join("charge_control_end_threshold"))
|
fs::read_n::<u64>(self.path.join("charge_control_end_threshold"))
|
||||||
.with_context(|| {
|
.with_context(|| format!("failed to read {self} charge threshold end"))?
|
||||||
format!("failed to read {self} charge threshold end")
|
|
||||||
})?
|
|
||||||
.map_or(100.0, |percent| percent as f64 / 100.0);
|
.map_or(100.0, |percent| percent as f64 / 100.0);
|
||||||
|
|
||||||
self.drain_rate_watts =
|
self.drain_rate_watts = match fs::read_n::<i64>(self.path.join("power_now"))
|
||||||
match fs::read_n::<i64>(self.path.join("power_now"))
|
|
||||||
.with_context(|| format!("failed to read {self} power drain"))?
|
.with_context(|| format!("failed to read {self} power drain"))?
|
||||||
{
|
{
|
||||||
Some(drain) => Some(drain as f64),
|
Some(drain) => Some(drain as f64),
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
let current_ua =
|
let current_ua = fs::read_n::<i32>(self.path.join("current_now"))
|
||||||
fs::read_n::<i32>(self.path.join("current_now"))
|
|
||||||
.with_context(|| format!("failed to read {self} current"))?;
|
.with_context(|| format!("failed to read {self} current"))?;
|
||||||
|
|
||||||
let voltage_uv =
|
let voltage_uv = fs::read_n::<i32>(self.path.join("voltage_now"))
|
||||||
fs::read_n::<i32>(self.path.join("voltage_now"))
|
|
||||||
.with_context(|| format!("failed to read {self} voltage"))?;
|
.with_context(|| format!("failed to read {self} voltage"))?;
|
||||||
|
|
||||||
current_ua.zip(voltage_uv).map(|(current, voltage)| {
|
current_ua.zip(voltage_uv).map(|(current, voltage)| {
|
||||||
|
@ -280,7 +258,7 @@ impl PowerSupply {
|
||||||
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
|
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
|
||||||
current as f64 * voltage as f64 / 1e12
|
current as f64 * voltage as f64 / 1e12
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS
|
self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS
|
||||||
|
@ -296,14 +274,12 @@ impl PowerSupply {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn charge_threshold_path_start(&self) -> Option<PathBuf> {
|
pub fn charge_threshold_path_start(&self) -> Option<PathBuf> {
|
||||||
self
|
self.threshold_config
|
||||||
.threshold_config
|
|
||||||
.map(|config| self.path.join(config.path_start))
|
.map(|config| self.path.join(config.path_start))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn charge_threshold_path_end(&self) -> Option<PathBuf> {
|
pub fn charge_threshold_path_end(&self) -> Option<PathBuf> {
|
||||||
self
|
self.threshold_config
|
||||||
.threshold_config
|
|
||||||
.map(|config| self.path.join(config.path_end))
|
.map(|config| self.path.join(config.path_end))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,49 +290,36 @@ impl PowerSupply {
|
||||||
fs::write(
|
fs::write(
|
||||||
&self.charge_threshold_path_start().ok_or_else(|| {
|
&self.charge_threshold_path_start().ok_or_else(|| {
|
||||||
anyhow!(
|
anyhow!(
|
||||||
"power supply '{name}' does not support changing charge threshold \
|
"power supply '{name}' does not support changing charge threshold levels",
|
||||||
levels",
|
|
||||||
name = self.name,
|
name = self.name,
|
||||||
)
|
)
|
||||||
})?,
|
})?,
|
||||||
&((charge_threshold_start * 100.0) as u8).to_string(),
|
&((charge_threshold_start * 100.0) as u8).to_string(),
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| format!("failed to set charge threshold start for {self}"))?;
|
||||||
format!("failed to set charge threshold start for {self}")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.charge_threshold_start = charge_threshold_start;
|
self.charge_threshold_start = charge_threshold_start;
|
||||||
|
|
||||||
log::info!(
|
log::info!("set battery threshold start for {self} to {charge_threshold_start}%");
|
||||||
"set battery threshold start for {self} to {charge_threshold_start}%"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_charge_threshold_end(
|
pub fn set_charge_threshold_end(&mut self, charge_threshold_end: f64) -> anyhow::Result<()> {
|
||||||
&mut self,
|
|
||||||
charge_threshold_end: f64,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
fs::write(
|
fs::write(
|
||||||
&self.charge_threshold_path_end().ok_or_else(|| {
|
&self.charge_threshold_path_end().ok_or_else(|| {
|
||||||
anyhow!(
|
anyhow!(
|
||||||
"power supply '{name}' does not support changing charge threshold \
|
"power supply '{name}' does not support changing charge threshold levels",
|
||||||
levels",
|
|
||||||
name = self.name,
|
name = self.name,
|
||||||
)
|
)
|
||||||
})?,
|
})?,
|
||||||
&((charge_threshold_end * 100.0) as u8).to_string(),
|
&((charge_threshold_end * 100.0) as u8).to_string(),
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| format!("failed to set charge threshold end for {self}"))?;
|
||||||
format!("failed to set charge threshold end for {self}")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.charge_threshold_end = charge_threshold_end;
|
self.charge_threshold_end = charge_threshold_end;
|
||||||
|
|
||||||
log::info!(
|
log::info!("set battery threshold end for {self} to {charge_threshold_end}%");
|
||||||
"set battery threshold end for {self} to {charge_threshold_end}%"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -364,23 +327,20 @@ impl PowerSupply {
|
||||||
pub fn get_available_platform_profiles() -> anyhow::Result<Vec<String>> {
|
pub fn get_available_platform_profiles() -> anyhow::Result<Vec<String>> {
|
||||||
let path = "/sys/firmware/acpi/platform_profile_choices";
|
let path = "/sys/firmware/acpi/platform_profile_choices";
|
||||||
|
|
||||||
let Some(content) = fs::read(path)
|
let Some(content) =
|
||||||
.context("failed to read available ACPI platform profiles")?
|
fs::read(path).context("failed to read available ACPI platform profiles")?
|
||||||
else {
|
else {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(
|
Ok(content
|
||||||
content
|
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(ToString::to_string)
|
.map(ToString::to_string)
|
||||||
.collect(),
|
.collect())
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the platform profile.
|
/// Sets the platform profile.
|
||||||
/// This changes the system performance, temperature, fan, and other hardware
|
/// This changes the system performance, temperature, fan, and other hardware replated characteristics.
|
||||||
/// related characteristics.
|
|
||||||
///
|
///
|
||||||
/// Also see [`The Kernel docs`] for this.
|
/// Also see [`The Kernel docs`] for this.
|
||||||
///
|
///
|
||||||
|
@ -393,16 +353,13 @@ impl PowerSupply {
|
||||||
.any(|avail_profile| avail_profile == profile)
|
.any(|avail_profile| avail_profile == profile)
|
||||||
{
|
{
|
||||||
bail!(
|
bail!(
|
||||||
"profile '{profile}' is not available for system. valid profiles: \
|
"profile '{profile}' is not available for system. valid profiles: {profiles}",
|
||||||
{profiles}",
|
|
||||||
profiles = profiles.join(", "),
|
profiles = profiles.join(", "),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::write("/sys/firmware/acpi/platform_profile", profile).context(
|
fs::write("/sys/firmware/acpi/platform_profile", profile)
|
||||||
"this probably means that your system does not support changing ACPI \
|
.context("this probably means that your system does not support changing ACPI profiles")
|
||||||
profiles",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn platform_profile() -> anyhow::Result<String> {
|
pub fn platform_profile() -> anyhow::Result<String> {
|
||||||
|
|
149
src/system.rs
149
src/system.rs
|
@ -1,19 +1,8 @@
|
||||||
use std::{
|
use std::{collections::HashMap, path::Path, time::Instant};
|
||||||
collections::HashMap,
|
|
||||||
path::Path,
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{
|
use anyhow::{Context, bail};
|
||||||
Context,
|
|
||||||
bail,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{cpu, fs, power_supply};
|
||||||
cpu,
|
|
||||||
fs,
|
|
||||||
power_supply,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct System {
|
pub struct System {
|
||||||
|
@ -63,8 +52,8 @@ impl System {
|
||||||
|
|
||||||
{
|
{
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
self.power_supplies = power_supply::PowerSupply::all()
|
self.power_supplies =
|
||||||
.context("failed to scan power supplies")?;
|
power_supply::PowerSupply::all().context("failed to scan power supplies")?;
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"rescanned all power supplies in {millis}ms",
|
"rescanned all power supplies in {millis}ms",
|
||||||
millis = start.elapsed().as_millis(),
|
millis = start.elapsed().as_millis(),
|
||||||
|
@ -77,8 +66,7 @@ impl System {
|
||||||
.any(|power_supply| power_supply.is_ac())
|
.any(|power_supply| power_supply.is_ac())
|
||||||
|| {
|
|| {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"checking whether if this device is a desktop to determine if it is \
|
"checking whether if this device is a desktop to determine if it is AC as no power supplies are"
|
||||||
AC as no power supplies are"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
@ -127,16 +115,14 @@ impl System {
|
||||||
let mut temperatures = HashMap::new();
|
let mut temperatures = HashMap::new();
|
||||||
|
|
||||||
for entry in fs::read_dir(PATH)
|
for entry in fs::read_dir(PATH)
|
||||||
.context("failed to read hardware information")?
|
.with_context(|| format!("failed to read hardware information from '{PATH}'"))?
|
||||||
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
|
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
|
||||||
{
|
{
|
||||||
let entry =
|
let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
|
||||||
entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
|
|
||||||
|
|
||||||
let entry_path = entry.path();
|
let entry_path = entry.path();
|
||||||
|
|
||||||
let Some(name) =
|
let Some(name) = fs::read(entry_path.join("name")).with_context(|| {
|
||||||
fs::read(entry_path.join("name")).with_context(|| {
|
|
||||||
format!(
|
format!(
|
||||||
"failed to read name of hardware entry at '{path}'",
|
"failed to read name of hardware entry at '{path}'",
|
||||||
path = entry_path.display(),
|
path = entry_path.display(),
|
||||||
|
@ -150,78 +136,14 @@ impl System {
|
||||||
// TODO: 'zenergy' can also report those stats, I think?
|
// TODO: 'zenergy' can also report those stats, I think?
|
||||||
"coretemp" | "k10temp" | "zenpower" | "amdgpu" => {
|
"coretemp" | "k10temp" | "zenpower" | "amdgpu" => {
|
||||||
Self::get_temperatures(&entry_path, &mut temperatures)?;
|
Self::get_temperatures(&entry_path, &mut temperatures)?;
|
||||||
},
|
}
|
||||||
|
|
||||||
// Other CPU temperature drivers.
|
// Other CPU temperature drivers.
|
||||||
_ if name.contains("cpu") || name.contains("temp") => {
|
_ if name.contains("cpu") || name.contains("temp") => {
|
||||||
Self::get_temperatures(&entry_path, &mut temperatures)?;
|
Self::get_temperatures(&entry_path, &mut temperatures)?;
|
||||||
},
|
|
||||||
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if temperatures.is_empty() {
|
_ => {}
|
||||||
const PATH: &str = "/sys/devices/virtual/thermal";
|
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"failed to get CPU temperature information by using hwmon, falling \
|
|
||||||
back to '{PATH}'"
|
|
||||||
);
|
|
||||||
|
|
||||||
let Some(thermal_zones) =
|
|
||||||
fs::read_dir(PATH).context("failed to read thermal information")?
|
|
||||||
else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut counter = 0;
|
|
||||||
|
|
||||||
for entry in thermal_zones {
|
|
||||||
let entry =
|
|
||||||
entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
|
|
||||||
|
|
||||||
let entry_path = entry.path();
|
|
||||||
|
|
||||||
let entry_name = entry.file_name();
|
|
||||||
let entry_name = entry_name.to_string_lossy();
|
|
||||||
|
|
||||||
if !entry_name.starts_with("thermal_zone") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(entry_type) =
|
|
||||||
fs::read(entry_path.join("type")).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to read type of zone at '{path}'",
|
|
||||||
path = entry_path.display(),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !entry_type.contains("cpu")
|
|
||||||
&& !entry_type.contains("x86")
|
|
||||||
&& !entry_type.contains("core")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(temperature_mc) = fs::read_n::<i64>(entry_path.join("temp"))
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to read temperature of zone at '{path}'",
|
|
||||||
path = entry_path.display(),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Magic value to see that it is from the thermal zones.
|
|
||||||
temperatures.insert(777 + counter, temperature_mc as f64 / 1000.0);
|
|
||||||
counter += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +185,7 @@ impl System {
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
log::debug!("label content: {label}");
|
log::debug!("label content: {number}");
|
||||||
|
|
||||||
// Match various common label formats:
|
// Match various common label formats:
|
||||||
// "Core X", "core X", "Core-X", "CPU Core X", etc.
|
// "Core X", "core X", "Core-X", "CPU Core X", etc.
|
||||||
|
@ -282,24 +204,18 @@ impl System {
|
||||||
.trim_start_matches("-")
|
.trim_start_matches("-")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
log::debug!(
|
log::debug!("stripped 'Core' or similar identifier prefix of label content: {number}");
|
||||||
"stripped 'Core' or similar identifier prefix of label content: \
|
|
||||||
{number}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let Ok(number) = number.parse::<u32>() else {
|
let Ok(number) = number.parse::<u32>() else {
|
||||||
log::debug!("stripped content not a valid number, skipping");
|
log::debug!("stripped content not a valid number, skipping");
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
log::debug!(
|
log::debug!("stripped content is a valid number, taking it as the core number");
|
||||||
"stripped content is a valid number, taking it as the core number"
|
|
||||||
);
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"it is fine if this number doesn't seem accurate due to CPU binning, see a more detailed explanation at: https://rgbcu.be/blog/why-cores"
|
"it is fine if this number doesn't seem accurate due to CPU binning, see a more detailed explanation at: https://rgbcu.be/blog/why-cores"
|
||||||
);
|
);
|
||||||
|
|
||||||
let Some(temperature_mc) =
|
let Some(temperature_mc) = fs::read_n::<i64>(&input_path).with_context(|| {
|
||||||
fs::read_n::<i64>(&input_path).with_context(|| {
|
|
||||||
format!(
|
format!(
|
||||||
"failed to read CPU temperature from '{path}'",
|
"failed to read CPU temperature from '{path}'",
|
||||||
path = input_path.display(),
|
path = input_path.display(),
|
||||||
|
@ -309,8 +225,8 @@ impl System {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"temperature content: {celsius} celsius",
|
"temperature content: {celcius} celcius",
|
||||||
celsius = temperature_mc as f64 / 1000.0
|
celcius = temperature_mc as f64 / 1000.0
|
||||||
);
|
);
|
||||||
|
|
||||||
temperatures.insert(number, temperature_mc as f64 / 1000.0);
|
temperatures.insert(number, temperature_mc as f64 / 1000.0);
|
||||||
|
@ -321,25 +237,25 @@ impl System {
|
||||||
|
|
||||||
fn is_desktop(&mut self) -> anyhow::Result<bool> {
|
fn is_desktop(&mut self) -> anyhow::Result<bool> {
|
||||||
log::debug!("checking chassis type to determine if we are a desktop");
|
log::debug!("checking chassis type to determine if we are a desktop");
|
||||||
if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type")
|
if let Some(chassis_type) =
|
||||||
.context("failed to read chassis type")?
|
fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")?
|
||||||
{
|
{
|
||||||
// 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower,
|
// 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower,
|
||||||
// 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In
|
// 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One,
|
||||||
// One, 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main
|
// 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis,
|
||||||
// Server Chassis, 31=Convertible Laptop
|
// 31=Convertible Laptop
|
||||||
match chassis_type.trim() {
|
match chassis_type.trim() {
|
||||||
// Desktop form factors.
|
// Desktop form factors.
|
||||||
"3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => {
|
"3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => {
|
||||||
log::debug!("chassis is a desktop form factor, short circuting true");
|
log::debug!("chassis is a desktop form factor, short circuting true");
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
},
|
}
|
||||||
|
|
||||||
// Laptop form factors.
|
// Laptop form factors.
|
||||||
"9" | "10" | "14" | "31" => {
|
"9" | "10" | "14" | "31" => {
|
||||||
log::debug!("chassis is a laptop form factor, short circuting false");
|
log::debug!("chassis is a laptop form factor, short circuting false");
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
},
|
}
|
||||||
|
|
||||||
// Unknown, continue with other checks
|
// Unknown, continue with other checks
|
||||||
_ => log::debug!("unknown chassis type"),
|
_ => log::debug!("unknown chassis type"),
|
||||||
|
@ -363,8 +279,7 @@ impl System {
|
||||||
|
|
||||||
log::debug!("checking if power saving paths exists");
|
log::debug!("checking if power saving paths exists");
|
||||||
// Check CPU power policies, desktops often don't have these
|
// Check CPU power policies, desktops often don't have these
|
||||||
let power_saving_exists =
|
let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp")
|
||||||
fs::exists("/sys/module/intel_pstate/parameters/no_hwp")
|
|
||||||
|| fs::exists("/sys/devices/system/cpu/cpufreq/conservative");
|
|| fs::exists("/sys/devices/system/cpu/cpufreq/conservative");
|
||||||
|
|
||||||
if !power_saving_exists {
|
if !power_saving_exists {
|
||||||
|
@ -373,9 +288,7 @@ impl System {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to assuming desktop if we can't determine.
|
// Default to assuming desktop if we can't determine.
|
||||||
log::debug!(
|
log::debug!("cannot determine whether if we are a desktop, defaulting to true");
|
||||||
"cannot determine whether if we are a desktop, defaulting to true"
|
|
||||||
);
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,15 +299,11 @@ impl System {
|
||||||
|
|
||||||
let mut parts = content.split_whitespace();
|
let mut parts = content.split_whitespace();
|
||||||
|
|
||||||
let (
|
let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) =
|
||||||
Some(load_average_1min),
|
(parts.next(), parts.next(), parts.next())
|
||||||
Some(load_average_5min),
|
|
||||||
Some(load_average_15min),
|
|
||||||
) = (parts.next(), parts.next(), parts.next())
|
|
||||||
else {
|
else {
|
||||||
bail!(
|
bail!(
|
||||||
"failed to parse first 3 load average entries due to there not being \
|
"failed to parse first 3 load average entries due to there not being enough, content: {content}"
|
||||||
enough, content: {content}"
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue