#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GpuStat {
Utilization,
MemoryUsed,
Temperature,
PowerDraw,
}
impl GpuStat {
#[must_use]
pub fn unit(&self) -> &'static str {
match self {
Self::Utilization => "%",
Self::MemoryUsed => "bytes",
Self::Temperature => "°C",
Self::PowerDraw => "mW",
}
}
#[must_use]
pub fn is_percentage(&self) -> bool {
matches!(self, Self::Utilization)
}
#[must_use]
pub fn is_thermal(&self) -> bool {
matches!(self, Self::Temperature)
}
}
#[derive(Debug, Clone)]
pub struct GpuStatSample {
pub stat: GpuStat,
pub value: f64,
pub critical_threshold: f64,
}
impl GpuStatSample {
#[must_use]
pub fn new(stat: GpuStat, value: f64, critical_threshold: f64) -> Self {
Self {
stat,
value,
critical_threshold,
}
}
#[must_use]
pub fn is_critical(&self) -> bool {
self.value >= self.critical_threshold
}
#[must_use]
pub fn headroom(&self) -> f64 {
self.critical_threshold - self.value
}
}
#[derive(Debug, Default)]
pub struct GpuStats {
utilization_samples: Vec<f64>,
memory_used_samples: Vec<u64>,
temperature_samples: Vec<f64>,
power_draw_samples: Vec<f64>,
total_memory_bytes: u64,
}
impl GpuStats {
#[must_use]
pub fn new(total_memory_bytes: u64) -> Self {
Self {
total_memory_bytes,
..Default::default()
}
}
pub fn record(&mut self, sample: &GpuStatSample) {
match sample.stat {
GpuStat::Utilization => self.utilization_samples.push(sample.value),
GpuStat::MemoryUsed => self.memory_used_samples.push(sample.value as u64),
GpuStat::Temperature => self.temperature_samples.push(sample.value),
GpuStat::PowerDraw => self.power_draw_samples.push(sample.value),
}
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn utilization_pct(&self) -> f64 {
if self.utilization_samples.is_empty() {
return 0.0;
}
let sum: f64 = self.utilization_samples.iter().sum();
sum / self.utilization_samples.len() as f64
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn memory_pct(&self) -> f64 {
if self.memory_used_samples.is_empty() || self.total_memory_bytes == 0 {
return 0.0;
}
let sum: u64 = self.memory_used_samples.iter().sum();
let avg = sum as f64 / self.memory_used_samples.len() as f64;
(avg / self.total_memory_bytes as f64) * 100.0
}
#[must_use]
pub fn peak_temperature(&self) -> Option<f64> {
self.temperature_samples.iter().copied().reduce(f64::max)
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn avg_power_draw_mw(&self) -> f64 {
if self.power_draw_samples.is_empty() {
return 0.0;
}
let sum: f64 = self.power_draw_samples.iter().sum();
sum / self.power_draw_samples.len() as f64
}
#[must_use]
pub fn sample_count(&self) -> usize {
self.utilization_samples.len()
+ self.memory_used_samples.len()
+ self.temperature_samples.len()
+ self.power_draw_samples.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gpu_stat_unit_utilization() {
assert_eq!(GpuStat::Utilization.unit(), "%");
}
#[test]
fn test_gpu_stat_unit_memory() {
assert_eq!(GpuStat::MemoryUsed.unit(), "bytes");
}
#[test]
fn test_gpu_stat_unit_temperature() {
assert_eq!(GpuStat::Temperature.unit(), "°C");
}
#[test]
fn test_gpu_stat_unit_power() {
assert_eq!(GpuStat::PowerDraw.unit(), "mW");
}
#[test]
fn test_gpu_stat_is_percentage() {
assert!(GpuStat::Utilization.is_percentage());
assert!(!GpuStat::MemoryUsed.is_percentage());
assert!(!GpuStat::Temperature.is_percentage());
}
#[test]
fn test_gpu_stat_is_thermal() {
assert!(GpuStat::Temperature.is_thermal());
assert!(!GpuStat::Utilization.is_thermal());
}
#[test]
fn test_sample_is_critical_true() {
let s = GpuStatSample::new(GpuStat::Temperature, 95.0, 90.0);
assert!(s.is_critical());
}
#[test]
fn test_sample_is_critical_false() {
let s = GpuStatSample::new(GpuStat::Temperature, 75.0, 90.0);
assert!(!s.is_critical());
}
#[test]
fn test_sample_is_critical_at_threshold() {
let s = GpuStatSample::new(GpuStat::Utilization, 90.0, 90.0);
assert!(s.is_critical());
}
#[test]
fn test_sample_headroom() {
let s = GpuStatSample::new(GpuStat::PowerDraw, 200.0, 250.0);
assert!((s.headroom() - 50.0).abs() < 1e-9);
}
#[test]
fn test_stats_utilization_pct_empty() {
let stats = GpuStats::new(8 * 1024 * 1024 * 1024);
assert!((stats.utilization_pct() - 0.0).abs() < 1e-9);
}
#[test]
fn test_stats_record_and_utilization_pct() {
let mut stats = GpuStats::new(8 * 1024 * 1024 * 1024);
stats.record(&GpuStatSample::new(GpuStat::Utilization, 80.0, 100.0));
stats.record(&GpuStatSample::new(GpuStat::Utilization, 60.0, 100.0));
assert!((stats.utilization_pct() - 70.0).abs() < 1e-9);
}
#[test]
fn test_stats_memory_pct() {
let total = 8_000_000_000u64;
let mut stats = GpuStats::new(total);
stats.record(&GpuStatSample::new(
GpuStat::MemoryUsed,
4_000_000_000.0,
f64::MAX,
));
let pct = stats.memory_pct();
assert!((pct - 50.0).abs() < 0.01);
}
#[test]
fn test_stats_peak_temperature() {
let mut stats = GpuStats::new(0);
assert!(stats.peak_temperature().is_none());
stats.record(&GpuStatSample::new(GpuStat::Temperature, 60.0, 100.0));
stats.record(&GpuStatSample::new(GpuStat::Temperature, 85.0, 100.0));
assert_eq!(stats.peak_temperature(), Some(85.0));
}
#[test]
fn test_stats_avg_power_draw() {
let mut stats = GpuStats::new(0);
stats.record(&GpuStatSample::new(GpuStat::PowerDraw, 100.0, 300.0));
stats.record(&GpuStatSample::new(GpuStat::PowerDraw, 200.0, 300.0));
assert!((stats.avg_power_draw_mw() - 150.0).abs() < 1e-9);
}
#[test]
fn test_stats_sample_count() {
let mut stats = GpuStats::new(0);
stats.record(&GpuStatSample::new(GpuStat::Utilization, 50.0, 100.0));
stats.record(&GpuStatSample::new(GpuStat::Temperature, 70.0, 90.0));
assert_eq!(stats.sample_count(), 2);
}
}