use std::process::Command;
pub(super) struct NvidiaSmiResult {
pub used_bytes: u64,
pub total_bytes: u64,
}
pub(super) fn query(idx: u32) -> Option<NvidiaSmiResult> {
let cmd_result = Command::new("nvidia-smi")
.args([
"--query-gpu=memory.used,memory.total",
"--format=csv,noheader,nounits",
])
.arg(format!("--id={idx}"))
.output();
let output = match cmd_result {
Ok(o) if o.status.success() => o,
#[cfg(feature = "debug-output")]
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
eprintln!(
"[nvidia-smi debug] subprocess for idx={idx} exited with {} \
(stderr trimmed: {:?})",
o.status,
stderr.trim(),
);
return None;
}
#[cfg(not(feature = "debug-output"))]
Ok(_) => return None,
#[cfg(feature = "debug-output")]
Err(e) => {
eprintln!("[nvidia-smi debug] failed to spawn for idx={idx}: {e}");
return None;
}
#[cfg(not(feature = "debug-output"))]
Err(_) => return None,
};
let stdout = String::from_utf8_lossy(&output.stdout);
#[allow(clippy::question_mark)]
let Some(line_raw) = stdout.lines().next() else {
#[cfg(feature = "debug-output")]
eprintln!("[nvidia-smi debug] empty stdout for idx={idx}");
return None;
};
let line = line_raw.trim();
let mut parts = line.split(',');
let used_str = parts.next().map(str::trim)?;
let total_str = parts.next().map(str::trim)?;
let used_mb: u64 = match used_str.parse() {
Ok(v) => v,
#[cfg(feature = "debug-output")]
Err(e) => {
eprintln!("[nvidia-smi debug] failed to parse used '{used_str}' for idx={idx}: {e}");
return None;
}
#[cfg(not(feature = "debug-output"))]
Err(_) => return None,
};
let total_mb: u64 = match total_str.parse() {
Ok(v) => v,
#[cfg(feature = "debug-output")]
Err(e) => {
eprintln!("[nvidia-smi debug] failed to parse total '{total_str}' for idx={idx}: {e}");
return None;
}
#[cfg(not(feature = "debug-output"))]
Err(_) => return None,
};
let used_bytes = used_mb.saturating_mul(1_048_576);
let total_bytes = total_mb.saturating_mul(1_048_576);
#[cfg(feature = "debug-output")]
eprintln!(
"[nvidia-smi debug] idx={idx}: used={used_mb}MiB total={total_mb}MiB \
({used_bytes} / {total_bytes} bytes)"
);
Some(NvidiaSmiResult {
used_bytes,
total_bytes,
})
}
pub(super) struct ComputeApp {
pub pid: u32,
pub name: Option<String>,
pub used_bytes: u64,
}
pub(super) fn query_compute_apps(idx: u32) -> Option<Vec<ComputeApp>> {
let cmd_result = Command::new("nvidia-smi")
.args([
"--query-compute-apps=pid,process_name,used_memory",
"--format=csv,noheader,nounits",
])
.arg(format!("--id={idx}"))
.output();
let output = match cmd_result {
Ok(o) if o.status.success() => o,
#[cfg(feature = "debug-output")]
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
eprintln!(
"[nvidia-smi debug] --query-compute-apps for idx={idx} exited with {} \
(stderr trimmed: {:?})",
o.status,
stderr.trim(),
);
return None;
}
#[cfg(not(feature = "debug-output"))]
Ok(_) => return None,
#[cfg(feature = "debug-output")]
Err(e) => {
eprintln!("[nvidia-smi debug] failed to spawn --query-compute-apps for idx={idx}: {e}");
return None;
}
#[cfg(not(feature = "debug-output"))]
Err(_) => return None,
};
let stdout = String::from_utf8_lossy(&output.stdout);
let rows: Vec<ComputeApp> = stdout
.lines()
.filter_map(|line_raw| parse_compute_app_line(line_raw, idx))
.collect();
#[cfg(feature = "debug-output")]
eprintln!(
"[nvidia-smi debug] query_compute_apps(idx={idx}): {} row(s)",
rows.len()
);
Some(rows)
}
#[cfg_attr(not(feature = "debug-output"), allow(unused_variables))]
fn parse_compute_app_line(line_raw: &str, idx: u32) -> Option<ComputeApp> {
let line = line_raw.trim();
if line.is_empty() {
return None;
}
let (rest, used_str) = line.rsplit_once(',')?;
let (pid_str, name_str) = rest.split_once(',')?;
let pid: u32 = match pid_str.trim().parse() {
Ok(v) => v,
#[cfg(feature = "debug-output")]
Err(e) => {
eprintln!(
"[nvidia-smi debug] --query-compute-apps idx={idx}: failed to parse pid {pid_str:?}: {e}"
);
return None;
}
#[cfg(not(feature = "debug-output"))]
Err(_) => return None,
};
let used_mb: u64 = match used_str.trim().parse() {
Ok(v) => v,
#[cfg(feature = "debug-output")]
Err(e) => {
eprintln!(
"[nvidia-smi debug] --query-compute-apps idx={idx}: failed to parse used_memory \
{used_str:?} for pid {pid}: {e}"
);
return None;
}
#[cfg(not(feature = "debug-output"))]
Err(_) => return None,
};
let trimmed_name = name_str.trim();
let name = if trimmed_name.is_empty() {
None
} else {
Some(trimmed_name.to_owned())
};
let used_bytes = used_mb.saturating_mul(1_048_576);
Some(ComputeApp {
pid,
name,
used_bytes,
})
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::missing_docs_in_private_items
)]
mod tests {
use super::*;
#[test]
fn parse_compute_app_basic() {
let row = parse_compute_app_line("12345, python.exe, 1024", 0).unwrap();
assert_eq!(row.pid, 12345);
assert_eq!(row.name.as_deref(), Some("python.exe"));
assert_eq!(row.used_bytes, 1024 * 1_048_576);
}
#[test]
fn parse_compute_app_protected_name() {
let row = parse_compute_app_line("999, ?, 256", 0).unwrap();
assert_eq!(row.pid, 999);
assert_eq!(row.name.as_deref(), Some("?"));
assert_eq!(row.used_bytes, 256 * 1_048_576);
}
#[test]
fn parse_compute_app_name_with_comma() {
let row = parse_compute_app_line("42, weird,name.exe, 8", 0).unwrap();
assert_eq!(row.pid, 42);
assert_eq!(row.name.as_deref(), Some("weird,name.exe"));
assert_eq!(row.used_bytes, 8 * 1_048_576);
}
#[test]
fn parse_compute_app_empty_line() {
assert!(parse_compute_app_line("", 0).is_none());
assert!(parse_compute_app_line(" ", 0).is_none());
}
#[test]
fn parse_compute_app_unparseable_pid() {
assert!(parse_compute_app_line("notanumber, python.exe, 1024", 0).is_none());
}
#[test]
fn parse_compute_app_unparseable_memory() {
assert!(parse_compute_app_line("123, python.exe, notanumber", 0).is_none());
}
#[test]
fn parse_compute_app_too_few_fields() {
assert!(parse_compute_app_line("123, python.exe", 0).is_none());
assert!(parse_compute_app_line("12345", 0).is_none());
}
}