Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
354 changes: 287 additions & 67 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ clap_complete = "4.5.59"
clap_complete_nushell = "4.5.9"
ctrlc = "3.5.1"
env_logger = "0.11.8"
humantime = "2.3.0"
jiff = "0.2.20"
log = "0.4.28"
nix = { features = [ "fs" ], version = "0.31.1" }
num_cpus = "1.17.0"
humantime = "2.3.0"
serde = { features = [ "derive" ], version = "1.0.228" }
toml = "0.9.8"
yansi = { features = [ "detect-env", "detect-tty" ], version = "1.0.1" }
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ but Watt attempts to support multiple vendor implementations including:
# High performance for sustained workloads with thermal protection
[[rule]]
if.all = [
{ is-more-than = 0.8, value = "%cpu-usage" },
{ is-more-than = 0.8, value = { cpu-usage-since = "2sec" } },
{ is-less-than = 30.0, value = "$cpu-idle-seconds" },
{ is-less-than = 75.0, value = "$cpu-temperature" },
]
Expand Down Expand Up @@ -262,15 +262,30 @@ Watt includes a powerful expression language for defining conditions:

#### System Variables

- `"%cpu-usage"` - Current CPU usage percentage (0.0-1.0)
- `{ cpu-usage-since = "<duration>" }` - CPU usage percentage over a duration
(e.g., `"1sec"`, `"5sec"`)
- `"$cpu-usage-volatility"` - CPU usage volatility measurement
- `"$cpu-temperature"` - CPU temperature in Celsius
- `"$cpu-temperature-volatility"` - CPU temperature volatility
- `"$cpu-idle-seconds"` - Seconds since last significant CPU activity
- `"$cpu-frequency-maximum"` - CPU hardware maximum frequency in MHz
- `"$cpu-frequency-minimum"` - CPU hardware minimum frequency in MHz
- `"$cpu-scaling-maximum"` - Current CPU scaling maximum frequency in MHz (from
`scaling_max_freq`)
- `"%cpu-core-count"` - Number of CPU cores
- `{ load-average-since = "<duration>" }` - System load average over a duration
(e.g., `"5sec"`, `"1min"`)
- `"$hour-of-day"` - Current hour (0-23) based on system local time
- `"?lid-closed"` - Boolean indicating if laptop lid is closed
- `"%power-supply-charge"` - Battery charge percentage (0.0-1.0)
- `"%power-supply-discharge-rate"` - Current discharge rate
- `"%battery-cycles"` - Battery cycle count (aggregated average across all
batteries)
- `"%battery-health"` - Battery health percentage (0.0-1.0, aggregated average
across all batteries)
- `{ battery-cycles-for = "<name>" }` - Battery cycle count for a specific
battery (e.g., `"BAT0"`)
- `{ battery-health-for = "<name>" }` - Battery health for a specific battery
- `"?discharging"` - Boolean indicating if system is on battery power
- `"?frequency-available"` - Boolean indicating if CPU frequency control is
available
Expand All @@ -288,6 +303,7 @@ if.is-energy-performance-preference-available = "balance_performance"
if.is-energy-perf-bias-available = "5"
if.is-platform-profile-available = "low-power"
if.is-driver-loaded = "intel_pstate"
if.is-battery-available = "BAT0"
```

Each will be `true` only if the named value is available on your system. If the
Expand All @@ -305,7 +321,7 @@ You can use operators with TOML attribute sets:

```toml
[[rule]]
if = { is-more-than = "%cpu-usage", value = 0.8 }
if = { is-more-than = { cpu-usage-since = "1sec" }, value = 0.8 }
```

However, `all` and `any` do not take a `value` argument, but instead take a list
Expand Down Expand Up @@ -359,7 +375,7 @@ power.platform-profile = { if.is-platform-profile-available = "low-power", then
# High performance mode for sustained high load
[[rule]]
if.all = [
{ is-more-than = 0.8, value = "%cpu-usage" },
{ is-more-than = 0.8, value = { cpu-usage-since = "2sec" } },
{ is-less-than = 30.0, value = "$cpu-idle-seconds" },
{ is-less-than = 75.0, value = "$cpu-temperature" },
]
Expand All @@ -374,7 +390,7 @@ cpu.turbo = { if = "?turbo-available", then = true }
[[rule]]
if.all = [
{ not = "?discharging" },
{ is-more-than = 0.1, value = "%cpu-usage" },
{ is-more-than = 0.1, value = { cpu-usage-since = "1sec" } },
{ is-less-than = 80.0, value = "$cpu-temperature" },
]
priority = 70
Expand All @@ -387,8 +403,8 @@ cpu.turbo = { if = "?turbo-available", then = true }
# Moderate performance for medium load
[[rule]]
if.all = [
{ is-more-than = 0.4, value = "%cpu-usage" },
{ is-less-than = 0.8, value = "%cpu-usage" },
{ is-more-than = 0.4, value = { cpu-usage-since = "5sec" } },
{ is-less-than = 0.8, value = { cpu-usage-since = "5sec" } },
]
priority = 60

Expand All @@ -399,7 +415,7 @@ cpu.governor = { if.is-governor-available = "schedutil", then = "schedutil" }
# Power saving during low activity
[[rule]]
if.all = [
{ is-less-than = 0.2, value = "%cpu-usage" },
{ is-less-than = 0.2, value = { cpu-usage-since = "10sec" } },
{ is-more-than = 60.0, value = "$cpu-idle-seconds" },
]
priority = 50
Expand Down
1 change: 1 addition & 0 deletions watt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ clap-verbosity-flag.workspace = true
ctrlc.workspace = true
env_logger.workspace = true
humantime.workspace = true
jiff.workspace = true
log.workspace = true
nix.workspace = true
num_cpus.workspace = true
Expand Down
140 changes: 139 additions & 1 deletion watt/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
}

fn find_battery<'a>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be inlined, or defined inside the fn that uses it

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be moved to eval but I reckon we'll need it elsewhere if we end up adding more battery-specific expressions. For now I'm moving it but I strongly suspect it'll be moved out eventually.

power_supplies: &'a HashSet<Arc<power_supply::PowerSupply>>,
name: &str,
) -> Option<&'a power_supply::PowerSupply> {
power_supplies
.iter()
.find(|ps| ps.name == name && ps.type_ == "Battery")
.map(|arc| arc.as_ref())
}

#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
pub struct CpusDelta {
Expand Down Expand Up @@ -390,9 +400,20 @@ mod expression {
named!(cpu_frequency_maximum => "$cpu-frequency-maximum");
named!(cpu_frequency_minimum => "$cpu-frequency-minimum");

named!(cpu_scaling_maximum => "$cpu-scaling-maximum");

named!(cpu_core_count => "%cpu-core-count");

named!(lid_closed => "?lid-closed");

named!(hour_of_day => "$hour-of-day");

named!(power_supply_charge => "%power-supply-charge");
named!(power_supply_discharge_rate => "%power-supply-discharge-rate");

named!(battery_cycles => "$battery-cycles");
named!(battery_health => "%battery-health");

named!(discharging => "?discharging");
}

Expand Down Expand Up @@ -421,6 +442,11 @@ pub enum Expression {
value: Box<Expression>,
},

IsBatteryAvailable {
#[serde(rename = "is-battery-available")]
value: Box<Expression>,
},

#[serde(with = "expression::frequency_available")]
FrequencyAvailable,

Expand Down Expand Up @@ -453,12 +479,45 @@ pub enum Expression {
#[serde(with = "expression::cpu_frequency_minimum")]
CpuFrequencyMinimum,

#[serde(with = "expression::cpu_scaling_maximum")]
CpuScalingMaximum,

#[serde(with = "expression::cpu_core_count")]
CpuCoreCount,

LoadAverageSince {
#[serde(rename = "load-average-since")]
duration: String,
},

#[serde(with = "expression::lid_closed")]
LidClosed,

#[serde(with = "expression::hour_of_day")]
HourOfDay,

#[serde(with = "expression::power_supply_charge")]
PowerSupplyCharge,

#[serde(with = "expression::power_supply_discharge_rate")]
PowerSupplyDischargeRate,

#[serde(with = "expression::battery_cycles")]
BatteryCycles,

#[serde(with = "expression::battery_health")]
BatteryHealth,

BatteryCyclesFor {
#[serde(rename = "battery-cycles-for")]
name: String,
},

BatteryHealthFor {
#[serde(rename = "battery-health-for")]
name: String,
},

#[serde(with = "expression::discharging")]
Discharging,

Expand Down Expand Up @@ -620,9 +679,14 @@ pub struct EvalState<'peripherals, 'context> {
pub cpu_frequency_maximum: Option<f64>,
pub cpu_frequency_minimum: Option<f64>,

pub lid_closed: bool,

pub power_supply_charge: Option<f64>,
pub power_supply_discharge_rate: Option<f64>,

pub battery_cycles: Option<f64>,
pub battery_health: Option<f64>,

pub discharging: bool,

pub context: EvalContext<'context>,
Expand Down Expand Up @@ -743,10 +807,21 @@ impl Expression {

Boolean(crate::fs::exists(format!("/sys/module/{value}")))
},
IsBatteryAvailable { value } => {
let value = eval!(value).try_into_string()?;

Boolean(find_battery(&state.power_supplies, &value).is_some())
},
FrequencyAvailable => Boolean(state.frequency_available),
TurboAvailable => Boolean(state.turbo_available),

CpuUsage => Number(state.cpu_usage),
CpuUsage => {
bail!(
"`%cpu-usage` is deprecated and has been removed. Use \
`cpu-usage-since = \"<duration>\"` instead. For example, \
`cpu-usage-since = \"1sec\"` for CPU usage over the last second."
)
},
CpuUsageSince { duration } => {
let duration = humantime::parse_duration(duration)
.with_context(|| format!("failed to parse duration '{duration}'"))?;
Expand Down Expand Up @@ -777,11 +852,65 @@ impl Expression {
CpuFrequencyMaximum => Number(try_ok!(state.cpu_frequency_maximum)),
CpuFrequencyMinimum => Number(try_ok!(state.cpu_frequency_minimum)),

CpuScalingMaximum => {
let max = state
.cpus
.iter()
.filter_map(|cpu| cpu.frequency_mhz_maximum)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this is the hardware limit. We have no field for software, add cpu.frequency_mhz_software_cap

.max()
.map(|v| v as f64);
Number(try_ok!(max))
},

CpuCoreCount => Number(state.cpus.len() as f64),

LoadAverageSince { duration } => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duration should be an Expr so people can do ifs etc inside. We have .try_into_string as well.

let duration = humantime::parse_duration(duration)
.with_context(|| format!("failed to parse duration '{duration}'"))?;
let recent_logs: Vec<&system::CpuLog> = state
.cpu_log
.iter()
.rev()
.take_while(|log| log.at.elapsed() < duration)
.collect();

if recent_logs.len() < 2 {
return Ok(None);
}

Number(
recent_logs.iter().map(|log| log.load_average).sum::<f64>()
/ recent_logs.len() as f64,
)
},

LidClosed => Boolean(state.lid_closed),

HourOfDay => {
let ts = jiff::Timestamp::now()
.in_tz("local")
.context("failed to get local timezone for `$hour-of-day`")?;
Number(ts.hour() as f64)
},

PowerSupplyCharge => Number(try_ok!(state.power_supply_charge)),
PowerSupplyDischargeRate => {
Number(try_ok!(state.power_supply_discharge_rate))
},

BatteryCycles => Number(try_ok!(state.battery_cycles)),
BatteryHealth => Number(try_ok!(state.battery_health)),

BatteryCyclesFor { name } => {
let battery = find_battery(&state.power_supplies, name);
Number(try_ok!(battery.and_then(|ps| ps.cycles).map(|c| c as f64)))
},

BatteryHealthFor { name } => {
let battery = find_battery(&state.power_supplies, name);
Number(try_ok!(battery.and_then(|ps| ps.health)))
},

Discharging => Boolean(state.discharging),

literal @ (Boolean(_) | Number(_) | String(_)) => literal.clone(),
Expand Down Expand Up @@ -1075,8 +1204,11 @@ mod tests {
cpu_idle_seconds: 10.0,
cpu_frequency_maximum: Some(base_freq as f64),
cpu_frequency_minimum: Some(1000.0),
lid_closed: false,
power_supply_charge: Some(0.8),
power_supply_discharge_rate: Some(10.0),
battery_cycles: Some(100.0),
battery_health: Some(0.95),
discharging: false,
context: EvalContext::Cpu(&cpu),
cpus: &cpus,
Expand Down Expand Up @@ -1162,8 +1294,11 @@ mod tests {
cpu_idle_seconds: 10.0,
cpu_frequency_maximum: Some(3333.0),
cpu_frequency_minimum: Some(1000.0),
lid_closed: false,
power_supply_charge: Some(0.8),
power_supply_discharge_rate: Some(10.0),
battery_cycles: Some(100.0),
battery_health: Some(0.95),
discharging: false,
context: EvalContext::Cpu(&cpu),
cpus: &cpus,
Expand Down Expand Up @@ -1237,8 +1372,11 @@ mod tests {
cpu_idle_seconds: 0.0,
cpu_frequency_maximum: Some(3333.0),
cpu_frequency_minimum: Some(1000.0),
lid_closed: false,
power_supply_charge: None,
power_supply_discharge_rate: None,
battery_cycles: None,
battery_health: None,
discharging: false,
context: EvalContext::Cpu(&cpu),
cpus: &cpus,
Expand Down
Loading