use std::time::Duration;
pub type DvfsTable = Vec<u32>;
const DVFS_ENTRY_LEN: usize = 8;
const MAX_PLAUSIBLE_CLOCK_MHZ: u32 = 4_000;
pub fn parse_dvfs_table(blob: &[u8]) -> Option<DvfsTable> {
if blob.is_empty() || !blob.len().is_multiple_of(DVFS_ENTRY_LEN) {
return None;
}
let mut table = Vec::with_capacity(blob.len() / DVFS_ENTRY_LEN);
for pair in blob.chunks_exact(DVFS_ENTRY_LEN) {
let hz = u32::from_le_bytes([pair[0], pair[1], pair[2], pair[3]]);
let mhz = ((u64::from(hz) + 500_000) / 1_000_000) as u32;
if mhz > MAX_PLAUSIBLE_CLOCK_MHZ {
return None;
}
table.push(mhz);
}
Some(table)
}
pub fn state_index(name: &str) -> Option<usize> {
if name.eq_ignore_ascii_case("off") {
return Some(0);
}
let digits = name.strip_prefix('P').or_else(|| name.strip_prefix('p'))?;
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
digits.parse().ok()
}
pub fn residency_weighted_clock_mhz(
residencies: &[(String, u64)],
table: &DvfsTable,
) -> Option<u32> {
let mut weighted = 0u128;
let mut active_ticks = 0u128;
let mut known_ticks = 0u128;
for (name, ticks) in residencies {
let clock = match state_index(name).and_then(|index| table.get(index)) {
Some(clock) => *clock,
None if *ticks == 0 => continue,
None => return None,
};
known_ticks += u128::from(*ticks);
if clock > 0 {
weighted += u128::from(*ticks) * u128::from(clock);
active_ticks += u128::from(*ticks);
}
}
if known_ticks == 0 {
return None;
}
if active_ticks == 0 {
return Some(0);
}
u32::try_from(weighted / active_ticks).ok()
}
pub fn active_residency_pct(residencies: &[(String, u64)], table: &DvfsTable) -> Option<f32> {
let mut active = 0u128;
let mut total = 0u128;
for (name, ticks) in residencies {
let clock = match state_index(name).and_then(|index| table.get(index)) {
Some(clock) => *clock,
None if *ticks == 0 => continue,
None => return None,
};
total += u128::from(*ticks);
if clock > 0 {
active += u128::from(*ticks);
}
}
if total == 0 {
return None;
}
Some((active as f64 / total as f64 * 100.0) as f32)
}
pub fn energy_delta_to_watts(delta: i64, unit: &str, elapsed: Duration) -> Option<f32> {
if delta < 0 {
return None;
}
let seconds = elapsed.as_secs_f64();
if seconds <= 0.0 {
return None;
}
let joules = delta as f64 * energy_unit_joules(unit)?;
Some((joules / seconds) as f32)
}
fn energy_unit_joules(unit: &str) -> Option<f64> {
match unit.trim() {
"nJ" => Some(1e-9),
"uJ" | "µJ" | "μJ" => Some(1e-6),
"mJ" => Some(1e-3),
"J" => Some(1.0),
_ => None,
}
}
pub fn clamp_pct(raw: i64) -> f32 {
(raw as f32).clamp(0.0, 100.0)
}
#[cfg(test)]
mod tests {
use super::*;
const M3_DVFS_BLOB: &[u8] = &[
0x00, 0x00, 0x00, 0x00, 0x7d, 0x00, 0x00, 0x00, 0x80, 0x78, 0x25, 0x14, 0x71, 0x02, 0x00, 0x00, 0x80, 0xee, 0xd5, 0x24, 0xa8, 0x02, 0x00, 0x00, 0x00, 0xff, 0x71, 0x2f, 0xd5, 0x02, 0x00, 0x00, 0x00, 0x59, 0xd4, 0x31, 0xfd, 0x02, 0x00, 0x00, 0x00, 0x28, 0x50, 0x37, 0xfd, 0x02, 0x00, 0x00, 0x00, 0x5e, 0xbe, 0x38, 0x2f, 0x03, 0x00, 0x00, 0x00, 0x48, 0xf1, 0x3e, 0x2f, 0x03, 0x00, 0x00, 0x40, 0x81, 0xc3, 0x3e, 0x6b, 0x03, 0x00, 0x00, 0x80, 0xc8, 0xbc, 0x45, 0x6b, 0x03, 0x00, 0x00, 0x00, 0x20, 0xaa, 0x44, 0x93, 0x03, 0x00, 0x00, 0x80, 0xbb, 0x2c, 0x4c, 0x93, 0x03, 0x00, 0x00, 0x00, 0x95, 0xc3, 0x47, 0xac, 0x03, 0x00, 0x00, 0x80, 0x42, 0xc0, 0x4f, 0xac, 0x03, 0x00, 0x00, ];
fn m3_table() -> DvfsTable {
parse_dvfs_table(M3_DVFS_BLOB).expect("the captured M3 blob must decode")
}
fn residency(pairs: &[(&str, u64)]) -> Vec<(String, u64)> {
pairs.iter().map(|(n, t)| ((*n).into(), *t)).collect()
}
#[test]
fn dvfs_blob_decodes_to_the_states_the_hardware_reports() {
let table = m3_table();
assert_eq!(table.len(), 14, "parked state plus P1..P13");
assert_eq!(table[0], 0, "index 0 is the parked state");
assert_eq!(table[1], 338);
assert_eq!(table[13], 1338);
}
#[test]
fn dvfs_table_is_not_assumed_monotonic() {
let table = m3_table();
assert!(table[8] < table[7], "P8 really is slower than P7 here");
}
#[test]
fn dvfs_blob_rejects_a_partial_pair() {
assert_eq!(parse_dvfs_table(&[0x00, 0x01, 0x02]), None);
}
#[test]
fn dvfs_blob_rejects_an_empty_property() {
assert_eq!(parse_dvfs_table(&[]), None);
}
#[test]
fn dvfs_blob_rejects_an_implausible_clock() {
let blob = [0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00];
assert_eq!(parse_dvfs_table(&blob), None);
}
#[test]
fn state_names_map_to_table_indices() {
assert_eq!(state_index("OFF"), Some(0));
assert_eq!(state_index("off"), Some(0));
assert_eq!(state_index("P1"), Some(1));
assert_eq!(state_index("P13"), Some(13));
}
#[test]
fn unknown_state_names_are_not_guessed_at() {
assert_eq!(state_index("SW_P1"), None);
assert_eq!(state_index("IDLE"), None);
assert_eq!(state_index("P"), None);
assert_eq!(state_index("Px"), None);
assert_eq!(state_index(""), None);
}
#[test]
fn clock_excludes_the_parked_state_from_the_average() {
let table = m3_table();
let r = residency(&[("OFF", 900), ("P13", 100)]);
assert_eq!(residency_weighted_clock_mhz(&r, &table), Some(1338));
}
#[test]
fn clock_weights_active_states_by_residency() {
let table = m3_table();
let r = residency(&[("P1", 3), ("P13", 1)]);
assert_eq!(residency_weighted_clock_mhz(&r, &table), Some(588));
}
#[test]
fn clock_is_zero_when_the_gpu_was_parked_all_interval() {
let table = m3_table();
let r = residency(&[("OFF", 24_000_000)]);
assert_eq!(residency_weighted_clock_mhz(&r, &table), Some(0));
}
#[test]
fn clock_is_unknown_when_no_counter_advanced() {
let table = m3_table();
assert_eq!(residency_weighted_clock_mhz(&[], &table), None);
let r = residency(&[("OFF", 0), ("P1", 0)]);
assert_eq!(residency_weighted_clock_mhz(&r, &table), None);
}
#[test]
fn clock_is_unknown_when_the_table_cannot_name_a_state_that_ran() {
let table = m3_table();
let r = residency(&[("P1", 10), ("P99", 10)]);
assert_eq!(residency_weighted_clock_mhz(&r, &table), None);
assert_eq!(active_residency_pct(&r, &table), None);
}
#[test]
fn clock_ignores_padding_states_that_never_ran() {
let table = m3_table();
let r = residency(&[("OFF", 900), ("P1", 100), ("P14", 0), ("P15", 0)]);
assert_eq!(residency_weighted_clock_mhz(&r, &table), Some(338));
let pct = active_residency_pct(&r, &table).expect("counters advanced");
assert!((pct - 10.0).abs() < 0.001, "expected 10%, got {pct}");
}
#[test]
fn clock_survives_a_full_second_of_maximum_residency() {
let table = m3_table();
let r = residency(&[("P13", u64::MAX)]);
assert_eq!(residency_weighted_clock_mhz(&r, &table), Some(1338));
}
#[test]
fn active_residency_is_the_unparked_fraction() {
let table = m3_table();
let r = residency(&[("OFF", 750), ("P1", 250)]);
let pct = active_residency_pct(&r, &table).expect("counters advanced");
assert!((pct - 25.0).abs() < 0.001, "expected 25%, got {pct}");
}
#[test]
fn active_residency_is_unknown_when_nothing_advanced() {
assert_eq!(active_residency_pct(&[], &m3_table()), None);
}
#[test]
fn energy_converts_every_unit_the_channels_use() {
let one_second = Duration::from_secs(1);
let nano = energy_delta_to_watts(159_324_641, "nJ", one_second).unwrap();
let milli = energy_delta_to_watts(153, "mJ", one_second).unwrap();
assert!((nano - 0.159).abs() < 0.001, "nJ path: {nano}");
assert!((milli - 0.153).abs() < 0.001, "mJ path: {milli}");
}
#[test]
fn energy_accepts_both_spellings_of_micro() {
let one_second = Duration::from_secs(1);
for unit in ["uJ", "µJ", "μJ"] {
let w = energy_delta_to_watts(1_000_000, unit, one_second)
.unwrap_or_else(|| panic!("unit {unit} must be recognised"));
assert!((w - 1.0).abs() < 0.001, "unit {unit}: {w}");
}
}
#[test]
fn energy_scales_by_the_real_interval() {
let w = energy_delta_to_watts(2_000, "mJ", Duration::from_secs(2)).unwrap();
assert!((w - 1.0).abs() < 0.001, "got {w}");
}
#[test]
fn energy_rejects_an_unknown_unit() {
assert_eq!(
energy_delta_to_watts(1, "furlongs", Duration::from_secs(1)),
None
);
}
#[test]
fn energy_rejects_a_counter_reset() {
assert_eq!(
energy_delta_to_watts(-5, "mJ", Duration::from_secs(1)),
None
);
}
#[test]
fn energy_rejects_a_zero_interval() {
assert_eq!(energy_delta_to_watts(100, "mJ", Duration::ZERO), None);
}
#[test]
fn driver_percentages_are_clamped_to_the_gauge_range() {
assert_eq!(clamp_pct(-1), 0.0);
assert_eq!(clamp_pct(0), 0.0);
assert_eq!(clamp_pct(62), 62.0);
assert_eq!(clamp_pct(101), 100.0);
}
}