use crate::Result;
use crate::config::AppMatch;
use std::time::Duration;
use sysinfo::System;
pub trait PowerSensor: Send {
fn power(&mut self) -> Result<f64>;
}
pub trait Platform: Send {
fn name(&self) -> String {
let name = System::name().unwrap_or_else(|| "Unknown OS".to_string());
let version = System::os_version().unwrap_or_default();
if version.is_empty() {
name
} else {
format!("{name} {version}")
}
}
fn cpu(&self) -> Box<dyn PowerSensor>;
fn gpu(&self) -> Box<dyn PowerSensor>;
fn cpu_usage(&self) -> Box<dyn CpuUtilization>;
fn process_cpu_usage(&self) -> Option<Box<dyn ProcessCpuUtilization>> {
None
}
fn app_cpu_usage(
&self,
_refresh_interval: Duration,
_app_match: AppMatch,
) -> Option<Box<dyn AppCpuUtilization>> {
None
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CpuTotals {
pub total: u64,
pub idle: u64,
}
impl CpuTotals {
#[must_use]
pub fn new(total: u64, idle: u64) -> Self {
Self { total, idle }
}
#[must_use]
pub fn utilization_since(&self, previous: &CpuTotals) -> f64 {
let total_delta = self.total.saturating_sub(previous.total);
let idle_delta = self.idle.saturating_sub(previous.idle);
if total_delta == 0 {
return 0.0;
}
total_delta.saturating_sub(idle_delta) as f64 / total_delta as f64
}
}
pub trait CpuUtilization: Send {
fn cpu_utilization(&mut self) -> f64;
fn last_totals(&self) -> Option<CpuTotals> {
None
}
}
pub trait ProcessCpuUtilization: Send {
fn process_cpu_utilization(&mut self, pid: u32, cpu_total: Option<u64>) -> f64;
}
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct AppSample {
pub utilization: f64,
pub pids: Vec<u32>,
}
pub trait AppCpuUtilization: Send {
fn app_snapshot(&mut self, app_name: &str, cpu_total: Option<u64>) -> AppSample;
}
#[must_use]
pub fn attribute_power(utilization: f64, attributable_cpu_power: f64, cpu_utilization: f64) -> f64 {
if cpu_utilization < 0.01 || !cpu_utilization.is_finite() {
return 0.0;
}
let attributed = 100.0 * ((utilization * attributable_cpu_power) / cpu_utilization);
if !attributed.is_finite() {
return 0.0;
}
attributed.clamp(0.0, attributable_cpu_power.max(0.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn idle_machine_attributes_nothing() {
assert_eq!(attribute_power(0.5, 40.0, 0.0), 0.0);
assert_eq!(attribute_power(0.5, 40.0, 0.009), 0.0);
}
#[test]
fn half_the_busy_cpu_gets_half_the_power() {
assert_eq!(attribute_power(0.25, 40.0, 50.0), 20.0);
}
#[test]
fn attribution_never_exceeds_the_cpu_budget() {
assert_eq!(attribute_power(0.9, 40.0, 10.0), 40.0);
}
#[test]
fn negative_and_non_finite_inputs_are_clamped() {
assert_eq!(attribute_power(0.5, -5.0, 50.0), 0.0);
assert_eq!(attribute_power(f64::NAN, 40.0, 50.0), 0.0);
assert_eq!(attribute_power(0.5, 40.0, f64::NAN), 0.0);
}
#[test]
fn utilization_is_busy_over_total() {
let previous = CpuTotals::new(1_000, 800);
let current = CpuTotals::new(1_100, 850);
assert_eq!(current.utilization_since(&previous), 0.5);
}
#[test]
fn no_elapsed_time_means_no_usage() {
let totals = CpuTotals::new(1_000, 800);
assert_eq!(totals.utilization_since(&totals), 0.0);
}
#[test]
fn counters_moving_backwards_do_not_underflow() {
let previous = CpuTotals::new(1_000, 800);
let current = CpuTotals::new(900, 900);
assert_eq!(current.utilization_since(&previous), 0.0);
}
}