mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-09-24 03:25:34 +00:00
feature: initial Intel GPU support to get process usage (#2239)
This adds very basic GPU support for getting process usage for Intel GPUs on Linux. Note this PR does not handle memory/VRAM usage for now. The implementation is basically a clone of the AMD GPU one.
This commit is contained in:
+15
-2
@@ -449,12 +449,25 @@ impl DataCollector {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(data) =
|
||||
amd::get_amd_vecs(&self.widgets_to_harvest, self.last_collection_time)
|
||||
amd::get_amd_gpu_data(&self.widgets_to_harvest, self.last_collection_time)
|
||||
{
|
||||
if let Some(mut mem) = data.memory {
|
||||
local_gpu.append(&mut mem);
|
||||
}
|
||||
if let Some(mut proc) = data.procs {
|
||||
if let Some(mut proc) = data.process_data {
|
||||
local_gpu_pids.append(&mut proc.1);
|
||||
local_gpu_total_mem += proc.0;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Some(data) =
|
||||
intel::get_intel_gpu_data(&self.widgets_to_harvest, self.last_collection_time)
|
||||
{
|
||||
if let Some(mut mem) = data.memory {
|
||||
local_gpu.append(&mut mem);
|
||||
}
|
||||
if let Some(mut proc) = data.process_data {
|
||||
local_gpu_pids.append(&mut proc.1);
|
||||
local_gpu_total_mem += proc.0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#[cfg(feature = "nvidia")]
|
||||
pub mod nvidia;
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "gpu"))]
|
||||
pub mod amd;
|
||||
cfg_select! {
|
||||
all(target_os = "linux", feature = "gpu") =>{
|
||||
pub mod intel;
|
||||
pub mod amd;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! A collection of functions/utilities to get AMD GPU data.
|
||||
//!
|
||||
//! Note this currently only works on Linux.
|
||||
|
||||
mod amd_gpu_marketing;
|
||||
|
||||
use std::{
|
||||
@@ -20,13 +24,16 @@ use crate::{
|
||||
utils::int_hash::{IntHashMap, IntHashSet},
|
||||
};
|
||||
|
||||
// TODO: May be able to clean up some of these, Option<Vec> for example is a bit
|
||||
// redundant.
|
||||
/// AMD GPU data.
|
||||
///
|
||||
/// TODO: May be able to clean up some of these, Option<Vec> for example is a bit
|
||||
/// redundant.
|
||||
pub struct AmdGpuData {
|
||||
pub memory: Option<Vec<(String, MemData)>>,
|
||||
pub procs: Option<(u64, Vec<IntHashMap<Pid, (u64, u32)>>)>,
|
||||
pub process_data: Option<(u64, Vec<IntHashMap<Pid, (u64, u32)>>)>,
|
||||
}
|
||||
|
||||
/// AMD GPU VRAM usage.
|
||||
pub struct AmdGpuMemory {
|
||||
pub total: u64,
|
||||
pub used: u64,
|
||||
@@ -89,6 +96,7 @@ pub fn get_amd_name(device_path: &Path) -> Option<String> {
|
||||
.map(|tuple| tuple.2.to_string())
|
||||
}
|
||||
|
||||
/// VRAM used for a device.
|
||||
fn get_amd_vram(device_path: &Path) -> Option<AmdGpuMemory> {
|
||||
// get vram memory info from sysfs
|
||||
let vram_total_path = device_path.join("mem_info_vram_total");
|
||||
@@ -138,20 +146,23 @@ fn get_amd_fdinfo(device_path: &Path) -> Option<IntHashMap<Pid, AmdGpuProc>> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_amd_vecs(widgets_to_harvest: &UsedWidgets, prev_time: Instant) -> Option<AmdGpuData> {
|
||||
let device_path_list = enumerate_drm_devices("amdgpu")?;
|
||||
pub fn get_amd_gpu_data(
|
||||
widgets_to_harvest: &UsedWidgets, prev_time: Instant,
|
||||
) -> Option<AmdGpuData> {
|
||||
// TODO: Add caching for this.
|
||||
let device_paths = enumerate_drm_devices("amdgpu")?;
|
||||
let interval = Instant::now().duration_since(prev_time);
|
||||
let num_gpu = device_path_list.len();
|
||||
let num_gpu = device_paths.len();
|
||||
let mut mem_vec = Vec::with_capacity(num_gpu);
|
||||
let mut proc_vec = Vec::with_capacity(num_gpu);
|
||||
let mut total_mem = 0;
|
||||
|
||||
PREV_PROC_DATA.with_borrow_mut(|prev_proc_data| {
|
||||
let device_path_set = device_path_list.iter().cloned().collect::<HashSet<_>>();
|
||||
let device_path_set = device_paths.iter().cloned().collect::<HashSet<_>>();
|
||||
prev_proc_data.retain(|k, _| device_path_set.contains(k));
|
||||
});
|
||||
|
||||
for device_path in device_path_list {
|
||||
for device_path in device_paths {
|
||||
let device_name = get_amd_name(&device_path)
|
||||
.unwrap_or(amd_gpu_marketing::AMDGPU_DEFAULT_NAME.to_string());
|
||||
|
||||
@@ -247,6 +258,6 @@ pub fn get_amd_vecs(widgets_to_harvest: &UsedWidgets, prev_time: Instant) -> Opt
|
||||
|
||||
Some(AmdGpuData {
|
||||
memory: (!mem_vec.is_empty()).then_some(mem_vec),
|
||||
procs: (!proc_vec.is_empty()).then_some((total_mem, proc_vec)),
|
||||
process_data: (!proc_vec.is_empty()).then_some((total_mem, proc_vec)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
//! A collection of functions/utilities to get Intel GPU data.
|
||||
//!
|
||||
//! Note this currently only works on Linux. For Linux, we support both the `i915` and `xe` DRM drivers.
|
||||
//! For more info, see <https://docs.kernel.org/gpu/drm-usage-stats.html> for info on the fdinfo format.
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
num::NonZeroU64,
|
||||
path::{Path, PathBuf},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||
|
||||
use crate::{
|
||||
app::layout_manager::UsedWidgets,
|
||||
collection::{
|
||||
linux::drm::{collect_drm_fdinfo, diff_usage, enumerate_drm_devices, get_drm_render_nodes},
|
||||
memory::MemData,
|
||||
processes::Pid,
|
||||
},
|
||||
utils::int_hash::{IntHashMap, IntHashSet},
|
||||
};
|
||||
|
||||
/// Which DRM driver backs a given Intel device.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum IntelDriver {
|
||||
I915,
|
||||
Xe,
|
||||
}
|
||||
|
||||
impl IntelDriver {
|
||||
/// The kernel module name, i.e. the `/sys/module/<name>` directory.
|
||||
const fn module(self) -> &'static str {
|
||||
match self {
|
||||
IntelDriver::I915 => "i915",
|
||||
IntelDriver::Xe => "xe",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Intel GPU data.
|
||||
///
|
||||
/// TODO: May be able to clean up some of these, Option<Vec> for example is a bit
|
||||
/// redundant.
|
||||
pub struct IntelGpuData {
|
||||
pub memory: Option<Vec<(String, MemData)>>,
|
||||
pub process_data: Option<(u64, Vec<IntHashMap<Pid, (u64, u32)>>)>,
|
||||
}
|
||||
|
||||
/// Intel GPU VRAM usage.
|
||||
pub struct IntelGpuMemory {
|
||||
pub total: u64,
|
||||
pub used: u64,
|
||||
}
|
||||
|
||||
/// Per-process accumulator. It holds fields for *both* drivers; only the subset relevant to the
|
||||
/// device's driver is ever populated (see [`get_intel_fdinfo`]).
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
struct IntelGpuProc {
|
||||
// i915: per-engine busy time, in nanoseconds (accumulating counters).
|
||||
render_usage: u64,
|
||||
copy_usage: u64,
|
||||
video_usage: u64,
|
||||
video_enhance_usage: u64,
|
||||
compute_usage: u64,
|
||||
// xe: busy + elapsed cycle counters, summed across all engines.
|
||||
cycles: u64,
|
||||
total_cycles: u64,
|
||||
// Per-process GPU memory, in bytes.
|
||||
mem_used: u64,
|
||||
}
|
||||
|
||||
// TODO: This is kind of a hack.
|
||||
thread_local! {
|
||||
static PREV_PROC_DATA: RefCell<HashMap<PathBuf, IntHashMap<Pid, IntelGpuProc>>> = RefCell::new(HashMap::default());
|
||||
static LAST_CLEAN_COUNTER: RefCell<u32> = const { RefCell::new(0) };
|
||||
}
|
||||
|
||||
/// Apparently there's no marketing name to grab...?
|
||||
///
|
||||
/// TODO: Investigate this properly.
|
||||
fn get_intel_name(num_gpu: usize, idx: usize) -> String {
|
||||
const INTEL_DEFAULT_NAME: &str = "Intel GPU";
|
||||
|
||||
if num_gpu > 1 {
|
||||
format!("{INTEL_DEFAULT_NAME} {idx}")
|
||||
} else {
|
||||
INTEL_DEFAULT_NAME.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// VRAM used for a device.
|
||||
fn get_intel_vram(_device_path: &Path, _driver: IntelDriver) -> Option<IntelGpuMemory> {
|
||||
// TODO: Not yet implemented.
|
||||
None
|
||||
}
|
||||
|
||||
fn get_intel_fdinfo(
|
||||
device_path: &Path, driver: IntelDriver,
|
||||
) -> Option<IntHashMap<Pid, IntelGpuProc>> {
|
||||
let drm_paths = get_drm_render_nodes(device_path)?;
|
||||
|
||||
match driver {
|
||||
IntelDriver::I915 => {
|
||||
collect_drm_fdinfo(&drm_paths, |usage: &mut IntelGpuProc, (keyword, value)| {
|
||||
match keyword {
|
||||
"drm-engine-render" => usage.render_usage += value,
|
||||
"drm-engine-copy" => usage.copy_usage += value,
|
||||
"drm-engine-video" => usage.video_usage += value,
|
||||
"drm-engine-video-enhance" => usage.video_enhance_usage += value,
|
||||
"drm-engine-compute" => usage.compute_usage += value,
|
||||
// Memory keys are in KiB: local0 = discrete VRAM, system0 = shared system RAM.
|
||||
"drm-total-local0" | "drm-total-system0" => usage.mem_used += value << 10,
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
IntelDriver::Xe => {
|
||||
collect_drm_fdinfo(&drm_paths, |usage: &mut IntelGpuProc, (keyword, value)| {
|
||||
match keyword {
|
||||
// rcs = render, ccs = compute, vcs = video decode, vecs = video enhance, bcs = copy.
|
||||
"drm-cycles-rcs" | "drm-cycles-ccs" | "drm-cycles-vcs" | "drm-cycles-vecs"
|
||||
| "drm-cycles-bcs" => usage.cycles += value,
|
||||
"drm-total-cycles-rcs"
|
||||
| "drm-total-cycles-ccs"
|
||||
| "drm-total-cycles-vcs"
|
||||
| "drm-total-cycles-vecs"
|
||||
| "drm-total-cycles-bcs" => usage.total_cycles += value,
|
||||
// Memory key is in KiB.
|
||||
"drm-total-vram0" => usage.mem_used += value << 10,
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_util(
|
||||
driver: IntelDriver, prev: &IntelGpuProc, cur: &IntelGpuProc, interval: &Duration,
|
||||
) -> u32 {
|
||||
match driver {
|
||||
// Busy-time deltas over the interval, summed across engines (as AMD does).
|
||||
IntelDriver::I915 => {
|
||||
let util = diff_usage(prev.render_usage, cur.render_usage, interval)
|
||||
+ diff_usage(prev.copy_usage, cur.copy_usage, interval)
|
||||
+ diff_usage(prev.video_usage, cur.video_usage, interval)
|
||||
+ diff_usage(prev.video_enhance_usage, cur.video_enhance_usage, interval)
|
||||
+ diff_usage(prev.compute_usage, cur.compute_usage, interval);
|
||||
|
||||
util.try_into().unwrap_or(0)
|
||||
}
|
||||
// Ratio of busy cycles to elapsed cycles over the interval.
|
||||
IntelDriver::Xe => {
|
||||
let cycles_delta = cur.cycles.saturating_sub(prev.cycles);
|
||||
let total_delta = cur.total_cycles.saturating_sub(prev.total_cycles);
|
||||
|
||||
if prev.total_cycles == 0 || total_delta == 0 {
|
||||
0
|
||||
} else {
|
||||
cycles_delta
|
||||
.saturating_mul(100)
|
||||
.checked_div(total_delta)
|
||||
.unwrap_or(0)
|
||||
.try_into()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_intel_gpu_data(
|
||||
widgets_to_harvest: &UsedWidgets, prev_time: Instant,
|
||||
) -> Option<IntelGpuData> {
|
||||
// TODO: Add caching for this.
|
||||
let mut i915_devices: Vec<PathBuf> = vec![];
|
||||
let mut xe_devices: Vec<PathBuf> = vec![];
|
||||
|
||||
if let Some(paths) = enumerate_drm_devices(IntelDriver::I915.module()) {
|
||||
i915_devices.extend(paths);
|
||||
}
|
||||
|
||||
if let Some(paths) = enumerate_drm_devices(IntelDriver::Xe.module()) {
|
||||
xe_devices.extend(paths);
|
||||
}
|
||||
|
||||
if i915_devices.is_empty() && xe_devices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let interval = Instant::now().duration_since(prev_time);
|
||||
let num_gpus = i915_devices.len() + xe_devices.len();
|
||||
let mut mem_vec = Vec::with_capacity(num_gpus);
|
||||
let mut proc_vec = Vec::with_capacity(num_gpus);
|
||||
let mut total_mem = 0;
|
||||
|
||||
PREV_PROC_DATA.with_borrow_mut(|prev_proc_data| {
|
||||
let device_path_set = i915_devices
|
||||
.iter()
|
||||
.chain(xe_devices.iter())
|
||||
.collect::<HashSet<_>>();
|
||||
prev_proc_data.retain(|k, _| device_path_set.contains(k));
|
||||
});
|
||||
|
||||
let devices = [
|
||||
(IntelDriver::I915, i915_devices),
|
||||
(IntelDriver::Xe, xe_devices),
|
||||
];
|
||||
|
||||
for (idx, (device_path, driver)) in devices
|
||||
.into_iter()
|
||||
.flat_map(|(driver, devices)| {
|
||||
devices
|
||||
.into_iter()
|
||||
.map(move |device_path| (device_path, driver))
|
||||
})
|
||||
.enumerate()
|
||||
{
|
||||
let device_name = get_intel_name(num_gpus, idx);
|
||||
|
||||
if let Some(mem) = get_intel_vram(&device_path, driver) {
|
||||
if widgets_to_harvest.use_mem
|
||||
&& let Some(total_bytes) = NonZeroU64::new(mem.total)
|
||||
{
|
||||
mem_vec.push((
|
||||
device_name.clone(),
|
||||
MemData {
|
||||
total_bytes,
|
||||
used_bytes: mem.used,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
total_mem += mem.total;
|
||||
}
|
||||
|
||||
if widgets_to_harvest.use_proc
|
||||
&& let Some(procs) = get_intel_fdinfo(&device_path, driver)
|
||||
{
|
||||
PREV_PROC_DATA.with_borrow_mut(|prev_proc_data| {
|
||||
let prev_fdinfo = prev_proc_data.entry(device_path).or_default();
|
||||
let mut seen_pids = IntHashSet::default();
|
||||
|
||||
let mut procs_map = IntHashMap::default();
|
||||
for (proc_pid, proc_usage) in procs {
|
||||
seen_pids.insert(proc_pid);
|
||||
if let Some(prev_usage) = prev_fdinfo.get_mut(&proc_pid) {
|
||||
let gpu_util = compute_util(driver, prev_usage, &proc_usage, &interval);
|
||||
|
||||
if gpu_util > 0 || proc_usage.mem_used > 0 {
|
||||
procs_map.insert(proc_pid, (proc_usage.mem_used, gpu_util));
|
||||
}
|
||||
|
||||
*prev_usage = proc_usage;
|
||||
} else {
|
||||
prev_fdinfo.insert(proc_pid, proc_usage);
|
||||
}
|
||||
}
|
||||
|
||||
prev_fdinfo.retain(|k, _| seen_pids.contains(k));
|
||||
|
||||
if !procs_map.is_empty() {
|
||||
proc_vec.push(procs_map);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bit of a hacky way to keep this trimmed. Ain't pretty but it should work.
|
||||
LAST_CLEAN_COUNTER.with_borrow_mut(|counter| {
|
||||
*counter += 1;
|
||||
|
||||
if *counter >= 300 {
|
||||
PREV_PROC_DATA.with_borrow_mut(|prev_proc_data| {
|
||||
for prev_fdinfo in prev_proc_data.values_mut() {
|
||||
prev_fdinfo.shrink_to_fit();
|
||||
}
|
||||
|
||||
prev_proc_data.shrink_to_fit();
|
||||
});
|
||||
|
||||
*counter = 0;
|
||||
}
|
||||
});
|
||||
|
||||
Some(IntelGpuData {
|
||||
memory: (!mem_vec.is_empty()).then_some(mem_vec),
|
||||
process_data: (!proc_vec.is_empty()).then_some((total_mem, proc_vec)),
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! A collection of functions/utilities to get NVIDIA GPU data.
|
||||
|
||||
use std::{num::NonZeroU64, sync::OnceLock};
|
||||
|
||||
use nvml_wrapper::{
|
||||
|
||||
Reference in New Issue
Block a user