Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ env_logger = "0.11.8"
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" }
1 change: 1 addition & 0 deletions watt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ clap.workspace = true
clap-verbosity-flag.workspace = true
ctrlc.workspace = true
env_logger.workspace = true
humantime.workspace = true
log.workspace = true
nix.workspace = true
num_cpus.workspace = true
Expand Down
35 changes: 35 additions & 0 deletions watt/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{
collections::{
HashMap,
HashSet,
VecDeque,
},
fs,
path::Path,
Expand All @@ -20,6 +21,7 @@ use serde::{
use crate::{
cpu,
power_supply,
system,
};

fn is_default<T: Default + PartialEq>(value: &T) -> bool {
Expand Down Expand Up @@ -433,6 +435,11 @@ pub enum Expression {
#[serde(with = "expression::cpu_usage_volatility")]
CpuUsageVolatility,

CpuUsageSince {
#[serde(rename = "cpu-usage-since")]
duration: String,
},

#[serde(with = "expression::cpu_temperature")]
CpuTemperature,

Expand Down Expand Up @@ -624,6 +631,7 @@ pub struct EvalState<'peripherals, 'context> {

pub cpus: &'peripherals HashSet<Arc<cpu::Cpu>>,
pub power_supplies: &'peripherals HashSet<Arc<power_supply::PowerSupply>>,
pub cpu_log: &'peripherals VecDeque<system::CpuLog>,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -741,6 +749,27 @@ impl Expression {
TurboAvailable => Boolean(state.turbo_available),

CpuUsage => Number(state.cpu_usage),
CpuUsageSince { duration } => {
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 None for insufficient data, consistent with volatility
// expressions
return Ok(None);
}

Number(
recent_logs.iter().map(|log| log.usage).sum::<f64>()
/ recent_logs.len() as f64,
)
},
CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)),
CpuTemperature => Number(try_ok!(state.cpu_temperature)),
CpuTemperatureVolatility => {
Expand Down Expand Up @@ -1027,13 +1056,15 @@ mod tests {
available_epbs: vec![],
epb: None,
stat: cpu::CpuStat::default(),
previous_stat: None,
info: None,
});

let mut cpus = HashSet::new();
cpus.insert(cpu.clone());

let power_supplies = HashSet::new();
let cpu_log = VecDeque::new();

// Create an eval state with the base frequency
let state = EvalState {
Expand All @@ -1052,6 +1083,7 @@ mod tests {
context: EvalContext::Cpu(&cpu),
cpus: &cpus,
power_supplies: &power_supplies,
cpu_log: &cpu_log,
};

// Create an expression like: { value = "$cpu-frequency-maximum", multiply = 0.65 }
Expand Down Expand Up @@ -1112,13 +1144,15 @@ mod tests {
available_epbs: vec![],
epb: None,
stat: cpu::CpuStat::default(),
previous_stat: None,
info: None,
});

let mut cpus = HashSet::new();
cpus.insert(cpu.clone());

let power_supplies = HashSet::new();
let cpu_log = VecDeque::new();

let state = EvalState {
frequency_available: true,
Expand All @@ -1136,6 +1170,7 @@ mod tests {
context: EvalContext::Cpu(&cpu),
cpus: &cpus,
power_supplies: &power_supplies,
cpu_log: &cpu_log,
};

// 3333 * 0.65 = 2166.45
Expand Down
41 changes: 37 additions & 4 deletions watt/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,27 @@ impl CpuStat {
self.idle + self.iowait
}

pub fn usage(&self) -> f64 {
1.0 - self.idle() as f64 / self.total() as f64
/// Calculates usage based on delta between this stat and a previous stat.
/// This gives the current CPU usage percentage over the time interval.
pub fn usage_delta(&self, previous: &CpuStat) -> f64 {
let total_delta = self.total().saturating_sub(previous.total()) as f64;
let idle_delta = self.idle().saturating_sub(previous.idle()) as f64;

if total_delta == 0.0 {
return 0.0;
}

1.0 - idle_delta / total_delta
}

/// Calculates usage since boot (average utilization).
/// This is the historical average, not current usage.
pub fn usage_average(&self) -> f64 {
let total = self.total() as f64;
if total == 0.0 {
return 0.0;
}
1.0 - self.idle() as f64 / total
}
}

Expand All @@ -75,8 +94,10 @@ pub struct Cpu {
pub available_epbs: Vec<String>,
pub epb: Option<String>,

pub stat: CpuStat,
pub info: Option<Arc<HashMap<String, String>>>,
pub stat: CpuStat,
/// Previous stat reading for calculating current usage.
pub previous_stat: Option<CpuStat>,
pub info: Option<Arc<HashMap<String, String>>>,
}

impl PartialEq for Cpu {
Expand All @@ -102,6 +123,14 @@ impl fmt::Display for Cpu {
}

impl Cpu {
/// Returns current CPU usage based on delta from previous reading.
/// Returns 0.0 on first reading when no previous stat is available.
pub fn current_usage(&self) -> f64 {
match &self.previous_stat {
Some(prev) => self.stat.usage_delta(prev),
None => 0.0,
}
}
/// Get all CPUs.
pub fn all() -> anyhow::Result<Vec<Cpu>> {
fn from_number(number: u32, cache: &CpuScanCache) -> anyhow::Result<Cpu> {
Expand Down Expand Up @@ -365,6 +394,10 @@ impl Cpu {
},
};

// Store current stat as previous before updating to enable delta
// calculation
self.previous_stat = Some(self.stat.clone());

self.stat = stat
.get(&self.number)
.with_context(|| format!("failed to get stat of {self}"))?
Expand Down
6 changes: 5 additions & 1 deletion watt/power_supply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,11 @@ impl PowerSupply {
self.is_from_peripheral = 'is_from_peripheral: {
let name_lower = self.name.to_lowercase();

log::trace!("power supply '{name}' type: {type_}", name = self.name, type_ = self.type_);
log::trace!(
"power supply '{name}' type: {type_}",
name = self.name,
type_ = self.type_
);

// Common peripheral battery names.
if name_lower.contains("mouse")
Expand Down
27 changes: 20 additions & 7 deletions watt/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ use crate::{
power_supply,
};

#[derive(Debug)]
struct CpuLog {
at: Instant,
#[derive(Debug, Clone, PartialEq)]
pub struct CpuLog {
pub at: Instant,

/// CPU usage between 0-1, a percentage.
usage: f64,
pub usage: f64,

/// CPU temperature in celsius.
temperature: f64,
pub temperature: f64,
}

#[derive(Debug)]
Expand Down Expand Up @@ -84,10 +84,22 @@ impl System {

{
let start = Instant::now();

// Preserve previous stats for delta calculation
let previous_stats: HashMap<u32, cpu::CpuStat> = self
.cpus
.iter()
.map(|cpu| (cpu.number, cpu.stat.clone()))
.collect();

self.cpus = cpu::Cpu::all()
.context("failed to scan CPUs")?
.into_iter()
.map(Arc::from)
.map(|mut cpu| {
// Transfer previous stat for this CPU
cpu.previous_stat = previous_stats.get(&cpu.number).cloned();
Arc::from(cpu)
})
.collect();
log::info!(
"scanned all CPUs in {millis}ms",
Expand Down Expand Up @@ -167,7 +179,7 @@ impl System {
let cpu_log = CpuLog {
at,

usage: self.cpus.iter().map(|cpu| cpu.stat.usage()).sum::<f64>()
usage: self.cpus.iter().map(|cpu| cpu.current_usage()).sum::<f64>()
/ self.cpus.len() as f64,

temperature: self.cpu_temperatures.values().sum::<f64>()
Expand Down Expand Up @@ -798,6 +810,7 @@ pub fn run_daemon(config: config::DaemonConfig) -> anyhow::Result<()> {

cpus: &system.cpus,
power_supplies: &system.power_supplies,
cpu_log: &system.cpu_log,
};

let mut cpu_deltas: HashMap<Arc<cpu::Cpu>, cpu::Delta> = system
Expand Down