1use 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
40type 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 #[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 uuid: UUID,
75
76 support: ProcessorSupport,
78}
79
80pub struct AmdSmi<H: AmdSmiHardware = AmdSmiWrapperHardware> {
81 config: AmdSmiConfig,
83
84 hardware: Arc<H>,
86
87 handle: Option<WorkerHandle>,
89
90 processors: Arc<HashMap<usize, Processor>>,
92
93 energy_counters: HashMap<usize, EnergyCounter>,
95
96 vram_counters: Arc<Mutex<HashMap<usize, VramCounter>>>,
98
99 power_counters: Arc<Mutex<HashMap<usize, PowerCounter>>>,
101
102 utilization_counters: Arc<Mutex<HashMap<usize, UtilizationCounter>>>,
104}
105
106impl<H: AmdSmiHardware> AmdSmi<H> {
107 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 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 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 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 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 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 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}