use crate::device::readers::intel_gpu_sysfs::read_u64;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::Instant;
#[path = "intel_gpu_engine/discovery.rs"]
mod discovery;
pub use discovery::discover_engine_counters;
#[cfg(test)]
#[path = "intel_gpu_engine/tests.rs"]
mod tests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EngineCounter {
pub class: &'static str,
pub instance: String,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct EngineSample {
pub counter: EngineCounter,
pub last_busy_ns: u64,
pub last_busy_pct: f64,
}
pub struct EngineState {
pub samples: Vec<EngineSample>,
pub last_tick: Option<Instant>,
pub discovery_attempted: bool,
now_fn: fn() -> Instant,
}
impl std::fmt::Debug for EngineState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EngineState")
.field("samples", &self.samples)
.field("last_tick", &self.last_tick)
.field("discovery_attempted", &self.discovery_attempted)
.finish_non_exhaustive()
}
}
impl EngineState {
pub fn empty() -> Self {
Self {
samples: Vec::new(),
last_tick: None,
discovery_attempted: false,
now_fn: Instant::now,
}
}
#[cfg(test)]
pub fn with_clock(now_fn: fn() -> Instant) -> Self {
Self {
samples: Vec::new(),
last_tick: None,
discovery_attempted: false,
now_fn,
}
}
}
#[derive(Debug, Clone)]
pub struct EngineReadout {
pub primary_utilization: f64,
pub per_class: Vec<(&'static str, f64)>,
pub status_note: Option<&'static str>,
}
pub const ENGINE_UNAVAILABLE_NOTE: &str =
"Engine counters unavailable (kernel does not expose engine busy)";
pub const ENGINE_SEEDING_NOTE: &str = "Engine counters seeded (utilization available next refresh)";
pub fn refresh(state: &mut EngineState, device_dir: &Path) -> EngineReadout {
if !state.discovery_attempted {
let counters = discover_engine_counters(device_dir);
state.samples = counters
.into_iter()
.map(|counter| EngineSample {
counter,
last_busy_ns: 0,
last_busy_pct: 0.0,
})
.collect();
state.discovery_attempted = true;
}
if state.samples.is_empty() {
return EngineReadout {
primary_utilization: 0.0,
per_class: Vec::new(),
status_note: Some(ENGINE_UNAVAILABLE_NOTE),
};
}
let now = (state.now_fn)();
let prev_tick = state.last_tick;
let current_values: Vec<Option<u64>> = state
.samples
.iter()
.map(|s| read_u64(&s.counter.path))
.collect();
let Some(prev) = prev_tick else {
for (sample, value) in state.samples.iter_mut().zip(current_values.iter()) {
if let Some(v) = value {
sample.last_busy_ns = *v;
}
sample.last_busy_pct = 0.0;
}
state.last_tick = Some(now);
return EngineReadout {
primary_utilization: 0.0,
per_class: Vec::new(),
status_note: Some(ENGINE_SEEDING_NOTE),
};
};
let delta_wall_ns = now.saturating_duration_since(prev).as_nanos();
if delta_wall_ns == 0 {
return EngineReadout {
primary_utilization: 0.0,
per_class: Vec::new(),
status_note: None,
};
}
let delta_wall_f = delta_wall_ns as f64;
for (sample, value) in state.samples.iter_mut().zip(current_values.iter()) {
let Some(current) = *value else {
continue;
};
let delta_busy = current.saturating_sub(sample.last_busy_ns);
let pct = ((delta_busy as f64) / delta_wall_f * 100.0).clamp(0.0, 100.0);
sample.last_busy_pct = pct;
sample.last_busy_ns = current;
}
state.last_tick = Some(now);
let mut per_class: Vec<(&'static str, f64)> = Vec::new();
for sample in &state.samples {
let class = sample.counter.class;
if let Some(entry) = per_class.iter_mut().find(|(c, _)| *c == class) {
if sample.last_busy_pct > entry.1 {
entry.1 = sample.last_busy_pct;
}
} else {
per_class.push((class, sample.last_busy_pct));
}
}
let render_or_compute = per_class
.iter()
.filter(|(c, _)| *c == "render" || *c == "compute")
.map(|(_, pct)| *pct)
.fold(f64::NEG_INFINITY, f64::max);
let primary = if render_or_compute.is_finite() {
render_or_compute
} else {
per_class
.iter()
.map(|(_, pct)| *pct)
.fold(0.0_f64, f64::max)
};
per_class.sort_by(|a, b| class_order(a.0).cmp(&class_order(b.0)).then(a.0.cmp(b.0)));
EngineReadout {
primary_utilization: primary,
per_class,
status_note: None,
}
}
pub fn refresh_with_lock(state: &Mutex<EngineState>, device_dir: &Path) -> EngineReadout {
let mut guard = match state.lock() {
Ok(g) => g,
Err(poisoned) => {
eprintln!(
"Warning: Intel GPU engine-state mutex was poisoned for {}, recovering...",
device_dir.display()
);
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| poisoned.into_inner())) {
Ok(mut g) => {
*g = EngineState::empty();
g
}
Err(_) => {
eprintln!(
"Critical: failed to recover engine-state mutex for {}, returning unavailable",
device_dir.display()
);
return EngineReadout {
primary_utilization: 0.0,
per_class: Vec::new(),
status_note: Some(ENGINE_UNAVAILABLE_NOTE),
};
}
}
}
};
refresh(&mut guard, device_dir)
}
pub fn apply_engine_readout(detail: &mut HashMap<String, String>, readout: &EngineReadout) {
if let Some(note) = readout.status_note {
detail.insert("Utilization".to_string(), note.to_string());
} else {
detail.remove("Utilization");
}
for (class, pct) in &readout.per_class {
let key = format!("Engine: {class}");
detail.insert(key, format!("{pct:.2}%"));
}
}
fn class_order(class: &str) -> u8 {
match class {
"render" => 0,
"compute" => 1,
_ => 2,
}
}