mecha10-diagnostics 0.1.25

Diagnostics and metrics collection for Mecha10 robotics framework
Documentation
//! Metric helper types for tracking and aggregating diagnostics
//!
//! Provides efficient thread-safe metric types:
//! - `Counter`: Monotonically increasing counter
//! - `Gauge`: Current value that can go up or down
//! - `Histogram`: Latency/duration tracking with percentiles

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

/// Thread-safe monotonic counter
///
/// Use for counting events (frames received, errors, etc.)
#[derive(Debug, Clone)]
pub struct Counter {
    value: Arc<AtomicU64>,
}

impl Counter {
    /// Create a new counter starting at 0
    pub fn new() -> Self {
        Self {
            value: Arc::new(AtomicU64::new(0)),
        }
    }

    /// Increment counter by 1
    pub fn inc(&self) {
        self.value.fetch_add(1, Ordering::Relaxed);
    }

    /// Increment counter by n
    pub fn add(&self, n: u64) {
        self.value.fetch_add(n, Ordering::Relaxed);
    }

    /// Get current value
    pub fn get(&self) -> u64 {
        self.value.load(Ordering::Relaxed)
    }

    /// Reset counter to 0
    pub fn reset(&self) {
        self.value.store(0, Ordering::Relaxed);
    }
}

impl Default for Counter {
    fn default() -> Self {
        Self::new()
    }
}

/// Thread-safe gauge (value that can go up or down)
///
/// Use for current values (queue depth, active connections, etc.)
#[derive(Debug, Clone)]
pub struct Gauge {
    value: Arc<AtomicU64>,
}

impl Gauge {
    /// Create a new gauge starting at 0
    pub fn new() -> Self {
        Self {
            value: Arc::new(AtomicU64::new(0)),
        }
    }

    /// Set gauge to value
    pub fn set(&self, value: u64) {
        self.value.store(value, Ordering::Relaxed);
    }

    /// Increment gauge by 1
    pub fn inc(&self) {
        self.value.fetch_add(1, Ordering::Relaxed);
    }

    /// Decrement gauge by 1
    pub fn dec(&self) {
        self.value.fetch_sub(1, Ordering::Relaxed);
    }

    /// Add n to gauge
    pub fn add(&self, n: u64) {
        self.value.fetch_add(n, Ordering::Relaxed);
    }

    /// Subtract n from gauge
    pub fn sub(&self, n: u64) {
        self.value.fetch_sub(n, Ordering::Relaxed);
    }

    /// Get current value
    pub fn get(&self) -> u64 {
        self.value.load(Ordering::Relaxed)
    }
}

impl Default for Gauge {
    fn default() -> Self {
        Self::new()
    }
}

/// Histogram for tracking latency/duration distributions
///
/// Uses HdrHistogram for efficient percentile calculation.
/// NOTE: Not thread-safe - wrap in Mutex if needed for concurrent updates.
pub struct Histogram {
    inner: hdrhistogram::Histogram<u64>,
}

impl Histogram {
    /// Create a new histogram
    ///
    /// # Arguments
    /// * `max_value` - Maximum value to track (e.g., 60_000_000 for 60 seconds in microseconds)
    /// * `significant_figures` - Precision (1-5, default 3)
    pub fn new(max_value: u64, significant_figures: u8) -> Result<Self, String> {
        hdrhistogram::Histogram::new_with_max(max_value, significant_figures)
            .map(|inner| Self { inner })
            .map_err(|e| format!("Failed to create histogram: {}", e))
    }

    /// Create a histogram for latency tracking (up to 60 seconds)
    pub fn for_latency() -> Result<Self, String> {
        Self::new(60_000_000, 3) // 60s in microseconds, 3 sig figs
    }

    /// Create a histogram for duration tracking (up to 10 seconds)
    pub fn for_duration() -> Result<Self, String> {
        Self::new(10_000_000, 3) // 10s in microseconds, 3 sig figs
    }

    /// Record a value
    pub fn record(&mut self, value: u64) {
        let _ = self.inner.record(value);
    }

    /// Get count of recorded values
    pub fn count(&self) -> u64 {
        self.inner.len()
    }

    /// Get minimum value
    pub fn min(&self) -> u64 {
        self.inner.min()
    }

    /// Get maximum value
    pub fn max(&self) -> u64 {
        self.inner.max()
    }

    /// Get mean value
    pub fn mean(&self) -> f64 {
        self.inner.mean()
    }

    /// Get value at percentile (0.0-100.0)
    pub fn value_at_percentile(&self, percentile: f64) -> u64 {
        self.inner.value_at_percentile(percentile)
    }

    /// Get P50 (median)
    pub fn p50(&self) -> u64 {
        self.value_at_percentile(50.0)
    }

    /// Get P95
    pub fn p95(&self) -> u64 {
        self.value_at_percentile(95.0)
    }

    /// Get P99
    pub fn p99(&self) -> u64 {
        self.value_at_percentile(99.0)
    }

    /// Reset histogram
    pub fn reset(&mut self) {
        self.inner.reset();
    }
}

impl std::fmt::Debug for Histogram {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Histogram")
            .field("count", &self.count())
            .field("min", &self.min())
            .field("max", &self.max())
            .field("mean", &self.mean())
            .field("p50", &self.p50())
            .field("p95", &self.p95())
            .field("p99", &self.p99())
            .finish()
    }
}

/// Get current timestamp in microseconds since Unix epoch
fn now_micros() -> u64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros() as u64
}

/// Rate calculator for computing per-second rates
///
/// Tracks value changes over time to compute rates like frames/sec, bytes/sec, etc.
#[derive(Debug, Clone)]
pub struct RateCalculator {
    last_value: Arc<AtomicU64>,
    last_timestamp: Arc<AtomicU64>,
}

impl RateCalculator {
    /// Create a new rate calculator
    pub fn new() -> Self {
        Self {
            last_value: Arc::new(AtomicU64::new(0)),
            last_timestamp: Arc::new(AtomicU64::new(now_micros())),
        }
    }

    /// Calculate rate (per second) based on new value
    ///
    /// Returns None if not enough time has passed for accurate calculation
    pub fn calculate(&self, current_value: u64) -> Option<f64> {
        let now = now_micros();
        let last_value = self.last_value.load(Ordering::Relaxed);
        let last_timestamp = self.last_timestamp.load(Ordering::Relaxed);

        // Need at least 100ms between samples
        let elapsed_us = now.saturating_sub(last_timestamp);
        if elapsed_us < 100_000 {
            return None;
        }

        // Calculate rate
        let value_delta = current_value.saturating_sub(last_value);
        let elapsed_sec = elapsed_us as f64 / 1_000_000.0;
        let rate = value_delta as f64 / elapsed_sec;

        // Update last values
        self.last_value.store(current_value, Ordering::Relaxed);
        self.last_timestamp.store(now, Ordering::Relaxed);

        Some(rate)
    }

    /// Reset the calculator
    pub fn reset(&self) {
        self.last_value.store(0, Ordering::Relaxed);
        self.last_timestamp.store(now_micros(), Ordering::Relaxed);
    }
}

impl Default for RateCalculator {
    fn default() -> Self {
        Self::new()
    }
}