Skip to main content

joule_profiler_source_amdsmi/
lib.rs

1//! AMD SMI (AMD System Management Interface) energy profiling integration for Joule Profiler.
2//!
3//! This module provides energy consumption, VRAM usage and GPU utilization metrics for AMD GPUs using the AMD SMI library.
4
5use std::{
6    collections::HashMap,
7    sync::{Arc, Mutex},
8    time::Duration,
9};
10
11use bitflags::bitflags;
12use futures::{StreamExt, future::try_join_all};
13use joule_profiler_core::{
14    sensor::{Sensor, Sensors},
15    source::MetricReader,
16    types::{Metric, Metrics},
17    unit::{MetricUnit, Unit, UnitPrefix},
18};
19use log::{debug, trace};
20use tokio::task::{JoinHandle, spawn_blocking};
21use tokio_timerfd::Interval;
22use tokio_util::sync::CancellationToken;
23
24use crate::{
25    config::AmdSmiConfig,
26    counters::{Counter, EnergyCounter, PowerCounter, UtilizationCounter, VramCounter},
27    error::AmdSmiError::{self},
28    hardware::{AmdSmiHardware, AmdSmiWrapperHardware},
29};
30
31pub mod config;
32pub mod counters;
33pub mod error;
34mod hardware;
35
36pub type UUID = String;
37
38type Result<T> = std::result::Result<T, AmdSmiError>;
39
40/// Polling task handle and its cancellation token.
41type WorkerHandle = (CancellationToken, JoinHandle<Result<()>>);
42
43const AMDSMI_SOURCE_NAME: &str = "amdsmi";
44
45const MICRO_JOULE_UNIT: MetricUnit = MetricUnit {
46    prefix: UnitPrefix::Micro,
47    unit: Unit::Joule,
48};
49
50const BYTE_UNIT: MetricUnit = MetricUnit {
51    prefix: UnitPrefix::None,
52    unit: Unit::Byte,
53};
54
55const PERCENT_UNIT: MetricUnit = MetricUnit {
56    prefix: UnitPrefix::None,
57    unit: Unit::Percent,
58};
59
60bitflags! {
61    /// The supports of a device.
62    #[derive(Debug, Clone, Copy)]
63    struct ProcessorSupport: u8 {
64        const Energy = 1;
65        const Power = 1 << 1;
66        const Vram = 1 << 2;
67        const Utilization = 1 << 3;
68    }
69}
70
71#[derive(Debug, Clone)]
72pub struct Processor {
73    /// The UUID of the device.
74    uuid: UUID,
75
76    /// The supports of the device (e.g., Energy, Power, VRAM).
77    support: ProcessorSupport,
78}
79
80pub struct AmdSmi<H: AmdSmiHardware = AmdSmiWrapperHardware> {
81    /// Source configuration.
82    config: AmdSmiConfig,
83
84    /// The hardware used for querying AMD SMI lib. Used for testing.
85    hardware: Arc<H>,
86
87    /// The handle to the polling task and its cancellation token.
88    handle: Option<WorkerHandle>,
89
90    /// Map of GPU devices, the key is an index to avoid cloning the devices UUID.
91    processors: Arc<HashMap<usize, Processor>>,
92
93    /// The current energy counters.
94    energy_counters: HashMap<usize, EnergyCounter>,
95
96    /// The current vram counters.
97    vram_counters: Arc<Mutex<HashMap<usize, VramCounter>>>,
98
99    /// The current power counters.
100    power_counters: Arc<Mutex<HashMap<usize, PowerCounter>>>,
101
102    /// The current GPU utilization counters.
103    utilization_counters: Arc<Mutex<HashMap<usize, UtilizationCounter>>>,
104}
105
106impl<H: AmdSmiHardware> AmdSmi<H> {
107    /// Creates the worker task for power and vram polling at the specified polling interval.
108    pub fn create_worker(
109        hardware: Arc<H>,
110        processors: Arc<HashMap<usize, Processor>>,
111        power_counters: Arc<Mutex<HashMap<usize, PowerCounter>>>,
112        vram_counters: Arc<Mutex<HashMap<usize, VramCounter>>>,
113        usage_counters: Arc<Mutex<HashMap<usize, UtilizationCounter>>>,
114        poll_interval: Duration,
115    ) -> Result<WorkerHandle> {
116        let mut ticker = Interval::new_interval(poll_interval)?;
117
118        let cancellation_token = CancellationToken::new();
119        let cancellation_token_clone = cancellation_token.clone();
120
121        let handle = tokio::spawn(async move {
122            debug!("Starting AMD SMI source polling.");
123
124            loop {
125                tokio::select! {
126                    _ = ticker.next() => {
127                        trace!("Polled AMD SMI source.");
128                        Self::read_polled_counters(&hardware, &processors, &power_counters, &vram_counters, &usage_counters).await?;
129                    }
130
131                    () = cancellation_token.cancelled() => {
132                        debug!("AMD SMI worker stopped.");
133                        break;
134                    }
135                }
136            }
137
138            Ok(())
139        });
140
141        Ok((cancellation_token_clone, handle))
142    }
143
144    /// Reads the power and vram counters for each processors and updates the current counters.
145    ///
146    /// Spawn every blocking read upfront so they run concurrently on the
147    /// blocking thread pool, instead of awaiting them one by one.
148    async fn read_polled_counters(
149        hardware: &Arc<H>,
150        processors: &Arc<HashMap<usize, Processor>>,
151        power_counters: &Arc<Mutex<HashMap<usize, PowerCounter>>>,
152        vram_counters: &Arc<Mutex<HashMap<usize, VramCounter>>>,
153        utilization_counters: &Arc<Mutex<HashMap<usize, UtilizationCounter>>>,
154    ) -> Result<()> {
155        let mut vram_tasks = Vec::new();
156        let mut power_tasks = Vec::new();
157        let mut utilization_tasks = Vec::new();
158
159        for (index, processor) in processors.iter() {
160            let index = *index;
161
162            if processor.support.contains(ProcessorSupport::Vram) {
163                let hardware = hardware.clone();
164                let processor = processor.clone();
165                vram_tasks.push(spawn_blocking(move || {
166                    hardware
167                        .get_vram_usage(&processor)
168                        .map(|vram| (index, vram))
169                }));
170            }
171            if processor.support.contains(ProcessorSupport::Power) {
172                let hardware = hardware.clone();
173                let processor = processor.clone();
174                power_tasks.push(spawn_blocking(move || {
175                    hardware.get_power(&processor).map(|power| (index, power))
176                }));
177            }
178            if processor.support.contains(ProcessorSupport::Utilization) {
179                let hardware = hardware.clone();
180                let processor = processor.clone();
181                utilization_tasks.push(spawn_blocking(move || {
182                    hardware
183                        .get_gpu_activity(&processor)
184                        .map(|usage| (index, usage.gpu_usage))
185                }));
186            }
187        }
188
189        let vram_updates = try_join_all(vram_tasks).await?;
190        let power_updates = try_join_all(power_tasks).await?;
191        let utilization_updates = try_join_all(utilization_tasks).await?;
192
193        {
194            let mut lock = vram_counters
195                .lock()
196                .map_err(|_| AmdSmiError::MutexPoisoned)?;
197            for (index, vram_usage) in vram_updates.into_iter().collect::<Result<Vec<_>>>()? {
198                lock.entry(index).or_default().update(vram_usage);
199            }
200        }
201
202        {
203            let mut lock = power_counters
204                .lock()
205                .map_err(|_| AmdSmiError::MutexPoisoned)?;
206            for (index, power) in power_updates.into_iter().collect::<Result<Vec<_>>>()? {
207                lock.entry(index).or_default().push(power);
208            }
209        }
210
211        {
212            let mut lock = utilization_counters
213                .lock()
214                .map_err(|_| AmdSmiError::MutexPoisoned)?;
215            for (index, utilization) in utilization_updates
216                .into_iter()
217                .collect::<Result<Vec<_>>>()?
218            {
219                lock.entry(index).or_default().update(utilization);
220            }
221        }
222
223        Ok(())
224    }
225}
226
227impl<H: AmdSmiHardware> MetricReader for AmdSmi<H> {
228    type Type = HashMap<usize, Counter>;
229    type Error = AmdSmiError;
230    type Config = AmdSmiConfig;
231
232    /// Initializes the AMD SMI source and retrieve the GPU devices.
233    fn from_config(config: AmdSmiConfig) -> Result<Self> {
234        let mut amdsmi = H::new()?;
235        let processors = amdsmi
236            .init_processors(config.gpus_spec.as_ref())?
237            .into_iter()
238            .enumerate()
239            .collect();
240
241        Ok(Self {
242            config,
243            hardware: Arc::new(amdsmi),
244            handle: None,
245            processors: Arc::new(processors),
246            energy_counters: HashMap::new(),
247            vram_counters: Arc::default(),
248            power_counters: Arc::default(),
249            utilization_counters: Arc::default(),
250        })
251    }
252
253    /// Makes a measurement for every devices.
254    ///
255    /// Spawns the different measures in the blocking thread pool to execute
256    /// them in parallel without blocking the async pool.
257    async fn measure(&mut self) -> Result<()> {
258        let mut energy_tasks = Vec::new();
259        for (index, processor) in self.processors.iter() {
260            if processor.support.contains(ProcessorSupport::Energy) {
261                let hardware = self.hardware.clone();
262                let processor = processor.clone();
263                let index = *index;
264                energy_tasks.push(spawn_blocking(move || {
265                    hardware
266                        .get_energy_count(&processor)
267                        .map(|energy| (index, energy))
268                }));
269            }
270        }
271
272        Self::read_polled_counters(
273            &self.hardware,
274            &self.processors,
275            &self.power_counters,
276            &self.vram_counters,
277            &self.utilization_counters,
278        )
279        .await?;
280
281        for (index, energy) in try_join_all(energy_tasks)
282            .await?
283            .into_iter()
284            .collect::<Result<Vec<_>>>()?
285        {
286            self.energy_counters
287                .entry(index)
288                .or_default()
289                .update(energy);
290        }
291
292        Ok(())
293    }
294
295    /// Retrieve the current counters and reset them for the next phase.
296    async fn retrieve(&mut self) -> Result<Self::Type> {
297        let mut energy_counters = self.energy_counters.clone();
298        for counter in self.energy_counters.values_mut() {
299            counter.reset();
300        }
301
302        let mut lock = self
303            .vram_counters
304            .lock()
305            .map_err(|_| AmdSmiError::MutexPoisoned)?;
306        let mut vram_counters = lock.clone();
307        for counter in lock.values_mut() {
308            counter.reset();
309        }
310
311        let mut lock = self
312            .power_counters
313            .lock()
314            .map_err(|_| AmdSmiError::MutexPoisoned)?;
315        let mut power_counters = lock.clone();
316        for counter in lock.values_mut() {
317            counter.reset();
318        }
319
320        let mut lock = self
321            .utilization_counters
322            .lock()
323            .map_err(|_| AmdSmiError::MutexPoisoned)?;
324        let mut utilization_counters = lock.clone();
325        for counter in lock.values_mut() {
326            counter.reset();
327        }
328
329        let map = self
330            .processors
331            .keys()
332            .map(|index| {
333                let energy = energy_counters.remove(index);
334                let vram = vram_counters.remove(index);
335                let power = power_counters.remove(index);
336                let utilization = utilization_counters.remove(index);
337                let counter = Counter {
338                    energy,
339                    vram,
340                    power,
341                    utilization,
342                };
343                (*index, counter)
344            })
345            .collect();
346
347        Ok(map)
348    }
349
350    /// Creates the polling task if a polling interval has been configured.
351    async fn init(&mut self, _pid: i32) -> Result<()> {
352        self.handle = Some(Self::create_worker(
353            self.hardware.clone(),
354            self.processors.clone(),
355            self.power_counters.clone(),
356            self.vram_counters.clone(),
357            self.utilization_counters.clone(),
358            self.config.poll_interval,
359        )?);
360
361        debug!("AMD SMI source initialized.");
362        Ok(())
363    }
364
365    /// Joins the polling task if it exists.
366    async fn join(&mut self) -> Result<()> {
367        if let Some((cancellation_token, handle)) = self.handle.take() {
368            debug!("Joining AMD SMI source polling task.");
369            cancellation_token.cancel();
370            handle.await??;
371        }
372        Ok(())
373    }
374
375    fn get_sensors(&self) -> Result<Sensors> {
376        let sensors = self
377            .processors
378            .values()
379            .flat_map(|p| {
380                let mut processor_sensors = Vec::new();
381                let uuid = &p.uuid;
382
383                if p.support.contains(ProcessorSupport::Energy)
384                    || p.support.contains(ProcessorSupport::Power)
385                {
386                    processor_sensors.push(Sensor::new(
387                        format!("GPU-{uuid}-energy"),
388                        MICRO_JOULE_UNIT,
389                        Self::get_name(),
390                    ));
391                }
392
393                if p.support.contains(ProcessorSupport::Vram) {
394                    processor_sensors.push(Sensor::new(
395                        format!("GPU-{uuid}-vram_min"),
396                        BYTE_UNIT,
397                        Self::get_name(),
398                    ));
399                    processor_sensors.push(Sensor::new(
400                        format!("GPU-{uuid}-vram_max"),
401                        BYTE_UNIT,
402                        Self::get_name(),
403                    ));
404                }
405
406                if p.support.contains(ProcessorSupport::Utilization) {
407                    processor_sensors.push(Sensor::new(
408                        format!("GPU-{uuid}-utilization_min"),
409                        PERCENT_UNIT,
410                        Self::get_name(),
411                    ));
412                    processor_sensors.push(Sensor::new(
413                        format!("GPU-{uuid}-utilization_max"),
414                        PERCENT_UNIT,
415                        Self::get_name(),
416                    ));
417                }
418
419                processor_sensors
420            })
421            .collect();
422        Ok(sensors)
423    }
424
425    fn to_metrics(&self, result: Self::Type) -> Result<Metrics> {
426        let metrics = result
427            .into_iter()
428            .flat_map(|(index, counter)| {
429                let uuid = &self
430                    .processors
431                    .get(&index)
432                    .ok_or(AmdSmiError::NoSuchDeviceFromIndex(index))?
433                    .uuid;
434
435                let mut processor_metrics = Vec::new();
436
437                let energy = counter
438                    .energy
439                    .map_or_else(|| counter.power.map(|c| c.compute_energy()), |c| c.diff());
440
441                if let Some(energy) = energy {
442                    processor_metrics.push(Metric::new(
443                        format!("GPU-{uuid}-energy"),
444                        energy,
445                        MICRO_JOULE_UNIT,
446                        Self::get_name(),
447                    ));
448                }
449
450                if let Some(vram) = counter.vram
451                    && let Some(min) = vram.min
452                    && let Some(max) = vram.max
453                {
454                    processor_metrics.push(Metric::new(
455                        format!("GPU-{uuid}-vram_min"),
456                        min,
457                        BYTE_UNIT,
458                        Self::get_name(),
459                    ));
460
461                    processor_metrics.push(Metric::new(
462                        format!("GPU-{uuid}-vram_max"),
463                        max,
464                        BYTE_UNIT,
465                        Self::get_name(),
466                    ));
467                }
468
469                if let Some(utilization) = counter.utilization
470                    && let Some(min) = utilization.min
471                    && let Some(max) = utilization.max
472                {
473                    processor_metrics.push(Metric::new(
474                        format!("GPU-{uuid}-utilization_min"),
475                        u64::from(min),
476                        PERCENT_UNIT,
477                        Self::get_name(),
478                    ));
479
480                    processor_metrics.push(Metric::new(
481                        format!("GPU-{uuid}-utilization_max"),
482                        u64::from(max),
483                        PERCENT_UNIT,
484                        Self::get_name(),
485                    ));
486                }
487
488                Ok::<Metrics, AmdSmiError>(processor_metrics)
489            })
490            .flatten()
491            .collect();
492        Ok(metrics)
493    }
494
495    fn get_name() -> &'static str {
496        AMDSMI_SOURCE_NAME
497    }
498
499    fn get_id() -> &'static str {
500        AMDSMI_SOURCE_NAME
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use std::{collections::HashMap, sync::Arc, time::Duration};
507
508    use amdsmi::types::{EnergyCount, GpuUsageInfo};
509    use joule_profiler_core::source::MetricReader;
510    use mockall::predicate::function;
511    use tokio::time::sleep;
512
513    use crate::{
514        AmdSmi, Processor, ProcessorSupport,
515        config::AmdSmiConfig,
516        counters::{Counter, PowerMeasurement},
517        error::AmdSmiError,
518        hardware::MockAmdSmiHardware,
519    };
520
521    fn make_processor(uuid: &str, support: ProcessorSupport) -> Processor {
522        Processor {
523            uuid: uuid.to_owned(),
524            support,
525        }
526    }
527
528    fn processors_map(list: Vec<Processor>) -> Arc<HashMap<usize, Processor>> {
529        Arc::new(list.into_iter().enumerate().collect())
530    }
531
532    fn build_amdsmi(
533        hardware: MockAmdSmiHardware,
534        processors: Arc<HashMap<usize, Processor>>,
535        poll_interval: Duration,
536    ) -> AmdSmi<MockAmdSmiHardware> {
537        AmdSmi {
538            config: AmdSmiConfig {
539                poll_interval,
540                gpus_spec: None,
541            },
542            hardware: Arc::new(hardware),
543            handle: None,
544            processors,
545            energy_counters: HashMap::default(),
546            vram_counters: Arc::default(),
547            power_counters: Arc::default(),
548            utilization_counters: Arc::default(),
549        }
550    }
551
552    fn energy_count(value: u64) -> EnergyCount {
553        EnergyCount {
554            energy_accumulator: value,
555            counter_resolution: 1.0,
556            timestamp: 0,
557        }
558    }
559
560    fn gpu_usage(gpu_usage: u32) -> GpuUsageInfo {
561        GpuUsageInfo {
562            gpu_usage,
563            mem_usage: 0,
564        }
565    }
566
567    fn power_measurement(power: u32) -> PowerMeasurement {
568        PowerMeasurement {
569            timestamp: 0,
570            power,
571        }
572    }
573
574    fn by_uuid(uuid: &'static str) -> impl mockall::Predicate<Processor> {
575        function(move |p: &Processor| p.uuid == uuid)
576    }
577
578    #[test]
579    fn get_sensors_energy_support_emits_energy_sensor() {
580        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Energy)]);
581        let amdsmi = build_amdsmi(
582            MockAmdSmiHardware::default(),
583            processors,
584            Duration::from_secs(1),
585        );
586
587        let sensors = amdsmi.get_sensors().unwrap();
588        assert!(sensors.iter().any(|s| s.name == "GPU-UUID_0-energy"));
589    }
590
591    #[test]
592    fn get_sensors_power_support_emits_energy_sensor() {
593        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Power)]);
594        let amdsmi = build_amdsmi(
595            MockAmdSmiHardware::default(),
596            processors,
597            Duration::from_secs(1),
598        );
599
600        let sensors = amdsmi.get_sensors().unwrap();
601        assert!(sensors.iter().any(|s| s.name == "GPU-UUID_0-energy"));
602    }
603
604    #[test]
605    fn get_sensors_vram_support_emits_vram_sensors() {
606        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Vram)]);
607        let amdsmi = build_amdsmi(
608            MockAmdSmiHardware::default(),
609            processors,
610            Duration::from_secs(1),
611        );
612
613        let sensors = amdsmi.get_sensors().unwrap();
614        assert!(sensors.iter().any(|s| s.name == "GPU-UUID_0-vram_min"));
615        assert!(sensors.iter().any(|s| s.name == "GPU-UUID_0-vram_max"));
616    }
617
618    #[test]
619    fn get_sensors_utilization_support_emits_utilization_sensors() {
620        let processors = processors_map(vec![make_processor(
621            "UUID_0",
622            ProcessorSupport::Utilization,
623        )]);
624        let amdsmi = build_amdsmi(
625            MockAmdSmiHardware::default(),
626            processors,
627            Duration::from_secs(1),
628        );
629
630        let sensors = amdsmi.get_sensors().unwrap();
631        assert!(
632            sensors
633                .iter()
634                .any(|s| s.name == "GPU-UUID_0-utilization_min")
635        );
636        assert!(
637            sensors
638                .iter()
639                .any(|s| s.name == "GPU-UUID_0-utilization_max")
640        );
641    }
642
643    #[test]
644    fn get_sensors_empty_when_no_processors() {
645        let amdsmi = build_amdsmi(
646            MockAmdSmiHardware::default(),
647            Arc::new(HashMap::new()),
648            Duration::from_secs(1),
649        );
650        assert!(amdsmi.get_sensors().unwrap().is_empty());
651    }
652
653    #[test]
654    fn get_sensors_counts_all_sensors_for_full_support() {
655        let processors = processors_map(vec![make_processor(
656            "UUID_0",
657            ProcessorSupport::Energy | ProcessorSupport::Vram | ProcessorSupport::Utilization,
658        )]);
659        let amdsmi = build_amdsmi(
660            MockAmdSmiHardware::default(),
661            processors,
662            Duration::from_secs(1),
663        );
664        assert_eq!(amdsmi.get_sensors().unwrap().len(), 5);
665    }
666
667    #[tokio::test]
668    async fn measure_reads_energy_for_energy_capable_processor() {
669        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Energy)]);
670
671        let mut hw = MockAmdSmiHardware::default();
672        hw.expect_get_energy_count()
673            .with(by_uuid("UUID_0"))
674            .once()
675            .returning(|_| Ok(energy_count(1_000_000)));
676
677        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
678        amdsmi.measure().await.unwrap();
679
680        assert!(amdsmi.energy_counters.contains_key(&0));
681    }
682
683    #[tokio::test]
684    async fn measure_reads_power_for_power_capable_processor() {
685        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Power)]);
686
687        let mut hw = MockAmdSmiHardware::default();
688        hw.expect_get_power()
689            .with(by_uuid("UUID_0"))
690            .once()
691            .returning(|_| Ok(power_measurement(150_000)));
692
693        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
694        amdsmi.measure().await.unwrap();
695
696        assert!(amdsmi.power_counters.lock().unwrap().contains_key(&0));
697    }
698
699    #[tokio::test]
700    async fn measure_reads_vram_for_vram_capable_processor() {
701        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Vram)]);
702
703        let mut hw = MockAmdSmiHardware::default();
704        hw.expect_get_vram_usage()
705            .with(by_uuid("UUID_0"))
706            .once()
707            .returning(|_| Ok(8_000_000_000));
708
709        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
710        amdsmi.measure().await.unwrap();
711
712        assert!(amdsmi.vram_counters.lock().unwrap().contains_key(&0));
713    }
714
715    #[tokio::test]
716    async fn measure_reads_utilization_for_utilization_capable_processor() {
717        let processors = processors_map(vec![make_processor(
718            "UUID_0",
719            ProcessorSupport::Utilization,
720        )]);
721
722        let mut hw = MockAmdSmiHardware::default();
723        hw.expect_get_gpu_activity()
724            .with(by_uuid("UUID_0"))
725            .once()
726            .returning(|_| Ok(gpu_usage(75)));
727
728        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
729        amdsmi.measure().await.unwrap();
730
731        assert!(amdsmi.utilization_counters.lock().unwrap().contains_key(&0));
732    }
733
734    #[tokio::test]
735    async fn measure_skips_energy_and_power_for_vram_only_processor() {
736        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Vram)]);
737
738        let mut hw = MockAmdSmiHardware::default();
739        hw.expect_get_energy_count().never();
740        hw.expect_get_power().never();
741        hw.expect_get_vram_usage()
742            .once()
743            .returning(|_| Ok(1_000_000));
744
745        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
746        amdsmi.measure().await.unwrap();
747
748        assert!(amdsmi.energy_counters.is_empty());
749        assert!(amdsmi.power_counters.lock().unwrap().is_empty());
750    }
751
752    #[tokio::test]
753    async fn measure_propagates_energy_error() {
754        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Energy)]);
755
756        let mut hw = MockAmdSmiHardware::default();
757        hw.expect_get_energy_count().once().returning(|_| {
758            Err(AmdSmiError::AmdSmiError(
759                amdsmi::error::AmdSmiError::NotSupported,
760            ))
761        });
762
763        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
764        assert!(amdsmi.measure().await.is_err());
765    }
766
767    #[tokio::test]
768    async fn measure_propagates_power_error() {
769        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Power)]);
770
771        let mut hw = MockAmdSmiHardware::default();
772        hw.expect_get_power().once().returning(|_| {
773            Err(AmdSmiError::AmdSmiError(
774                amdsmi::error::AmdSmiError::NotSupported,
775            ))
776        });
777
778        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
779        assert!(amdsmi.measure().await.is_err());
780    }
781
782    #[tokio::test]
783    async fn measure_propagates_vram_error() {
784        let processors = processors_map(vec![make_processor("UUID_0", ProcessorSupport::Vram)]);
785
786        let mut hw = MockAmdSmiHardware::default();
787        hw.expect_get_vram_usage().once().returning(|_| {
788            Err(AmdSmiError::AmdSmiError(
789                amdsmi::error::AmdSmiError::NotSupported,
790            ))
791        });
792
793        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
794        assert!(amdsmi.measure().await.is_err());
795    }
796
797    #[tokio::test]
798    async fn measure_propagates_utilization_error() {
799        let processors = processors_map(vec![make_processor(
800            "UUID_0",
801            ProcessorSupport::Utilization,
802        )]);
803
804        let mut hw = MockAmdSmiHardware::default();
805        hw.expect_get_gpu_activity().once().returning(|_| {
806            Err(AmdSmiError::AmdSmiError(
807                amdsmi::error::AmdSmiError::NotSupported,
808            ))
809        });
810
811        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
812        assert!(amdsmi.measure().await.is_err());
813    }
814
815    #[tokio::test]
816    async fn retrieve_returns_counters_and_resets_them() {
817        let processors = processors_map(vec![make_processor(
818            "UUID_0",
819            ProcessorSupport::Energy | ProcessorSupport::Vram,
820        )]);
821
822        let mut hw = MockAmdSmiHardware::default();
823        hw.expect_get_energy_count()
824            .returning(|_| Ok(energy_count(10_000)));
825        hw.expect_get_vram_usage().returning(|_| Ok(1_000_000));
826
827        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
828        amdsmi.measure().await.unwrap();
829        amdsmi.measure().await.unwrap();
830
831        let result = amdsmi.retrieve().await.unwrap();
832        assert!(result.contains_key(&0));
833
834        let result2 = amdsmi.retrieve().await.unwrap();
835        let counter: &Counter = result2.get(&0).unwrap();
836        assert!(
837            counter
838                .energy
839                .as_ref()
840                .and_then(super::counters::EnergyCounter::diff)
841                .is_none_or(|v| v == 0)
842        );
843    }
844
845    #[tokio::test]
846    async fn retrieve_includes_entry_for_every_processor() {
847        let processors = processors_map(vec![
848            make_processor("UUID_0", ProcessorSupport::Energy),
849            make_processor("UUID_1", ProcessorSupport::Power),
850            make_processor("UUID_2", ProcessorSupport::Vram),
851        ]);
852
853        let mut hw = MockAmdSmiHardware::default();
854        hw.expect_get_energy_count()
855            .returning(|_| Ok(energy_count(1_000)));
856        hw.expect_get_power()
857            .returning(|_| Ok(power_measurement(5_000)));
858        hw.expect_get_vram_usage().returning(|_| Ok(1_000_000));
859
860        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_secs(1));
861        amdsmi.measure().await.unwrap();
862
863        let result = amdsmi.retrieve().await.unwrap();
864        assert_eq!(result.len(), 3);
865        for index in [0, 1, 2] {
866            assert!(result.contains_key(&index));
867        }
868    }
869
870    #[tokio::test]
871    async fn worker_polls_counters_and_can_be_cancelled() {
872        let processors = processors_map(vec![make_processor(
873            "UUID_0",
874            ProcessorSupport::Power | ProcessorSupport::Vram | ProcessorSupport::Utilization,
875        )]);
876
877        let mut hw = MockAmdSmiHardware::default();
878        hw.expect_get_power()
879            .returning(|_| Ok(power_measurement(5_000)));
880        hw.expect_get_vram_usage().returning(|_| Ok(1_000_000));
881        hw.expect_get_gpu_activity()
882            .returning(|_| Ok(gpu_usage(42)));
883
884        let mut amdsmi = build_amdsmi(hw, processors, Duration::from_millis(20));
885        amdsmi.init(0).await.unwrap();
886
887        sleep(Duration::from_millis(80)).await;
888
889        amdsmi.join().await.unwrap();
890
891        assert!(amdsmi.power_counters.lock().unwrap().contains_key(&0));
892    }
893}