use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
#[path = "intel_gpu_fdinfo/io.rs"]
mod io;
use io::read_fdinfo_to_string;
const MAX_GPU_PROCESSES: usize = 4096;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FdInfo {
pub drm_driver: String,
pub drm_pdev: Option<String>,
pub drm_client_id: Option<u64>,
pub resident_bytes: u64,
pub resident_vram_bytes: u64,
}
pub fn parse_fdinfo(content: &str) -> Option<FdInfo> {
let mut drm_driver: Option<String> = None;
let mut drm_pdev: Option<String> = None;
let mut drm_client_id: Option<u64> = None;
let mut resident_bytes: u64 = 0;
let mut resident_vram_bytes: u64 = 0;
for raw_line in content.lines() {
let line = raw_line.trim();
if line.is_empty() {
continue;
}
let Some((key, value)) = line.split_once(':') else {
continue;
};
let key = key.trim();
let value = value.trim();
match key {
"drm-driver" => drm_driver = Some(value.to_string()),
"drm-pdev" => drm_pdev = Some(value.to_string()),
"drm-client-id" => drm_client_id = value.parse::<u64>().ok(),
k if k.starts_with("drm-resident-") => {
if let Some(bytes) = parse_memory_value(value) {
resident_bytes = resident_bytes.saturating_add(bytes);
if k.starts_with("drm-resident-vram") || k.starts_with("drm-resident-local") {
resident_vram_bytes = resident_vram_bytes.saturating_add(bytes);
}
}
}
_ => {}
}
}
let drm_driver = drm_driver?;
if drm_driver != "i915" && drm_driver != "xe" {
return None;
}
Some(FdInfo {
drm_driver,
drm_pdev,
drm_client_id,
resident_bytes,
resident_vram_bytes,
})
}
fn parse_memory_value(value: &str) -> Option<u64> {
let mut tokens = value.split_whitespace();
let number: u64 = tokens.next()?.parse().ok()?;
let unit = tokens.next().unwrap_or("");
let multiplier = match unit.to_ascii_lowercase().as_str() {
"kb" | "kib" => 1024,
"mb" | "mib" => 1024 * 1024,
"gb" | "gib" => 1024 * 1024 * 1024,
"b" | "" => 1,
_ => return None,
};
Some(number.saturating_mul(multiplier))
}
pub fn build_intel_drm_basenames(
intel_cards: &[(PathBuf, usize)],
drm_root: &Path,
) -> HashMap<String, usize> {
let mut basenames: HashMap<String, usize> = HashMap::new();
let mut pci_to_index: HashMap<String, usize> = HashMap::new();
for (card_path, idx) in intel_cards {
if let Some(basename) = card_path.file_name().and_then(|n| n.to_str()) {
basenames.insert(basename.to_string(), *idx);
}
if let Some(bus) = pci_bus_for_drm_node(card_path) {
pci_to_index.insert(bus, *idx);
}
}
let Ok(entries) = std::fs::read_dir(drm_root) else {
return basenames;
};
for entry in entries.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !is_card_node(name) && !is_render_node(name) {
continue;
}
if basenames.contains_key(name) {
continue;
}
let Some(bus) = pci_bus_for_drm_node(&path) else {
continue;
};
if let Some(idx) = pci_to_index.get(&bus) {
basenames.insert(name.to_string(), *idx);
}
}
basenames
}
fn pci_bus_for_drm_node(drm_node: &Path) -> Option<String> {
let link = std::fs::read_link(drm_node.join("device")).ok()?;
link.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
}
fn is_card_node(name: &str) -> bool {
if let Some(rest) = name.strip_prefix("card") {
!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
} else {
false
}
}
fn is_render_node(name: &str) -> bool {
if let Some(rest) = name.strip_prefix("renderD") {
!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
} else {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IntelDrmFd {
pub fd_num: u32,
pub fdinfo_path: PathBuf,
pub card_index: usize,
}
pub fn intel_drm_fds_for_pid(
pid: u32,
intel_drm_basenames: &HashMap<String, usize>,
proc_root: &Path,
) -> Vec<IntelDrmFd> {
let fd_dir = proc_root.join(pid.to_string()).join("fd");
let Ok(entries) = std::fs::read_dir(&fd_dir) else {
return Vec::new();
};
let mut out: Vec<IntelDrmFd> = Vec::new();
for entry in entries.flatten() {
let fd_path = entry.path();
let Some(fd_name) = fd_path.file_name().and_then(|n| n.to_str()) else {
continue;
};
let Ok(fd_num) = fd_name.parse::<u32>() else {
continue;
};
let Ok(target) = std::fs::read_link(&fd_path) else {
continue;
};
let Some(target_name) = target.file_name().and_then(|n| n.to_str()) else {
continue;
};
let Some(card_index) = intel_drm_basenames.get(target_name).copied() else {
continue;
};
let fdinfo_path = proc_root.join(pid.to_string()).join("fdinfo").join(fd_name);
out.push(IntelDrmFd {
fd_num,
fdinfo_path,
card_index,
});
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GpuProcessUsage {
pub pid: u32,
pub card_index: usize,
pub used_memory_bytes: u64,
}
pub fn collect_intel_gpu_processes(
intel_drm_basenames: &HashMap<String, usize>,
proc_root: &Path,
) -> Vec<GpuProcessUsage> {
if intel_drm_basenames.is_empty() {
return Vec::new();
}
let Ok(entries) = std::fs::read_dir(proc_root) else {
return Vec::new();
};
type Key = (u32, usize);
let mut per_client: HashMap<Key, HashMap<u64, u64>> = HashMap::new();
let mut no_client_id: HashMap<Key, HashMap<PathBuf, u64>> = HashMap::new();
let mut process_count: usize = 0;
for entry in entries.flatten() {
if process_count >= MAX_GPU_PROCESSES {
break;
}
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
let Ok(pid) = name.parse::<u32>() else {
continue;
};
let fds = intel_drm_fds_for_pid(pid, intel_drm_basenames, proc_root);
if fds.is_empty() {
continue;
}
process_count += 1;
for fd in fds {
let Some(content) = read_fdinfo_to_string(&fd.fdinfo_path) else {
continue; };
let Some(info) = parse_fdinfo(&content) else {
continue;
};
let key = (pid, fd.card_index);
match info.drm_client_id {
Some(cid) => {
let entry = per_client.entry(key).or_default().entry(cid).or_insert(0);
if info.resident_bytes > *entry {
*entry = info.resident_bytes;
}
}
None => {
let entry = no_client_id
.entry(key)
.or_default()
.entry(fd.fdinfo_path)
.or_insert(0);
if info.resident_bytes > *entry {
*entry = info.resident_bytes;
}
}
}
}
}
let mut keys: HashSet<Key> = HashSet::new();
for k in per_client.keys() {
keys.insert(*k);
}
for k in no_client_id.keys() {
keys.insert(*k);
}
let mut out: Vec<GpuProcessUsage> = Vec::with_capacity(keys.len());
for (pid, card_index) in keys {
let mut total: u64 = 0;
if let Some(by_client) = per_client.get(&(pid, card_index)) {
for bytes in by_client.values() {
total = total.saturating_add(*bytes);
}
}
if let Some(by_fd) = no_client_id.get(&(pid, card_index)) {
for bytes in by_fd.values() {
total = total.saturating_add(*bytes);
}
}
out.push(GpuProcessUsage {
pid,
card_index,
used_memory_bytes: total,
});
}
out.sort_by_key(|u| (u.pid, u.card_index));
out
}
pub fn sum_vram_by_card_from_fdinfo(
intel_drm_basenames: &HashMap<String, usize>,
proc_root: &Path,
) -> HashMap<usize, u64> {
let mut out: HashMap<usize, u64> = HashMap::new();
if intel_drm_basenames.is_empty() {
return out;
}
let Ok(entries) = std::fs::read_dir(proc_root) else {
return out;
};
let mut per_client: HashMap<usize, HashMap<u64, u64>> = HashMap::new();
let mut no_client_id: HashMap<usize, HashMap<PathBuf, u64>> = HashMap::new();
let mut process_count: usize = 0;
for entry in entries.flatten() {
if process_count >= MAX_GPU_PROCESSES {
break;
}
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
let Ok(pid) = name.parse::<u32>() else {
continue;
};
let fds = intel_drm_fds_for_pid(pid, intel_drm_basenames, proc_root);
if fds.is_empty() {
continue;
}
process_count += 1;
for fd in fds {
let Some(content) = read_fdinfo_to_string(&fd.fdinfo_path) else {
continue;
};
let Some(info) = parse_fdinfo(&content) else {
continue;
};
match info.drm_client_id {
Some(cid) => {
let entry = per_client
.entry(fd.card_index)
.or_default()
.entry(cid)
.or_insert(0);
*entry = (*entry).max(info.resident_vram_bytes);
}
None => {
let entry = no_client_id
.entry(fd.card_index)
.or_default()
.entry(fd.fdinfo_path)
.or_insert(0);
*entry = (*entry).max(info.resident_vram_bytes);
}
}
}
}
for (card_index, by_client) in &per_client {
let slot = out.entry(*card_index).or_insert(0);
for bytes in by_client.values() {
*slot = slot.saturating_add(*bytes);
}
}
for (card_index, by_fd) in &no_client_id {
let slot = out.entry(*card_index).or_insert(0);
for bytes in by_fd.values() {
*slot = slot.saturating_add(*bytes);
}
}
out
}
#[path = "intel_gpu_fdinfo/enrichment.rs"]
mod enrichment;
pub use enrichment::build_intel_process_infos;
#[cfg(test)]
#[path = "intel_gpu_fdinfo/tests.rs"]
mod tests;