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;
#[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,
}
#[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,
}
pub struct GodotCollector {
source: String,
total_frames: Counter,
dropped_frames: Counter,
control_connected: Arc<AtomicU64>, camera_connected: Arc<AtomicU64>,
control_reconnects: Counter,
camera_reconnects: Counter,
last_control_message_us: Arc<AtomicU64>,
last_camera_frame_us: Arc<AtomicU64>,
control_connected_since: Arc<parking_lot::Mutex<Option<Instant>>>,
camera_connected_since: Arc<parking_lot::Mutex<Option<Instant>>>,
control_url: String,
camera_url: String,
}
impl GodotCollector {
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,
}
}
#[inline]
pub fn record_frame(&self) {
self.total_frames.inc();
}
#[inline]
pub fn record_dropped_frame(&self) {
self.dropped_frames.inc();
}
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 {
*self.control_connected_since.lock() = Some(Instant::now());
if self.total_frames.get() > 0 {
self.control_reconnects.inc();
}
} else if !connected && was_connected {
*self.control_connected_since.lock() = None;
}
}
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 {
*self.camera_connected_since.lock() = Some(Instant::now());
if self.total_frames.get() > 0 {
self.camera_reconnects.inc();
}
} else if !connected && was_connected {
*self.camera_connected_since.lock() = None;
}
}
#[inline]
pub fn record_control_message(&self, timestamp_us: u64) {
self.last_control_message_us.store(timestamp_us, Ordering::Relaxed);
}
#[inline]
pub fn record_camera_frame(&self, timestamp_us: u64) {
self.last_camera_frame_us.store(timestamp_us, Ordering::Relaxed);
}
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(())
}
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(())
}
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(())
}
pub async fn publish_connection_only(&self, ctx: &Context) -> Result<()> {
self.publish_connection_metrics(ctx).await?;
Ok(())
}
}