use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, PoisonError};
use std::time::Instant;
use ruda_core::ir::PciAddress;
use crate::runtime::monitoring::{DeviceUtilization, UtilizationUnavailable};
#[derive(Debug)]
pub struct IntelIdleResidencyFiles {
idle_residency_file_per_gt: Vec<PathBuf>,
previous_reading: Mutex<Option<IdleResidencyReading>>,
}
#[derive(Debug)]
struct IdleResidencyReading {
taken_at: Instant,
idle_milliseconds_per_gt: Vec<u64>,
}
impl IntelIdleResidencyFiles {
pub fn new(pci_address: PciAddress) -> Result<Self, UtilizationUnavailable> {
let device_directory = PathBuf::from(format!("/sys/bus/pci/devices/{pci_address}"));
let mut idle_residency_file_per_gt = Self::i915_rc6_residency_files(&device_directory);
idle_residency_file_per_gt.extend(Self::xe_idle_residency_files(&device_directory));
if idle_residency_file_per_gt.is_empty() {
return Err(UtilizationUnavailable::QueryFailed(format!(
"{}: holds neither i915's rc6_residency_ms nor xe's idle_residency_ms",
device_directory.display()
)));
}
Ok(Self {
idle_residency_file_per_gt,
previous_reading: Mutex::new(None),
})
}
pub fn read(&self) -> Result<DeviceUtilization, UtilizationUnavailable> {
let reading = self
.read_idle_residency()
.map_err(UtilizationUnavailable::QueryFailed)?;
let mut previous_reading = self
.previous_reading
.lock()
.unwrap_or_else(PoisonError::into_inner);
let busy_percent = previous_reading
.as_ref()
.and_then(|earlier| reading.busy_percent_of_busiest_gt_since(earlier));
*previous_reading = Some(reading);
busy_percent
.map(DeviceUtilization::new)
.ok_or(UtilizationUnavailable::NoPreviousReading)
}
fn read_idle_residency(&self) -> Result<IdleResidencyReading, String> {
let idle_milliseconds_per_gt = self
.idle_residency_file_per_gt
.iter()
.map(|path| {
std::fs::read_to_string(path)
.map_err(|error| error.to_string())
.and_then(|text| {
text.trim()
.parse::<u64>()
.map_err(|error| error.to_string())
})
.map_err(|message| format!("{}: {message}", path.display()))
})
.collect::<Result<Vec<_>, _>>()?;
Ok(IdleResidencyReading {
taken_at: Instant::now(),
idle_milliseconds_per_gt,
})
}
fn i915_rc6_residency_files(device_directory: &Path) -> Vec<PathBuf> {
let cards = Self::numbered_entries(&device_directory.join("drm"), "card");
let per_gt: Vec<PathBuf> = cards
.iter()
.flat_map(|card| Self::numbered_entries(&card.join("gt"), "gt"))
.map(|gt| gt.join("rc6_residency_ms"))
.filter(|path| path.exists())
.collect();
if !per_gt.is_empty() {
return per_gt;
}
cards
.iter()
.map(|card| card.join("power/rc6_residency_ms"))
.filter(|path| path.exists())
.collect()
}
fn xe_idle_residency_files(device_directory: &Path) -> Vec<PathBuf> {
Self::numbered_entries(device_directory, "tile")
.iter()
.flat_map(|tile| Self::numbered_entries(tile, "gt"))
.map(|gt| gt.join("gtidle/idle_residency_ms"))
.filter(|path| path.exists())
.collect()
}
fn numbered_entries(directory: &Path, prefix: &str) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(directory) else {
return Vec::new();
};
entries
.filter_map(Result::ok)
.filter(|entry| {
entry
.file_name()
.to_str()
.and_then(|name| name.strip_prefix(prefix))
.is_some_and(|number| {
!number.is_empty() && number.bytes().all(|byte| byte.is_ascii_digit())
})
})
.map(|entry| entry.path())
.collect()
}
}
impl IdleResidencyReading {
fn busy_percent_of_busiest_gt_since(&self, earlier: &Self) -> Option<f32> {
let elapsed_milliseconds =
self.taken_at.duration_since(earlier.taken_at).as_secs_f64() * 1000.0;
let mut busiest_share: f64 = 0.0;
for (now, then) in self
.idle_milliseconds_per_gt
.iter()
.zip(&earlier.idle_milliseconds_per_gt)
{
let idle_milliseconds = now.checked_sub(*then)?;
busiest_share =
busiest_share.max(1.0 - idle_milliseconds as f64 / elapsed_milliseconds);
}
Some((busiest_share * 100.0) as f32)
}
}