use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub enum MemoryVariant {
Discrete,
Integrated,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_memory_bytes(device_dir: &Path, variant: MemoryVariant) -> (u64, u64) {
if variant == MemoryVariant::Integrated {
return (0, 0);
}
let mut total = read_u64(&device_dir.join("mem_info_vram_total")).unwrap_or(0);
let mut used = read_u64(&device_dir.join("mem_info_vram_used")).unwrap_or(0);
if total == 0 {
total = read_u64(&device_dir.join("tile0").join("vram0").join("total_bytes")).unwrap_or(0);
}
if used == 0 {
used = read_u64(&device_dir.join("tile0").join("vram0").join("used_bytes")).unwrap_or(0);
}
if total == 0 {
total = read_resource2_total_bytes(device_dir);
}
(used, total)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_resource2_total_bytes(device_dir: &Path) -> u64 {
const BAR2_APERTURE_BYTES: u64 = 256 * 1024 * 1024;
let path = device_dir.join("resource2");
match std::fs::metadata(&path) {
Ok(meta) => {
let size = meta.len();
if size > BAR2_APERTURE_BYTES { size } else { 0 }
}
Err(_) => 0,
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_frequency_mhz(device_dir: &Path) -> u32 {
if let Some(mhz) = read_u32(&device_dir.join("gt_cur_freq_mhz")) {
return mhz;
}
if let Some(raw) = read_u64(
&device_dir
.join("tile0")
.join("gt0")
.join("freq0")
.join("cur_freq"),
) {
let mhz = if raw > 100_000 { raw / 1_000_000 } else { raw };
if mhz <= u64::from(u32::MAX) {
return mhz as u32;
}
}
0
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_temperature_celsius(device_dir: &Path) -> u32 {
let hwmon_root = device_dir.join("hwmon");
let iter = match std::fs::read_dir(&hwmon_root) {
Ok(i) => i,
Err(_) => return 0,
};
let mut hwmon_paths: Vec<_> = iter.flatten().map(|entry| entry.path()).collect();
hwmon_paths.sort_unstable();
for idx in 1..=21 {
let input_name = format!("temp{idx}_input");
for hwmon_path in &hwmon_paths {
if let Some(milli) = read_u64(&hwmon_path.join(&input_name)) {
return u32::try_from(milli / 1000).unwrap_or(u32::MAX);
}
}
}
0
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_power_watts(device_dir: &Path) -> f64 {
let hwmon_root = device_dir.join("hwmon");
let iter = match std::fs::read_dir(&hwmon_root) {
Ok(i) => i,
Err(_) => return 0.0,
};
for entry in iter.flatten() {
if let Some(uw) = read_u64(&entry.path().join("power1_average")) {
return uw as f64 / 1_000_000.0;
}
}
0.0
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_energy_uj(device_dir: &Path) -> u64 {
let hwmon_root = device_dir.join("hwmon");
let iter = match std::fs::read_dir(&hwmon_root) {
Ok(i) => i,
Err(_) => return 0,
};
for entry in iter.flatten() {
if let Some(uj) = read_u64(&entry.path().join("energy1_input")) {
return uj;
}
}
0
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_fan_rpm(device_dir: &Path) -> Option<u32> {
let hwmon_root = device_dir.join("hwmon");
let iter = std::fs::read_dir(&hwmon_root).ok()?;
for entry in iter.flatten() {
if let Some(rpm) = read_u32(&entry.path().join("fan1_input"))
&& rpm > 0
{
return Some(rpm);
}
}
None
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_u64(path: &Path) -> Option<u64> {
std::fs::read_to_string(path)
.ok()?
.trim()
.parse::<u64>()
.ok()
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_u32(path: &Path) -> Option<u32> {
std::fs::read_to_string(path)
.ok()?
.trim()
.parse::<u32>()
.ok()
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn has_nonzero_u64(path: &Path) -> bool {
read_u64(path).map(|v| v > 0).unwrap_or(false)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn read_gtidle_ms(device_dir: &Path) -> [Option<u64>; 2] {
std::array::from_fn(|gt| {
let path = device_dir
.join("tile0")
.join(format!("gt{gt}"))
.join("gtidle")
.join("idle_residency_ms");
read_u64(&path)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn read_memory_integrated_returns_zero() {
let dir = tempdir().unwrap();
let (used, total) = read_memory_bytes(dir.path(), MemoryVariant::Integrated);
assert_eq!(used, 0);
assert_eq!(total, 0);
}
#[test]
fn read_memory_discrete_via_i915() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("mem_info_vram_total"), "17179869184\n").unwrap();
fs::write(dir.path().join("mem_info_vram_used"), "1073741824\n").unwrap();
let (used, total) = read_memory_bytes(dir.path(), MemoryVariant::Discrete);
assert_eq!(total, 17_179_869_184);
assert_eq!(used, 1_073_741_824);
}
#[test]
fn read_memory_discrete_via_xe() {
let dir = tempdir().unwrap();
let xe = dir.path().join("tile0").join("vram0");
fs::create_dir_all(&xe).unwrap();
fs::write(xe.join("total_bytes"), "12884901888\n").unwrap();
fs::write(xe.join("used_bytes"), "536870912\n").unwrap();
let (used, total) = read_memory_bytes(dir.path(), MemoryVariant::Discrete);
assert_eq!(total, 12_884_901_888);
assert_eq!(used, 536_870_912);
}
#[test]
fn read_memory_discrete_via_resource2_rebar() {
let dir = tempdir().unwrap();
let bar2 = fs::File::create(dir.path().join("resource2")).unwrap();
bar2.set_len(12 * 1024 * 1024 * 1024).unwrap();
let (used, total) = read_memory_bytes(dir.path(), MemoryVariant::Discrete);
assert_eq!(total, 12 * 1024 * 1024 * 1024);
assert_eq!(used, 0);
}
#[test]
fn resource2_rejects_256mib_aperture() {
let dir = tempdir().unwrap();
let bar2 = fs::File::create(dir.path().join("resource2")).unwrap();
bar2.set_len(256 * 1024 * 1024).unwrap();
assert_eq!(read_resource2_total_bytes(dir.path()), 0);
}
#[test]
fn resource2_missing_returns_zero() {
let dir = tempdir().unwrap();
assert_eq!(read_resource2_total_bytes(dir.path()), 0);
}
#[test]
fn read_energy_uj_reads_hwmon_counter() {
let dir = tempdir().unwrap();
let hwmon = dir.path().join("hwmon").join("hwmon0");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("energy1_input"), "123456789\n").unwrap();
assert_eq!(read_energy_uj(dir.path()), 123_456_789);
}
#[test]
fn read_energy_uj_missing_returns_zero() {
let dir = tempdir().unwrap();
assert_eq!(read_energy_uj(dir.path()), 0);
}
#[test]
fn frequency_prefers_i915_path() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("gt_cur_freq_mhz"), "2100\n").unwrap();
assert_eq!(read_frequency_mhz(dir.path()), 2100);
}
#[test]
fn frequency_xe_hz_path() {
let dir = tempdir().unwrap();
let freq = dir.path().join("tile0").join("gt0").join("freq0");
fs::create_dir_all(&freq).unwrap();
fs::write(freq.join("cur_freq"), "2500000000\n").unwrap();
assert_eq!(read_frequency_mhz(dir.path()), 2500);
}
#[test]
fn frequency_xe_mhz_path() {
let dir = tempdir().unwrap();
let freq = dir.path().join("tile0").join("gt0").join("freq0");
fs::create_dir_all(&freq).unwrap();
fs::write(freq.join("cur_freq"), "2300\n").unwrap();
assert_eq!(read_frequency_mhz(dir.path()), 2300);
}
#[test]
fn temperature_reads_temp1_first() {
let dir = tempdir().unwrap();
let hwmon = dir.path().join("hwmon").join("hwmon3");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("temp1_input"), "72500\n").unwrap();
assert_eq!(read_temperature_celsius(dir.path()), 72);
}
#[test]
fn temperature_falls_back_to_temp2() {
let dir = tempdir().unwrap();
let hwmon = dir.path().join("hwmon").join("hwmon1");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("temp2_input"), "69000\n").unwrap();
assert_eq!(read_temperature_celsius(dir.path()), 69);
}
#[test]
fn temperature_prefers_lower_channel_across_hwmon_directories() {
let dir = tempdir().unwrap();
let hwmon_root = dir.path().join("hwmon");
let first = hwmon_root.join("hwmon1");
let second = hwmon_root.join("hwmon2");
fs::create_dir_all(&first).unwrap();
fs::create_dir_all(&second).unwrap();
fs::write(first.join("temp2_input"), "69000\n").unwrap();
fs::write(second.join("temp1_input"), "72000\n").unwrap();
assert_eq!(read_temperature_celsius(dir.path()), 72);
}
#[test]
fn temperature_scans_full_xe_channel_range() {
let dir = tempdir().unwrap();
let hwmon = dir.path().join("hwmon").join("hwmon1");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("temp21_input"), "61000\n").unwrap();
assert_eq!(read_temperature_celsius(dir.path()), 61);
}
#[test]
fn temperature_saturates_before_narrowing_to_u32() {
let dir = tempdir().unwrap();
let hwmon = dir.path().join("hwmon").join("hwmon1");
fs::create_dir_all(&hwmon).unwrap();
let overflowing_milli = (u64::from(u32::MAX) + 1) * 1000;
fs::write(hwmon.join("temp1_input"), format!("{overflowing_milli}\n")).unwrap();
assert_eq!(read_temperature_celsius(dir.path()), u32::MAX);
}
#[test]
fn temperature_missing_returns_zero() {
let dir = tempdir().unwrap();
assert_eq!(read_temperature_celsius(dir.path()), 0);
}
#[test]
fn power_handles_microwatts() {
let dir = tempdir().unwrap();
let hwmon = dir.path().join("hwmon").join("hwmon2");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("power1_average"), "185500000\n").unwrap();
let w = read_power_watts(dir.path());
assert!((w - 185.5).abs() < 0.01, "got {w}");
}
#[test]
fn fan_rpm_handles_hwmon() {
let dir = tempdir().unwrap();
let hwmon = dir.path().join("hwmon").join("hwmon2");
fs::create_dir_all(&hwmon).unwrap();
fs::write(hwmon.join("fan1_input"), "1730\n").unwrap();
assert_eq!(read_fan_rpm(dir.path()), Some(1730));
}
#[test]
fn has_nonzero_u64_works() {
let dir = tempdir().unwrap();
let f = dir.path().join("x");
fs::write(&f, "0\n").unwrap();
assert!(!has_nonzero_u64(&f));
fs::write(&f, "42\n").unwrap();
assert!(has_nonzero_u64(&f));
assert!(!has_nonzero_u64(&dir.path().join("missing")));
}
#[test]
fn gtidle_readings_preserve_gt_identity() {
let dir = tempdir().unwrap();
let gt1 = dir.path().join("tile0").join("gt1").join("gtidle");
fs::create_dir_all(>1).unwrap();
fs::write(gt1.join("idle_residency_ms"), "42\n").unwrap();
assert_eq!(read_gtidle_ms(dir.path()), [None, Some(42)]);
}
}