gpu-histop 0.1.0

High-resolution GPU history monitor for NVIDIA, AMDGPU, and Apple Silicon
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};

use crossbeam_channel::{Receiver, unbounded};

use crate::backend::GpuBackend;
use crate::model::GpuSample;

#[derive(Debug)]
pub enum SamplerEvent {
    Samples(Vec<GpuSample>),
    Error(String),
}

pub struct SamplerHandle {
    stop: Arc<AtomicBool>,
    join: Option<JoinHandle<()>>,
}

impl SamplerHandle {
    pub fn stop(mut self) {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(join) = self.join.take() {
            let _ = join.join();
        }
    }
}

impl Drop for SamplerHandle {
    fn drop(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
    }
}

pub fn spawn_sampler(
    mut backend: Box<dyn GpuBackend>,
    interval: Duration,
) -> (Receiver<SamplerEvent>, SamplerHandle) {
    let (tx, rx) = unbounded();
    let stop = Arc::new(AtomicBool::new(false));
    let thread_stop = Arc::clone(&stop);

    let join = thread::Builder::new()
        .name("gpu-histop-sampler".to_owned())
        .spawn(move || {
            while !thread_stop.load(Ordering::Relaxed) {
                let started = Instant::now();
                let event = match backend.sample() {
                    Ok(samples) => SamplerEvent::Samples(samples),
                    Err(error) => SamplerEvent::Error(error.to_string()),
                };

                if tx.send(event).is_err() {
                    break;
                }

                sleep_until_next_tick(interval, started, &thread_stop);
            }
        })
        .expect("failed to spawn sampler thread");

    (
        rx,
        SamplerHandle {
            stop,
            join: Some(join),
        },
    )
}

fn sleep_until_next_tick(interval: Duration, started: Instant, stop: &AtomicBool) {
    let elapsed = started.elapsed();
    let mut remaining = interval.saturating_sub(elapsed);
    while !remaining.is_zero() && !stop.load(Ordering::Relaxed) {
        let nap = remaining.min(Duration::from_millis(20));
        thread::sleep(nap);
        remaining = remaining.saturating_sub(nap);
    }
}