mecha10-diagnostics 0.1.0

Diagnostics and metrics collection for Mecha10 robotics framework
Documentation
//! Godot simulation metrics collector
//!
//! Tracks Godot performance, scene metrics, and connection health.

use crate::metrics::Counter;
use crate::topics::*;
use crate::types::*;
use anyhow::Result;
use mecha10_core::prelude::*;
use mecha10_core::topics::Topic;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;

/// Performance metrics input parameters
#[derive(Debug, Clone)]
pub struct PerformanceMetricsInput {
    pub fps: f64,
    pub target_fps: f64,
    pub frame_time_ms: f64,
    pub physics_time_ms: f64,
    pub render_time_ms: f64,
    pub idle_time_ms: f64,
}

/// Scene metrics input parameters
#[derive(Debug, Clone)]
pub struct SceneMetricsInput {
    pub node_count: u64,
    pub physics_body_count: u64,
    pub memory_usage_bytes: u64,
    pub static_memory_bytes: u64,
    pub dynamic_memory_bytes: u64,
    pub object_count: u64,
    pub resource_count: u64,
}

/// Godot diagnostics collector
///
/// Tracks:
/// - Performance: FPS, frame time, physics time, render time
/// - Scene: Node count, memory usage, object count
/// - Connection: WebSocket health, reconnects, message timestamps
pub struct GodotCollector {
    source: String,

    // Performance tracking
    total_frames: Counter,
    dropped_frames: Counter,

    // Connection tracking
    control_connected: Arc<AtomicU64>, // 0 = disconnected, 1 = connected
    camera_connected: Arc<AtomicU64>,
    control_reconnects: Counter,
    camera_reconnects: Counter,
    last_control_message_us: Arc<AtomicU64>,
    last_camera_frame_us: Arc<AtomicU64>,

    // Connection uptime tracking
    control_connected_since: Arc<parking_lot::Mutex<Option<Instant>>>,
    camera_connected_since: Arc<parking_lot::Mutex<Option<Instant>>>,

    // URLs
    control_url: String,
    camera_url: String,
}

impl GodotCollector {
    /// Create a new Godot collector
    pub fn new(source: impl Into<String>, control_url: String, camera_url: String) -> Self {
        Self {
            source: source.into(),
            total_frames: Counter::new(),
            dropped_frames: Counter::new(),
            control_connected: Arc::new(AtomicU64::new(0)),
            camera_connected: Arc::new(AtomicU64::new(0)),
            control_reconnects: Counter::new(),
            camera_reconnects: Counter::new(),
            last_control_message_us: Arc::new(AtomicU64::new(0)),
            last_camera_frame_us: Arc::new(AtomicU64::new(0)),
            control_connected_since: Arc::new(parking_lot::Mutex::new(None)),
            camera_connected_since: Arc::new(parking_lot::Mutex::new(None)),
            control_url,
            camera_url,
        }
    }

    // ===== Event Recording Methods =====

    /// Record Godot frame rendered
    #[inline]
    pub fn record_frame(&self) {
        self.total_frames.inc();
    }

    /// Record dropped frame
    #[inline]
    pub fn record_dropped_frame(&self) {
        self.dropped_frames.inc();
    }

    /// Record control connection state change
    pub fn set_control_connected(&self, connected: bool) {
        let was_connected = self.control_connected.load(Ordering::Relaxed) == 1;
        self.control_connected
            .store(if connected { 1 } else { 0 }, Ordering::Relaxed);

        if connected && !was_connected {
            // Connection established
            *self.control_connected_since.lock() = Some(Instant::now());
            if self.total_frames.get() > 0 {
                // This is a reconnect (not initial connection)
                self.control_reconnects.inc();
            }
        } else if !connected && was_connected {
            // Connection lost
            *self.control_connected_since.lock() = None;
        }
    }

    /// Record camera connection state change
    pub fn set_camera_connected(&self, connected: bool) {
        let was_connected = self.camera_connected.load(Ordering::Relaxed) == 1;
        self.camera_connected
            .store(if connected { 1 } else { 0 }, Ordering::Relaxed);

        if connected && !was_connected {
            // Connection established
            *self.camera_connected_since.lock() = Some(Instant::now());
            if self.total_frames.get() > 0 {
                // This is a reconnect (not initial connection)
                self.camera_reconnects.inc();
            }
        } else if !connected && was_connected {
            // Connection lost
            *self.camera_connected_since.lock() = None;
        }
    }

    /// Record control message received
    #[inline]
    pub fn record_control_message(&self, timestamp_us: u64) {
        self.last_control_message_us.store(timestamp_us, Ordering::Relaxed);
    }

    /// Record camera frame received
    #[inline]
    pub fn record_camera_frame(&self, timestamp_us: u64) {
        self.last_camera_frame_us.store(timestamp_us, Ordering::Relaxed);
    }

    // ===== Publishing Methods =====

    /// Publish performance metrics
    ///
    /// Note: FPS, frame times, etc. should be provided by Godot itself or calculated
    /// externally. This method publishes the aggregated data.
    pub async fn publish_performance_metrics(&self, ctx: &Context, input: PerformanceMetricsInput) -> Result<()> {
        let metrics = GodotPerformanceMetrics {
            fps: input.fps,
            target_fps: input.target_fps,
            frame_time_ms: input.frame_time_ms,
            physics_time_ms: input.physics_time_ms,
            render_time_ms: input.render_time_ms,
            idle_time_ms: input.idle_time_ms,
            total_frames: self.total_frames.get(),
            dropped_frames: self.dropped_frames.get(),
        };

        let msg = DiagnosticMessage::new(&self.source, metrics);
        ctx.publish_to(
            Topic::<DiagnosticMessage<GodotPerformanceMetrics>>::new(TOPIC_DIAGNOSTICS_GODOT_PERFORMANCE),
            &msg,
        )
        .await?;

        Ok(())
    }

    /// Publish scene metrics
    ///
    /// These metrics should be provided by Godot's performance monitoring API.
    pub async fn publish_scene_metrics(&self, ctx: &Context, input: SceneMetricsInput) -> Result<()> {
        let metrics = GodotSceneMetrics {
            node_count: input.node_count,
            physics_body_count: input.physics_body_count,
            memory_usage_bytes: input.memory_usage_bytes,
            static_memory_bytes: input.static_memory_bytes,
            dynamic_memory_bytes: input.dynamic_memory_bytes,
            object_count: input.object_count,
            resource_count: input.resource_count,
        };

        let msg = DiagnosticMessage::new(&self.source, metrics);
        ctx.publish_to(
            Topic::<DiagnosticMessage<GodotSceneMetrics>>::new(TOPIC_DIAGNOSTICS_GODOT_SCENE),
            &msg,
        )
        .await?;

        Ok(())
    }

    /// Publish connection health metrics
    pub async fn publish_connection_metrics(&self, ctx: &Context) -> Result<()> {
        let control_connected = self.control_connected.load(Ordering::Relaxed) == 1;
        let camera_connected = self.camera_connected.load(Ordering::Relaxed) == 1;

        let control_uptime_seconds = self
            .control_connected_since
            .lock()
            .as_ref()
            .map(|start| start.elapsed().as_secs())
            .unwrap_or(0);

        let camera_uptime_seconds = self
            .camera_connected_since
            .lock()
            .as_ref()
            .map(|start| start.elapsed().as_secs())
            .unwrap_or(0);

        let metrics = GodotConnectionMetrics {
            control_connected,
            camera_connected,
            control_uptime_seconds,
            camera_uptime_seconds,
            control_reconnects: self.control_reconnects.get(),
            camera_reconnects: self.camera_reconnects.get(),
            last_control_message_us: self.last_control_message_us.load(Ordering::Relaxed),
            last_camera_frame_us: self.last_camera_frame_us.load(Ordering::Relaxed),
            control_url: self.control_url.clone(),
            camera_url: self.camera_url.clone(),
        };

        let msg = DiagnosticMessage::new(&self.source, metrics);
        ctx.publish_to(
            Topic::<DiagnosticMessage<GodotConnectionMetrics>>::new(TOPIC_DIAGNOSTICS_GODOT_CONNECTION),
            &msg,
        )
        .await?;

        Ok(())
    }

    /// Publish all Godot metrics (convenience method)
    ///
    /// Performance and scene metrics require external data from Godot.
    /// Connection metrics are tracked internally.
    pub async fn publish_connection_only(&self, ctx: &Context) -> Result<()> {
        self.publish_connection_metrics(ctx).await?;
        Ok(())
    }
}