cgpu 0.1.0

A tunable GPU compute executor with automatic CPU fallback, byte-based batching, and inline shader generation.
Documentation
use std::time::Duration;
use std::time::Instant;

use crate::batch::GpuBatchPlan;
use crate::config::{ExecutionMode, TelemetrySink};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TelemetryPhase {
    Plan,
    Encode,
    Gpu,
    Readback,
    Decode,
    CpuFallback,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct TelemetryReport {
    pub label: String,
    pub execution_mode: Option<ExecutionMode>,
    pub bytes_in: usize,
    pub bytes_out: usize,
    pub batch_count: usize,
    pub plan_time: Duration,
    pub encode_time: Duration,
    pub gpu_time: Duration,
    pub readback_time: Duration,
    pub decode_time: Duration,
    pub cpu_fallback_time: Duration,
}

impl TelemetryReport {
    pub fn new(label: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            ..Self::default()
        }
    }

    pub fn add_elapsed(&mut self, phase: TelemetryPhase, elapsed: Duration) {
        match phase {
            TelemetryPhase::Plan => self.plan_time += elapsed,
            TelemetryPhase::Encode => self.encode_time += elapsed,
            TelemetryPhase::Gpu => self.gpu_time += elapsed,
            TelemetryPhase::Readback => self.readback_time += elapsed,
            TelemetryPhase::Decode => self.decode_time += elapsed,
            TelemetryPhase::CpuFallback => self.cpu_fallback_time += elapsed,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GpuPhase {
    Pack,
    Gpu,
    Readback,
    Post,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct GpuTelemetry {
    pub pack_ms: f64,
    pub gpu_ms: f64,
    pub readback_ms: f64,
    pub post_ms: f64,
    pub bytes_in: usize,
    pub bytes_out: usize,
    pub batch_count: usize,
}

impl GpuTelemetry {
    pub fn record_plan(&mut self, plan: &GpuBatchPlan) {
        self.bytes_in = self.bytes_in.saturating_add(plan.bytes_in());
        self.bytes_out = self.bytes_out.saturating_add(plan.bytes_out());
        self.batch_count = self.batch_count.saturating_add(plan.batch_count());
    }

    pub fn add_elapsed_ms(&mut self, phase: GpuPhase, elapsed_ms: f64) {
        match phase {
            GpuPhase::Pack => self.pack_ms += elapsed_ms,
            GpuPhase::Gpu => self.gpu_ms += elapsed_ms,
            GpuPhase::Readback => self.readback_ms += elapsed_ms,
            GpuPhase::Post => self.post_ms += elapsed_ms,
        }
    }

    pub fn merge(&mut self, other: &Self) {
        self.pack_ms += other.pack_ms;
        self.gpu_ms += other.gpu_ms;
        self.readback_ms += other.readback_ms;
        self.post_ms += other.post_ms;
        self.bytes_in = self.bytes_in.saturating_add(other.bytes_in);
        self.bytes_out = self.bytes_out.saturating_add(other.bytes_out);
        self.batch_count = self.batch_count.saturating_add(other.batch_count);
    }

    pub fn log_line(&self, label: impl AsRef<str>) -> String {
        format!(
            "{} pack_ms={:.3} gpu_ms={:.3} readback_ms={:.3} post_ms={:.3} bytes_in={} bytes_out={} batch_count={}",
            label.as_ref(),
            self.pack_ms,
            self.gpu_ms,
            self.readback_ms,
            self.post_ms,
            self.bytes_in,
            self.bytes_out,
            self.batch_count
        )
    }
}

pub fn measure_phase<T>(
    telemetry: &mut GpuTelemetry,
    phase: GpuPhase,
    work: impl FnOnce() -> T,
) -> T {
    let started = Instant::now();
    let result = work();
    telemetry.add_elapsed_ms(phase, started.elapsed().as_secs_f64() * 1000.0);
    result
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct TelemetryCollector {
    reports: Vec<TelemetryReport>,
}

impl TelemetryCollector {
    pub fn record(&mut self, report: TelemetryReport, sink: &TelemetrySink) {
        match sink {
            TelemetrySink::Log => {}
            TelemetrySink::Callback(callback) => callback(&report),
            TelemetrySink::Buffer(sender) => {
                let _ = sender.send(report.clone());
            }
        }
        self.reports.push(report);
    }

    pub fn latest(&self) -> Option<&TelemetryReport> {
        self.reports.last()
    }

    pub fn reports(&self) -> &[TelemetryReport] {
        &self.reports
    }

    pub fn clear(&mut self) {
        self.reports.clear();
    }
}