use std::ffi::CStr;
use std::io;
use std::ptr;
use std::sync::{Mutex, OnceLock};
#[derive(Clone, Debug)]
pub struct XgmiLinkSnapshot {
pub link_id: u32,
pub is_active: bool,
pub speed_gbps: Option<f64>,
#[allow(dead_code)]
pub bit_rate_gbps: Option<f64>,
pub remote_pci_bdf: Option<String>,
pub tx_bytes: Option<u64>,
pub rx_bytes: Option<u64>,
}
#[derive(Clone, Debug)]
pub struct XgmiSnapshot {
pub gpu_index: u32,
pub gpu_name: String,
pub link_count: u32,
pub link_gbps: Option<f64>,
pub correctable_errors: Option<u64>,
pub uncorrectable_errors: Option<u64>,
pub metrics: Option<crate::gpu::GpuMetrics>,
pub links: Vec<XgmiLinkSnapshot>,
}
impl XgmiSnapshot {
pub fn active_links(&self) -> u32 {
self.links.iter().filter(|l| l.is_active).count() as u32
}
}
fn format_bdf(bdf: u64) -> String {
let function = bdf & 0x7;
let device = (bdf >> 3) & 0x1f;
let bus = (bdf >> 8) & 0xff;
let domain = (bdf >> 16) & 0xffff_ffff_ffff;
format!("{:04x}:{:02x}:{:02x}.{:x}", domain, bus, device, function)
}
mod ffi {
use std::os::raw::{c_char, c_void};
pub type AmdsmiStatus = u32;
pub type SocketHandle = *mut c_void;
pub type ProcessorHandle = *mut c_void;
pub const AMDSMI_STATUS_SUCCESS: AmdsmiStatus = 0;
pub const AMDSMI_INIT_AMD_GPUS: u64 = 1 << 1;
pub const AMDSMI_MAX_STRING_LENGTH: usize = 256;
pub const AMDSMI_MAX_NUM_XGMI_PHYSICAL_LINK: usize = 64;
pub const AMDSMI_IOLINK_TYPE_XGMI: u32 = 2;
pub const AMDSMI_GPU_BLOCK_XGMI_WAFL: u64 = 0x80;
pub const AMDSMI_TEMPERATURE_TYPE_EDGE: u32 = 0;
pub const AMDSMI_TEMP_CURRENT: u32 = 0;
pub const AMDSMI_CLK_TYPE_GFX: u32 = 0;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct LinkMetricsEntry {
pub bdf: u64,
pub bit_rate: u32, pub max_bandwidth: u32, pub link_type: u32, pub read_kb: u64, pub write_kb: u64, pub reserved: [u64; 2],
}
#[repr(C)]
pub struct LinkMetrics {
pub num_links: u32,
pub links: [LinkMetricsEntry; AMDSMI_MAX_NUM_XGMI_PHYSICAL_LINK],
pub reserved: [u64; 7],
}
#[repr(C)]
pub struct AsicInfo {
pub market_name: [c_char; AMDSMI_MAX_STRING_LENGTH],
pub vendor_id: u32,
pub vendor_name: [c_char; AMDSMI_MAX_STRING_LENGTH],
pub subvendor_id: u32,
pub device_id: u64,
pub rev_id: u32,
pub asic_serial: [c_char; AMDSMI_MAX_STRING_LENGTH],
pub oam_id: u32,
pub num_of_compute_units: u32,
pub target_graphics_version: u64,
pub reserved: [u32; 22],
}
#[repr(C)]
pub struct ErrorCount {
pub correctable_count: u64,
pub uncorrectable_count: u64,
pub deferred_count: u64,
pub reserved: [u64; 5],
}
#[repr(C)]
pub struct PcieInfo {
pub max_pcie_width: u16,
pub max_pcie_speed: u32, pub _rest: [u64; 63],
}
#[repr(C)]
pub struct EngineUsage {
pub gfx_activity: u32, pub umc_activity: u32, pub mm_activity: u32, pub reserved: [u32; 13],
}
#[repr(C)]
pub struct VramUsage {
pub vram_total: u32, pub vram_used: u32, pub reserved: [u32; 2],
}
#[repr(C)]
pub struct PowerInfo {
pub socket_power: u64, pub current_socket_power: u32,
pub average_socket_power: u32,
pub gfx_voltage: u32, pub soc_voltage: u32, pub mem_voltage: u32, pub power_limit: u32, pub reserved: [u32; 2],
}
#[repr(C)]
pub struct ClkInfo {
pub clk: u32, pub min_clk: u32, pub max_clk: u32, pub clk_locked: u8,
pub clk_deep_sleep: u8,
pub reserved: [u32; 4],
}
const _: () = assert!(std::mem::size_of::<LinkMetricsEntry>() == 56);
const _: () = assert!(std::mem::offset_of!(LinkMetricsEntry, read_kb) == 24);
const _: () = assert!(std::mem::size_of::<LinkMetrics>() == 3648);
const _: () = assert!(std::mem::offset_of!(LinkMetrics, links) == 8);
const _: () = assert!(std::mem::size_of::<AsicInfo>() == 896);
const _: () = assert!(std::mem::offset_of!(AsicInfo, device_id) == 520);
const _: () = assert!(std::mem::size_of::<ErrorCount>() == 64);
const _: () = assert!(std::mem::size_of::<PcieInfo>() == 512);
const _: () = assert!(std::mem::offset_of!(PcieInfo, max_pcie_speed) == 4);
const _: () = assert!(std::mem::offset_of!(PcieInfo, _rest) == 8);
const _: () = assert!(std::mem::size_of::<EngineUsage>() == 64);
const _: () = assert!(std::mem::offset_of!(EngineUsage, gfx_activity) == 0);
const _: () = assert!(std::mem::size_of::<VramUsage>() == 16);
const _: () = assert!(std::mem::offset_of!(VramUsage, vram_total) == 0);
const _: () = assert!(std::mem::offset_of!(VramUsage, vram_used) == 4);
const _: () = assert!(std::mem::size_of::<PowerInfo>() == 40);
const _: () = assert!(std::mem::offset_of!(PowerInfo, current_socket_power) == 8);
const _: () = assert!(std::mem::offset_of!(PowerInfo, average_socket_power) == 12);
const _: () = assert!(std::mem::size_of::<ClkInfo>() == 32);
const _: () = assert!(std::mem::offset_of!(ClkInfo, clk) == 0);
}
struct Amdsmi {
_lib: libloading::Library,
get_socket_handles: unsafe extern "C" fn(*mut u32, *mut ffi::SocketHandle) -> ffi::AmdsmiStatus,
get_processor_handles: unsafe extern "C" fn(
ffi::SocketHandle,
*mut u32,
*mut ffi::ProcessorHandle,
) -> ffi::AmdsmiStatus,
get_gpu_asic_info:
unsafe extern "C" fn(ffi::ProcessorHandle, *mut ffi::AsicInfo) -> ffi::AmdsmiStatus,
get_link_metrics:
unsafe extern "C" fn(ffi::ProcessorHandle, *mut ffi::LinkMetrics) -> ffi::AmdsmiStatus,
get_gpu_device_bdf: unsafe extern "C" fn(ffi::ProcessorHandle, *mut u64) -> ffi::AmdsmiStatus,
get_pcie_info:
unsafe extern "C" fn(ffi::ProcessorHandle, *mut ffi::PcieInfo) -> ffi::AmdsmiStatus,
topo_get_link_type: unsafe extern "C" fn(
ffi::ProcessorHandle,
ffi::ProcessorHandle,
*mut u64,
*mut u32,
) -> ffi::AmdsmiStatus,
get_gpu_ecc_count:
unsafe extern "C" fn(ffi::ProcessorHandle, u64, *mut ffi::ErrorCount) -> ffi::AmdsmiStatus,
get_gpu_activity: Option<
unsafe extern "C" fn(ffi::ProcessorHandle, *mut ffi::EngineUsage) -> ffi::AmdsmiStatus,
>,
get_gpu_vram_usage: Option<
unsafe extern "C" fn(ffi::ProcessorHandle, *mut ffi::VramUsage) -> ffi::AmdsmiStatus,
>,
get_temp_metric:
Option<unsafe extern "C" fn(ffi::ProcessorHandle, u32, u32, *mut i64) -> ffi::AmdsmiStatus>,
get_power_info: Option<
unsafe extern "C" fn(ffi::ProcessorHandle, *mut ffi::PowerInfo) -> ffi::AmdsmiStatus,
>,
get_clock_info: Option<
unsafe extern "C" fn(ffi::ProcessorHandle, u32, *mut ffi::ClkInfo) -> ffi::AmdsmiStatus,
>,
}
const LIB_CANDIDATES: [&str; 3] = [
"libamd_smi.so",
"libamd_smi.so.25",
"/opt/rocm/lib/libamd_smi.so",
];
impl Amdsmi {
fn load() -> Option<Self> {
for path in LIB_CANDIDATES {
if let Ok(lib) = unsafe { libloading::Library::new(path) } {
if let Some(api) = Self::from_lib(lib) {
return Some(api);
}
}
}
None
}
fn from_lib(lib: libloading::Library) -> Option<Self> {
unsafe {
let init: unsafe extern "C" fn(u64) -> ffi::AmdsmiStatus =
*lib.get(b"amdsmi_init\0").ok()?;
let api = Amdsmi {
get_socket_handles: *lib.get(b"amdsmi_get_socket_handles\0").ok()?,
get_processor_handles: *lib.get(b"amdsmi_get_processor_handles\0").ok()?,
get_gpu_asic_info: *lib.get(b"amdsmi_get_gpu_asic_info\0").ok()?,
get_link_metrics: *lib.get(b"amdsmi_get_link_metrics\0").ok()?,
get_gpu_device_bdf: *lib.get(b"amdsmi_get_gpu_device_bdf\0").ok()?,
get_pcie_info: *lib.get(b"amdsmi_get_pcie_info\0").ok()?,
topo_get_link_type: *lib.get(b"amdsmi_topo_get_link_type\0").ok()?,
get_gpu_ecc_count: *lib.get(b"amdsmi_get_gpu_ecc_count\0").ok()?,
get_gpu_activity: lib.get(b"amdsmi_get_gpu_activity\0").ok().map(|s| *s),
get_gpu_vram_usage: lib.get(b"amdsmi_get_gpu_vram_usage\0").ok().map(|s| *s),
get_temp_metric: lib.get(b"amdsmi_get_temp_metric\0").ok().map(|s| *s),
get_power_info: lib.get(b"amdsmi_get_power_info\0").ok().map(|s| *s),
get_clock_info: lib.get(b"amdsmi_get_clock_info\0").ok().map(|s| *s),
_lib: lib,
};
if init(ffi::AMDSMI_INIT_AMD_GPUS) != ffi::AMDSMI_STATUS_SUCCESS {
return None;
}
Some(api)
}
}
}
fn instance() -> Option<&'static Amdsmi> {
static INSTANCE: OnceLock<Option<Amdsmi>> = OnceLock::new();
INSTANCE.get_or_init(Amdsmi::load).as_ref()
}
#[repr(C)]
struct Padded<T> {
value: T,
_headroom: [u8; 4096],
}
impl<T> Padded<T> {
fn zeroed() -> Self {
unsafe { std::mem::zeroed() }
}
}
pub fn read_all_xgmi_stats() -> io::Result<Vec<XgmiSnapshot>> {
let api = match instance() {
Some(a) => a,
None => return Ok(Vec::new()),
};
let handles = enumerate_gpus(api);
if handles.is_empty() {
return Ok(Vec::new());
}
let bdfs: Vec<Option<u64>> = handles.iter().map(|&h| read_bdf(api, h)).collect();
let names = gpu_names(api, &handles);
let speeds = link_speeds(api, &handles);
let peers = xgmi_peers(api, &handles);
let mut snapshots = Vec::with_capacity(handles.len());
for idx in 0..handles.len() {
if let Some(snap) =
read_processor_snapshot(api, &handles, &bdfs, names, &speeds, &peers, idx)
{
snapshots.push(snap);
}
}
Ok(snapshots)
}
fn gpu_names(api: &Amdsmi, handles: &[ffi::ProcessorHandle]) -> &'static [String] {
static NAMES: OnceLock<Vec<String>> = OnceLock::new();
NAMES.get_or_init(|| {
handles
.iter()
.map(|&h| read_gpu_name(api, h).unwrap_or_default())
.collect()
})
}
fn xgmi_peers(api: &Amdsmi, handles: &[ffi::ProcessorHandle]) -> Vec<Vec<bool>> {
static PEERS: Mutex<Option<Vec<Vec<bool>>>> = Mutex::new(None);
let mut cached = PEERS.lock().unwrap_or_else(|e| e.into_inner());
if let Some(m) = cached.as_ref() {
if m.len() == handles.len() {
return m.clone();
}
}
let n = handles.len();
let mut matrix = vec![vec![false; n]; n];
let mut complete = true;
for src in 0..n {
for dst in 0..n {
if src == dst {
continue;
}
match is_xgmi_peer(api, handles[src], handles[dst]) {
Some(peer) => matrix[src][dst] = peer,
None => complete = false,
}
}
}
if complete && n > 0 {
*cached = Some(matrix.clone());
}
matrix
}
type LinkSpeed = (Option<f64>, Option<f64>);
fn link_speeds(api: &Amdsmi, handles: &[ffi::ProcessorHandle]) -> Vec<LinkSpeed> {
static CACHE: Mutex<Vec<Option<LinkSpeed>>> = Mutex::new(Vec::new());
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
if cache.len() < handles.len() {
cache.resize(handles.len(), None);
}
handles
.iter()
.zip(cache.iter_mut())
.map(|(&handle, slot)| match slot {
Some(speed) => *speed,
None => {
let speed = read_link_speed(api, handle);
if speed != (None, None) {
*slot = Some(speed);
}
speed
}
})
.collect()
}
fn enumerate_gpus(api: &Amdsmi) -> Vec<ffi::ProcessorHandle> {
let mut handles = Vec::new();
let mut socket_count: u32 = 0;
let rc = unsafe { (api.get_socket_handles)(&mut socket_count, ptr::null_mut()) };
if rc != ffi::AMDSMI_STATUS_SUCCESS || socket_count == 0 {
return handles;
}
let mut sockets: Vec<ffi::SocketHandle> = vec![ptr::null_mut(); socket_count as usize];
let rc = unsafe { (api.get_socket_handles)(&mut socket_count, sockets.as_mut_ptr()) };
if rc != ffi::AMDSMI_STATUS_SUCCESS {
return handles;
}
for &socket in sockets.iter().take(socket_count as usize) {
let mut proc_count: u32 = 0;
let rc = unsafe { (api.get_processor_handles)(socket, &mut proc_count, ptr::null_mut()) };
if rc != ffi::AMDSMI_STATUS_SUCCESS || proc_count == 0 {
continue;
}
let mut procs: Vec<ffi::ProcessorHandle> = vec![ptr::null_mut(); proc_count as usize];
let rc =
unsafe { (api.get_processor_handles)(socket, &mut proc_count, procs.as_mut_ptr()) };
if rc != ffi::AMDSMI_STATUS_SUCCESS {
continue;
}
handles.extend(procs.iter().take(proc_count as usize).copied());
}
handles
}
fn read_bdf(api: &Amdsmi, handle: ffi::ProcessorHandle) -> Option<u64> {
let mut bdf: u64 = 0;
let rc = unsafe { (api.get_gpu_device_bdf)(handle, &mut bdf) };
(rc == ffi::AMDSMI_STATUS_SUCCESS).then_some(bdf)
}
fn read_processor_snapshot(
api: &Amdsmi,
handles: &[ffi::ProcessorHandle],
bdfs: &[Option<u64>],
names: &[String],
speeds: &[LinkSpeed],
peers: &[Vec<bool>],
idx: usize,
) -> Option<XgmiSnapshot> {
let handle = handles[idx];
let mut metrics: Padded<ffi::LinkMetrics> = Padded::zeroed();
let rc = unsafe { (api.get_link_metrics)(handle, &mut metrics.value) };
if rc != ffi::AMDSMI_STATUS_SUCCESS {
return None;
}
let metrics = &metrics.value;
let gpu_name = names.get(idx).cloned().unwrap_or_default();
let (bit_rate_gbps, speed_gbps) = speeds.get(idx).copied().unwrap_or((None, None));
let (correctable_errors, uncorrectable_errors) = read_wafl_errors(api, handle);
let n = (metrics.num_links as usize).min(ffi::AMDSMI_MAX_NUM_XGMI_PHYSICAL_LINK);
let mut links = Vec::new();
let mut total_gbps = 0.0;
for (peer_idx, peer_bdf) in bdfs.iter().enumerate() {
if peer_idx == idx
|| !peers
.get(idx)
.and_then(|row| row.get(peer_idx))
.copied()
.unwrap_or(false)
{
continue;
}
let (tx_bytes, rx_bytes) = match metrics.links.get(peer_idx) {
Some(entry) if peer_idx < n && (entry.bdf == 0 || Some(entry.bdf) == *peer_bdf) => (
Some(entry.write_kb.wrapping_mul(1024)),
Some(entry.read_kb.wrapping_mul(1024)),
),
_ => (None, None),
};
total_gbps += speed_gbps.unwrap_or(0.0);
links.push(XgmiLinkSnapshot {
link_id: peer_idx as u32,
is_active: true,
speed_gbps,
bit_rate_gbps,
remote_pci_bdf: peer_bdf.map(format_bdf),
tx_bytes,
rx_bytes,
});
}
if links.is_empty() {
return None;
}
Some(XgmiSnapshot {
gpu_index: idx as u32,
gpu_name,
link_count: links.len() as u32,
link_gbps: (total_gbps > 0.0).then_some(total_gbps),
correctable_errors,
uncorrectable_errors,
metrics: Some(read_gpu_metrics(api, handle)).filter(|m| !m.is_empty()),
links,
})
}
fn read_gpu_metrics(api: &Amdsmi, handle: ffi::ProcessorHandle) -> crate::gpu::GpuMetrics {
let util_pct = api.get_gpu_activity.and_then(|f| {
let mut u: Padded<ffi::EngineUsage> = Padded::zeroed();
(unsafe { f(handle, &mut u.value) } == ffi::AMDSMI_STATUS_SUCCESS)
.then_some(u.value.gfx_activity)
});
let vram = api.get_gpu_vram_usage.and_then(|f| {
let mut v: Padded<ffi::VramUsage> = Padded::zeroed();
(unsafe { f(handle, &mut v.value) } == ffi::AMDSMI_STATUS_SUCCESS)
.then_some((v.value.vram_used as u64, v.value.vram_total as u64))
});
let temp_c = api.get_temp_metric.and_then(|f| {
let mut t: i64 = 0;
(unsafe {
f(
handle,
ffi::AMDSMI_TEMPERATURE_TYPE_EDGE,
ffi::AMDSMI_TEMP_CURRENT,
&mut t,
)
} == ffi::AMDSMI_STATUS_SUCCESS)
.then_some(t)
});
let power_w = api.get_power_info.and_then(|f| {
let mut p: Padded<ffi::PowerInfo> = Padded::zeroed();
if unsafe { f(handle, &mut p.value) } != ffi::AMDSMI_STATUS_SUCCESS {
return None;
}
let w = if p.value.current_socket_power > 0 {
p.value.current_socket_power
} else {
p.value.average_socket_power
};
(w > 0).then_some(w as f64)
});
let clock_mhz = api.get_clock_info.and_then(|f| {
let mut c: Padded<ffi::ClkInfo> = Padded::zeroed();
(unsafe { f(handle, ffi::AMDSMI_CLK_TYPE_GFX, &mut c.value) } == ffi::AMDSMI_STATUS_SUCCESS)
.then_some(c.value.clk)
});
crate::gpu::GpuMetrics {
util_pct,
vram_used_mb: vram.map(|(u, _)| u),
vram_total_mb: vram.map(|(_, t)| t),
temp_c,
power_w,
clock_mhz,
}
}
fn is_xgmi_peer(
api: &Amdsmi,
src: ffi::ProcessorHandle,
dst: ffi::ProcessorHandle,
) -> Option<bool> {
let mut hops: u64 = 0;
let mut link_type: u32 = 0;
let rc = unsafe { (api.topo_get_link_type)(src, dst, &mut hops, &mut link_type) };
if rc != ffi::AMDSMI_STATUS_SUCCESS {
return None;
}
Some(link_type == ffi::AMDSMI_IOLINK_TYPE_XGMI)
}
fn read_link_speed(api: &Amdsmi, handle: ffi::ProcessorHandle) -> LinkSpeed {
let mut info: Padded<ffi::PcieInfo> = Padded::zeroed();
let rc = unsafe { (api.get_pcie_info)(handle, &mut info.value) };
if rc != ffi::AMDSMI_STATUS_SUCCESS || info.value.max_pcie_speed == 0 {
return (None, None);
}
let info = &info.value;
let bit_rate = if info.max_pcie_speed >= 1000 {
info.max_pcie_speed as f64 / 1000.0
} else {
info.max_pcie_speed as f64
};
let bandwidth = (info.max_pcie_width > 0).then_some(bit_rate * info.max_pcie_width as f64);
(Some(bit_rate), bandwidth)
}
fn read_gpu_name(api: &Amdsmi, handle: ffi::ProcessorHandle) -> Option<String> {
let mut info: Padded<ffi::AsicInfo> = Padded::zeroed();
let rc = unsafe { (api.get_gpu_asic_info)(handle, &mut info.value) };
if rc != ffi::AMDSMI_STATUS_SUCCESS {
return None;
}
let info = &mut info.value;
info.market_name[ffi::AMDSMI_MAX_STRING_LENGTH - 1] = 0;
let name = unsafe { CStr::from_ptr(info.market_name.as_ptr() as *const std::os::raw::c_char) };
let name = name.to_string_lossy().trim().to_string();
(!name.is_empty()).then_some(name)
}
fn read_wafl_errors(api: &Amdsmi, handle: ffi::ProcessorHandle) -> (Option<u64>, Option<u64>) {
let mut ec: Padded<ffi::ErrorCount> = Padded::zeroed();
let rc =
unsafe { (api.get_gpu_ecc_count)(handle, ffi::AMDSMI_GPU_BLOCK_XGMI_WAFL, &mut ec.value) };
if rc != ffi::AMDSMI_STATUS_SUCCESS {
return (None, None);
}
(
Some(ec.value.correctable_count),
Some(ec.value.uncorrectable_count),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn link(id: u32, active: bool) -> XgmiLinkSnapshot {
XgmiLinkSnapshot {
link_id: id,
is_active: active,
speed_gbps: None,
bit_rate_gbps: None,
remote_pci_bdf: None,
tx_bytes: None,
rx_bytes: None,
}
}
#[test]
fn active_links_counts_active_only() {
let snap = XgmiSnapshot {
gpu_index: 0,
gpu_name: "test".to_string(),
link_count: 3,
link_gbps: None,
correctable_errors: None,
uncorrectable_errors: None,
metrics: None,
links: vec![link(0, true), link(1, false), link(2, true)],
};
assert_eq!(snap.active_links(), 2);
}
#[test]
fn active_links_no_links() {
let snap = XgmiSnapshot {
gpu_index: 0,
gpu_name: "test".to_string(),
link_count: 0,
link_gbps: None,
correctable_errors: None,
uncorrectable_errors: None,
metrics: None,
links: Vec::new(),
};
assert_eq!(snap.active_links(), 0);
}
#[test]
fn format_bdf_matches_lspci_form() {
assert_eq!(format_bdf(0x0000_0000_0000_0500), "0000:05:00.0");
assert_eq!(format_bdf(0x0000_0000_0000_f500), "0000:f5:00.0");
assert_eq!(format_bdf((0x65 << 8) | (0x1f << 3) | 0x7), "0000:65:1f.7");
}
#[test]
#[ignore]
fn smoke_read_all_xgmi_stats() {
let snaps = read_all_xgmi_stats().expect("io error");
assert!(!snaps.is_empty(), "expected XGMI-capable GPUs on this host");
assert!(
snaps.iter().any(|s| s.link_count == 7),
"expected at least one GPU with 7 XGMI links on amd1"
);
for s in &snaps {
assert!(s.link_count > 0);
assert!(
s.active_links() > 0,
"gpu{} has no active links",
s.gpu_index
);
for l in &s.links {
let bdf = l.remote_pci_bdf.as_deref().unwrap_or("<none>");
assert!(
l.remote_pci_bdf.is_some() && bdf != "0000:00:00.0",
"gpu{} link{} has null BDF",
s.gpu_index,
l.link_id
);
assert!(
l.speed_gbps.unwrap_or(0.0) > 0.0,
"gpu{} link{} has zero speed",
s.gpu_index,
l.link_id
);
assert!(
l.tx_bytes.is_some() && l.rx_bytes.is_some(),
"gpu{} link{} has no accumulator data",
s.gpu_index,
l.link_id
);
}
}
if let Some(s) = snaps.first() {
let fl = s.links.first();
eprintln!(
"gpu0 name={:?} link_gbps={:?} first_link={:?}",
s.gpu_name, s.link_gbps, fl
);
}
let count_fds = || std::fs::read_dir("/proc/self/fd").unwrap().count();
let before = count_fds();
for _ in 0..100 {
read_all_xgmi_stats().expect("io error");
}
let after = count_fds();
assert!(
after <= before + 2,
"fd leak: {} -> {} after 100 polls",
before,
after
);
}
}