#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct PowerReading {
pub(crate) cpu_power: f64,
pub(crate) gpu_power: Option<f64>,
}
pub(crate) fn parse_block(block: &str, is_apple_silicon: bool) -> Option<PowerReading> {
if is_apple_silicon {
Some(PowerReading {
cpu_power: extract_cpu_power(block)?,
gpu_power: extract_labelled_power(block, "GPU Power:"),
})
} else {
Some(PowerReading {
cpu_power: extract_intel_package_power(block)?,
gpu_power: None,
})
}
}
fn extract_cpu_power(block: &str) -> Option<f64> {
if let Some(power) = extract_labelled_power(block, "CPU Power:") {
return Some(power);
}
let clusters: f64 = block
.lines()
.map(str::trim)
.filter(|line| line.starts_with("E-Cluster Power:") || line.starts_with("P-Cluster Power:"))
.filter_map(milliwatts_after_colon)
.sum();
(clusters > 0.0).then_some(clusters)
}
fn extract_labelled_power(block: &str, label: &str) -> Option<f64> {
block
.lines()
.map(str::trim)
.find(|line| line.starts_with(label))
.and_then(milliwatts_after_colon)
}
fn milliwatts_after_colon(line: &str) -> Option<f64> {
let value = line.split(':').nth(1)?.split_whitespace().next()?;
Some(value.parse::<f64>().ok()? / 1000.0)
}
fn extract_intel_package_power(block: &str) -> Option<f64> {
let line = block
.lines()
.map(str::trim)
.find(|line| line.contains("package power") || line.contains("Package Power"))?;
let parts: Vec<&str> = line.split_whitespace().collect();
for (index, part) in parts.iter().enumerate() {
if let Some(value) = part.strip_suffix("mW")
&& let Ok(milliwatts) = value.parse::<f64>()
{
return Some(milliwatts / 1000.0);
}
if let Some(value) = part.strip_suffix('W')
&& let Ok(watts) = value.parse::<f64>()
{
return Some(watts);
}
if *part == "W"
&& index > 0
&& let Ok(watts) = parts[index - 1].parse::<f64>()
{
return Some(watts);
}
if *part == "mW"
&& index > 0
&& let Ok(milliwatts) = parts[index - 1].parse::<f64>()
{
return Some(milliwatts / 1000.0);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
const APPLE_SILICON_BLOCK: &str = "\
*** Sampled system activity (Mon Jan 1 12:00:00 2026) ***
**** Processor usage ****
E-Cluster Power: 120 mW
P-Cluster Power: 2380 mW
GPU Power: 415 mW
ANE Power: 0 mW
Combined Power (CPU + GPU + ANE): 2915 mW
";
const APPLE_SILICON_SINGLE_CPU_BLOCK: &str = "\
*** Sampled system activity ***
CPU Power: 3500 mW
GPU Power: 200 mW
";
const INTEL_BLOCK: &str = "\
*** Sampled system activity ***
Intel energy model derived package power (CPUs+GT+SA): 14.52W
";
#[test]
fn apple_silicon_cpu_power_sums_the_clusters() {
let reading = parse_block(APPLE_SILICON_BLOCK, true).unwrap();
assert!((reading.cpu_power - 2.5).abs() < 1e-9);
assert_eq!(reading.gpu_power, Some(0.415));
}
#[test]
fn apple_silicon_prefers_a_single_cpu_power_line() {
let reading = parse_block(APPLE_SILICON_SINGLE_CPU_BLOCK, true).unwrap();
assert_eq!(reading.cpu_power, 3.5);
assert_eq!(reading.gpu_power, Some(0.2));
}
#[test]
fn intel_package_power_is_read_in_watts() {
let reading = parse_block(INTEL_BLOCK, false).unwrap();
assert_eq!(reading.cpu_power, 14.52);
assert_eq!(reading.gpu_power, None);
}
#[test]
fn intel_package_power_handles_a_separated_unit() {
let block = "package power: 12.5 W\n";
assert_eq!(extract_intel_package_power(block), Some(12.5));
let block = "Package Power: 900 mW\n";
assert_eq!(extract_intel_package_power(block), Some(0.9));
}
#[test]
fn blocks_without_power_figures_are_skipped() {
let header = "*** Sampled system activity ***\n\n**** Processor usage ****\n";
assert_eq!(parse_block(header, true), None);
assert_eq!(parse_block(header, false), None);
assert_eq!(parse_block("", true), None);
}
#[test]
fn locale_specific_decimal_commas_are_not_misread() {
assert_eq!(extract_intel_package_power("package power: 14,52W\n"), None);
}
}