Skip to main content

trustformers_debug/
realtime_dashboard.rs

1//! Real-Time Debugging Dashboard
2//!
3//! This module provides a modern, real-time debugging dashboard with WebSocket support,
4//! interactive visualizations, and live data streaming for comprehensive neural network monitoring.
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, VecDeque};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11use sysinfo::System;
12use tokio::sync::broadcast;
13use tokio::time::interval;
14use tokio_stream::wrappers::BroadcastStream;
15use uuid::Uuid;
16
17/// Number of most-recent samples of a category examined for a *sustained*
18/// threshold breach (as opposed to a single noisy spike).
19const SUSTAINED_BREACH_WINDOW: usize = 5;
20
21/// Configuration for the real-time dashboard
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct DashboardConfig {
24    /// Port for WebSocket server
25    pub websocket_port: u16,
26    /// Update frequency in milliseconds
27    pub update_frequency_ms: u64,
28    /// Maximum number of data points to keep in memory
29    pub max_data_points: usize,
30    /// Enable GPU monitoring
31    pub enable_gpu_monitoring: bool,
32    /// Enable memory profiling
33    pub enable_memory_profiling: bool,
34    /// Enable network traffic monitoring
35    pub enable_network_monitoring: bool,
36    /// Enable performance alerts
37    pub enable_performance_alerts: bool,
38    /// Alert thresholds
39    pub alert_thresholds: AlertThresholds,
40}
41
42impl Default for DashboardConfig {
43    fn default() -> Self {
44        Self {
45            websocket_port: 8080,
46            update_frequency_ms: 100,
47            max_data_points: 1000,
48            enable_gpu_monitoring: true,
49            enable_memory_profiling: true,
50            enable_network_monitoring: false,
51            enable_performance_alerts: true,
52            alert_thresholds: AlertThresholds::default(),
53        }
54    }
55}
56
57/// Alert threshold configuration
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct AlertThresholds {
60    /// Memory usage threshold (percentage)
61    pub memory_threshold: f64,
62    /// GPU utilization threshold (percentage)
63    pub gpu_utilization_threshold: f64,
64    /// Temperature threshold (Celsius)
65    pub temperature_threshold: f64,
66    /// Loss spike threshold
67    pub loss_spike_threshold: f64,
68    /// Gradient norm threshold
69    pub gradient_norm_threshold: f64,
70}
71
72impl Default for AlertThresholds {
73    fn default() -> Self {
74        Self {
75            memory_threshold: 90.0,
76            gpu_utilization_threshold: 95.0,
77            temperature_threshold: 80.0,
78            loss_spike_threshold: 2.0,
79            gradient_norm_threshold: 10.0,
80        }
81    }
82}
83
84/// Real-time metric data point
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct MetricDataPoint {
87    pub timestamp: u64,
88    pub value: f64,
89    pub label: String,
90    pub category: MetricCategory,
91}
92
93/// Categories of metrics for organization
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
95pub enum MetricCategory {
96    Training,
97    Memory,
98    GPU,
99    Network,
100    Performance,
101    Custom(String),
102}
103
104/// Dashboard alert
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct DashboardAlert {
107    pub id: String,
108    pub timestamp: u64,
109    pub severity: AlertSeverity,
110    pub category: MetricCategory,
111    pub title: String,
112    pub message: String,
113    pub value: Option<f64>,
114    pub threshold: Option<f64>,
115}
116
117/// Alert severity levels
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub enum AlertSeverity {
120    Info,
121    Warning,
122    Error,
123    Critical,
124}
125
126/// WebSocket message types
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(tag = "type")]
129pub enum WebSocketMessage {
130    MetricUpdate {
131        data: Vec<MetricDataPoint>,
132    },
133    Alert {
134        alert: DashboardAlert,
135    },
136    ConfigUpdate {
137        config: DashboardConfig,
138    },
139    SessionInfo {
140        session_id: String,
141        uptime: u64,
142    },
143    HistoricalData {
144        category: MetricCategory,
145        data: Vec<MetricDataPoint>,
146    },
147    SystemStats {
148        stats: SystemStats,
149    },
150    #[serde(untagged)]
151    Generic {
152        message_type: String,
153        data: serde_json::Value,
154        timestamp: u64,
155        session_id: String,
156    },
157}
158
159/// Anomaly detection result
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct AnomalyDetection {
162    pub timestamp: u64,
163    pub value: f64,
164    pub expected_range: (f64, f64),
165    pub anomaly_type: AnomalyType,
166    pub confidence_score: f64,
167    pub category: MetricCategory,
168    pub description: String,
169}
170
171/// Types of anomalies that can be detected
172#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
173pub enum AnomalyType {
174    Spike,
175    Drop,
176    GradualIncrease,
177    GradualDecrease,
178    Outlier,
179}
180
181/// Advanced dashboard visualization data
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct DashboardVisualizationData {
184    pub heatmap_data: HashMap<MetricCategory, HeatmapData>,
185    pub time_series_data: HashMap<MetricCategory, Vec<TimeSeriesPoint>>,
186    pub correlation_matrix: Vec<Vec<f64>>,
187    pub performance_distribution: HashMap<MetricCategory, HistogramData>,
188    pub generated_at: u64,
189    pub session_id: String,
190}
191
192/// Heatmap data for metric visualization
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct HeatmapData {
195    pub intensity: f64,
196    pub normalized_intensity: f64,
197    pub data_points: usize,
198    pub timestamp: u64,
199}
200
201/// Time series data point for trend visualization
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct TimeSeriesPoint {
204    pub timestamp: u64,
205    pub value: f64,
206    pub label: String,
207}
208
209/// Histogram data for performance distribution analysis
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct HistogramData {
212    pub bins: Vec<HistogramBin>,
213    pub max_frequency: usize,
214}
215
216/// Individual histogram bin
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct HistogramBin {
219    pub range_start: f64,
220    pub range_end: f64,
221    pub frequency: usize,
222}
223
224/// Performance prediction result
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct PerformancePrediction {
227    pub category: MetricCategory,
228    pub predicted_value: f64,
229    pub confidence_interval: (f64, f64),
230    pub trend_direction: TrendDirection,
231    pub trend_strength: f64,
232    pub prediction_horizon_hours: u64,
233    pub model_accuracy: f64,
234    pub generated_at: u64,
235    pub recommendations: Vec<String>,
236}
237
238/// Trend direction for predictions
239#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
240pub enum TrendDirection {
241    Increasing,
242    Decreasing,
243    Stable,
244}
245
246/// Dashboard theme configuration
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct DashboardTheme {
249    pub name: String,
250    pub primary_color: String,
251    pub secondary_color: String,
252    pub background_color: String,
253    pub text_color: String,
254    pub accent_color: String,
255    pub chart_colors: Vec<String>,
256    pub dark_mode: bool,
257    pub font_family: String,
258    pub border_radius: u8,
259}
260
261impl Default for DashboardTheme {
262    fn default() -> Self {
263        Self {
264            name: "Default".to_string(),
265            primary_color: "#3b82f6".to_string(),
266            secondary_color: "#64748b".to_string(),
267            background_color: "#ffffff".to_string(),
268            text_color: "#1f2937".to_string(),
269            accent_color: "#10b981".to_string(),
270            chart_colors: vec![
271                "#3b82f6".to_string(),
272                "#ef4444".to_string(),
273                "#10b981".to_string(),
274                "#f59e0b".to_string(),
275                "#8b5cf6".to_string(),
276            ],
277            dark_mode: false,
278            font_family: "Inter, sans-serif".to_string(),
279            border_radius: 8,
280        }
281    }
282}
283
284/// Export format options
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub enum ExportFormat {
287    JSON,
288    CSV,
289    MessagePack,
290}
291
292/// System statistics
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct SystemStats {
295    pub uptime: u64,
296    pub total_alerts: usize,
297    pub active_connections: usize,
298    pub data_points_collected: usize,
299    pub memory_usage_mb: f64,
300    pub cpu_usage_percent: f64,
301}
302
303/// Real-time dashboard server
304#[derive(Debug)]
305pub struct RealtimeDashboard {
306    config: Arc<Mutex<DashboardConfig>>,
307    session_id: String,
308    start_time: Instant,
309    metric_data: Arc<Mutex<HashMap<MetricCategory, VecDeque<MetricDataPoint>>>>,
310    alert_history: Arc<Mutex<VecDeque<DashboardAlert>>>,
311    websocket_sender: broadcast::Sender<WebSocketMessage>,
312    active_connections: Arc<Mutex<usize>>,
313    total_data_points: Arc<Mutex<usize>>,
314    is_running: Arc<Mutex<bool>>,
315    /// Long-lived host telemetry handle (real `sysinfo` readings). Kept
316    /// long-lived rather than recreated per call because CPU-usage
317    /// measurement is delta-based: `sysinfo` needs two refreshes spaced
318    /// apart in real time to report a meaningful percentage.
319    system_info: Arc<Mutex<System>>,
320}
321
322impl RealtimeDashboard {
323    /// Create new real-time dashboard
324    pub fn new(config: DashboardConfig) -> Self {
325        let (websocket_sender, _) = broadcast::channel(1000);
326
327        Self {
328            config: Arc::new(Mutex::new(config)),
329            session_id: Uuid::new_v4().to_string(),
330            start_time: Instant::now(),
331            metric_data: Arc::new(Mutex::new(HashMap::new())),
332            alert_history: Arc::new(Mutex::new(VecDeque::new())),
333            websocket_sender,
334            active_connections: Arc::new(Mutex::new(0)),
335            total_data_points: Arc::new(Mutex::new(0)),
336            is_running: Arc::new(Mutex::new(false)),
337            system_info: Arc::new(Mutex::new(System::new_all())),
338        }
339    }
340
341    /// Start the dashboard server
342    pub async fn start(&self) -> Result<()> {
343        {
344            let mut running = self
345                .is_running
346                .lock()
347                .map_err(|_| anyhow::anyhow!("Failed to acquire running state lock"))?;
348            if *running {
349                return Ok(());
350            }
351            *running = true;
352        }
353
354        // Start periodic data collection
355        self.start_data_collection().await?;
356
357        // Start periodic system stats updates
358        self.start_system_stats_updates().await?;
359
360        // Start alert monitoring
361        self.start_alert_monitoring().await?;
362
363        Ok(())
364    }
365
366    /// Stop the dashboard server
367    pub fn stop(&self) {
368        if let Ok(mut running) = self.is_running.lock() {
369            *running = false;
370        }
371    }
372
373    /// Add a metric data point
374    pub fn add_metric(&self, category: MetricCategory, label: String, value: f64) -> Result<()> {
375        self.add_metrics(vec![(category, label, value)])
376    }
377
378    /// Add multiple metrics at once. Stores each point (bounded by
379    /// `max_data_points`), broadcasts the batch, and evaluates each real
380    /// value against the configured alert thresholds -- the same path used
381    /// by the periodic host-telemetry collector, so manually-supplied and
382    /// automatically-collected metrics raise alerts identically.
383    pub fn add_metrics(&self, metrics: Vec<(MetricCategory, String, f64)>) -> Result<()> {
384        let thresholds = self
385            .config
386            .lock()
387            .map_err(|_| anyhow::anyhow!("Failed to acquire config lock"))?
388            .alert_thresholds
389            .clone();
390        let max_points = self
391            .config
392            .lock()
393            .map_err(|_| anyhow::anyhow!("Failed to acquire config lock"))?
394            .max_data_points;
395
396        ingest_metrics(
397            &self.metric_data,
398            &self.total_data_points,
399            max_points,
400            &self.alert_history,
401            &thresholds,
402            &self.websocket_sender,
403            metrics,
404        )
405    }
406
407    /// Create an alert
408    pub fn create_alert(
409        &self,
410        severity: AlertSeverity,
411        category: MetricCategory,
412        title: String,
413        message: String,
414        value: Option<f64>,
415        threshold: Option<f64>,
416    ) -> Result<()> {
417        let alert = DashboardAlert {
418            id: Uuid::new_v4().to_string(),
419            timestamp: SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
420            severity,
421            category,
422            title,
423            message,
424            value,
425            threshold,
426        };
427
428        // Add to alert history
429        {
430            let mut history =
431                self.alert_history.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
432            history.push_back(alert.clone());
433
434            // Keep only last 100 alerts
435            while history.len() > 100 {
436                history.pop_front();
437            }
438        }
439
440        // Broadcast alert
441        let message = WebSocketMessage::Alert { alert };
442        let _ = self.websocket_sender.send(message);
443
444        Ok(())
445    }
446
447    /// Get historical data for a category
448    pub fn get_historical_data(&self, category: &MetricCategory) -> Vec<MetricDataPoint> {
449        let data = self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
450        data.get(category)
451            .map(|deque| deque.iter().cloned().collect())
452            .unwrap_or_default()
453    }
454
455    /// Get current system stats
456    pub fn get_system_stats(&self) -> SystemStats {
457        let uptime = self.start_time.elapsed().as_secs();
458        let total_alerts =
459            self.alert_history.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).len();
460        let active_connections =
461            *self.active_connections.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
462        let data_points_collected =
463            *self.total_data_points.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
464
465        // Simple memory and CPU usage estimation
466        let memory_usage_mb = self.estimate_memory_usage();
467        let cpu_usage_percent = self.estimate_cpu_usage();
468
469        SystemStats {
470            uptime,
471            total_alerts,
472            active_connections,
473            data_points_collected,
474            memory_usage_mb,
475            cpu_usage_percent,
476        }
477    }
478
479    /// Subscribe to WebSocket messages
480    pub fn subscribe(&self) -> BroadcastStream<WebSocketMessage> {
481        // Increment connection counter
482        {
483            let mut connections =
484                self.active_connections.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
485            *connections += 1;
486        }
487
488        BroadcastStream::new(self.websocket_sender.subscribe())
489    }
490
491    /// Update dashboard configuration
492    pub fn update_config(&self, new_config: DashboardConfig) -> Result<()> {
493        {
494            let mut config = self.config.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
495            *config = new_config.clone();
496        }
497
498        // Broadcast configuration update
499        let message = WebSocketMessage::ConfigUpdate { config: new_config };
500        let _ = self.websocket_sender.send(message);
501
502        Ok(())
503    }
504
505    /// Get current configuration
506    pub fn get_config(&self) -> DashboardConfig {
507        self.config.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).clone()
508    }
509
510    /// Start periodic data collection
511    async fn start_data_collection(&self) -> Result<()> {
512        let config = self.config.clone();
513        let metric_data = self.metric_data.clone();
514        let total_data_points = self.total_data_points.clone();
515        let alert_history = self.alert_history.clone();
516        let system_info = self.system_info.clone();
517        let websocket_sender = self.websocket_sender.clone();
518        let is_running = self.is_running.clone();
519
520        tokio::spawn(async move {
521            let mut interval = interval(Duration::from_millis(
522                config
523                    .lock()
524                    .unwrap_or_else(|poisoned| poisoned.into_inner())
525                    .update_frequency_ms,
526            ));
527
528            while *is_running.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) {
529                interval.tick().await;
530
531                // Collect real host telemetry, then store + alert on it via the
532                // exact same path `add_metric`/`add_metrics` use -- real values
533                // that never reach `metric_data`/the alert thresholds would be
534                // pointless to have collected in the first place.
535                let (metrics, max_points, thresholds) = {
536                    let cfg = config.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
537                    let metrics = collect_system_metrics(&cfg, &system_info);
538                    (metrics, cfg.max_data_points, cfg.alert_thresholds.clone())
539                };
540
541                if !metrics.is_empty() {
542                    let _ = ingest_metrics(
543                        &metric_data,
544                        &total_data_points,
545                        max_points,
546                        &alert_history,
547                        &thresholds,
548                        &websocket_sender,
549                        metrics,
550                    );
551                }
552            }
553        });
554
555        Ok(())
556    }
557
558    /// Start system stats updates
559    async fn start_system_stats_updates(&self) -> Result<()> {
560        let websocket_sender = self.websocket_sender.clone();
561        let start_time = self.start_time;
562        let alert_history = self.alert_history.clone();
563        let active_connections = self.active_connections.clone();
564        let total_data_points = self.total_data_points.clone();
565        let metric_data = self.metric_data.clone();
566        let system_info = self.system_info.clone();
567        let is_running = self.is_running.clone();
568
569        tokio::spawn(async move {
570            let mut interval = interval(Duration::from_secs(5)); // Update every 5 seconds
571
572            while *is_running.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) {
573                interval.tick().await;
574
575                let stats = SystemStats {
576                    uptime: start_time.elapsed().as_secs(),
577                    total_alerts: alert_history
578                        .lock()
579                        .unwrap_or_else(|poisoned| poisoned.into_inner())
580                        .len(),
581                    active_connections: *active_connections
582                        .lock()
583                        .unwrap_or_else(|poisoned| poisoned.into_inner()),
584                    data_points_collected: *total_data_points
585                        .lock()
586                        .unwrap_or_else(|poisoned| poisoned.into_inner()),
587                    memory_usage_mb: dashboard_footprint_mb(&metric_data),
588                    cpu_usage_percent: estimate_cpu_usage(&system_info),
589                };
590
591                let message = WebSocketMessage::SystemStats { stats };
592                let _ = websocket_sender.send(message);
593            }
594        });
595
596        Ok(())
597    }
598
599    /// Start alert monitoring
600    async fn start_alert_monitoring(&self) -> Result<()> {
601        let metric_data = self.metric_data.clone();
602        let config = self.config.clone();
603        let alert_history = self.alert_history.clone();
604        let websocket_sender = self.websocket_sender.clone();
605        let is_running = self.is_running.clone();
606
607        tokio::spawn(async move {
608            let mut interval = interval(Duration::from_secs(1));
609
610            while *is_running.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) {
611                interval.tick().await;
612
613                // Monitor the real, now-populated history for sustained
614                // threshold breaches and create alerts.
615                let thresholds = {
616                    let cfg = config.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
617                    cfg.alert_thresholds.clone()
618                };
619                check_threshold_breaches(
620                    &metric_data,
621                    &thresholds,
622                    &alert_history,
623                    &websocket_sender,
624                );
625            }
626        });
627
628        Ok(())
629    }
630
631    /// Estimate memory usage of dashboard
632    fn estimate_memory_usage(&self) -> f64 {
633        dashboard_footprint_mb(&self.metric_data)
634    }
635
636    /// Real, host-wide CPU usage percentage (see [`estimate_cpu_usage`] free function).
637    fn estimate_cpu_usage(&self) -> f64 {
638        estimate_cpu_usage(&self.system_info)
639    }
640
641    /// AI-powered anomaly detection for metric patterns
642    pub async fn detect_metric_anomalies(
643        &self,
644        category: &MetricCategory,
645    ) -> Result<Vec<AnomalyDetection>> {
646        let data = self.get_historical_data(category);
647        let mut anomalies = Vec::new();
648
649        if data.len() < 10 {
650            return Ok(anomalies); // Need sufficient data for anomaly detection
651        }
652
653        // Calculate statistical thresholds
654        let values: Vec<f64> = data.iter().map(|d| d.value).collect();
655        let mean = values.iter().sum::<f64>() / values.len() as f64;
656        let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
657        let std_dev = variance.sqrt();
658
659        // Z-score based anomaly detection
660        let z_threshold = 2.0; // 2 standard deviations
661        for point in data.iter() {
662            let z_score = (point.value - mean).abs() / std_dev;
663            if z_score > z_threshold {
664                let anomaly_type =
665                    if point.value > mean { AnomalyType::Spike } else { AnomalyType::Drop };
666
667                anomalies.push(AnomalyDetection {
668                    timestamp: point.timestamp,
669                    value: point.value,
670                    expected_range: (mean - std_dev, mean + std_dev),
671                    anomaly_type,
672                    confidence_score: (z_score - z_threshold) / z_threshold,
673                    category: category.clone(),
674                    description: format!(
675                        "Detected {} in {} metrics: value {} (Z-score: {:.2})",
676                        match anomaly_type {
677                            AnomalyType::Spike => "spike",
678                            AnomalyType::Drop => "drop",
679                            _ => "anomaly",
680                        },
681                        match category {
682                            MetricCategory::Training => "training",
683                            MetricCategory::Memory => "memory",
684                            MetricCategory::GPU => "GPU",
685                            MetricCategory::Network => "network",
686                            MetricCategory::Performance => "performance",
687                            MetricCategory::Custom(name) => name,
688                        },
689                        point.value,
690                        z_score
691                    ),
692                });
693            }
694        }
695
696        // Advanced pattern detection - look for gradual trends
697        if data.len() >= 20 {
698            let recent_window = &data[data.len() - 10..];
699            let earlier_window = &data[data.len() - 20..data.len() - 10];
700
701            let recent_avg =
702                recent_window.iter().map(|d| d.value).sum::<f64>() / recent_window.len() as f64;
703            let earlier_avg =
704                earlier_window.iter().map(|d| d.value).sum::<f64>() / earlier_window.len() as f64;
705
706            let trend_change = (recent_avg - earlier_avg) / earlier_avg;
707
708            if trend_change.abs() > 0.3 {
709                // 30% change
710                if let Some(last_point) = recent_window.last() {
711                    anomalies.push(AnomalyDetection {
712                        timestamp: last_point.timestamp,
713                        value: recent_avg,
714                        expected_range: (earlier_avg * 0.9, earlier_avg * 1.1),
715                        anomaly_type: if trend_change > 0.0 {
716                            AnomalyType::GradualIncrease
717                        } else {
718                            AnomalyType::GradualDecrease
719                        },
720                        confidence_score: trend_change.abs(),
721                        category: category.clone(),
722                        description: format!(
723                            "Detected gradual {} trend: {:.1}% change over recent measurements",
724                            if trend_change > 0.0 { "increase" } else { "decrease" },
725                            trend_change.abs() * 100.0
726                        ),
727                    });
728                }
729            }
730        }
731
732        Ok(anomalies)
733    }
734
735    /// Generate advanced visualization data for modern dashboard components
736    pub fn generate_advanced_visualizations(&self) -> Result<DashboardVisualizationData> {
737        let mut heatmap_data = HashMap::new();
738        let mut time_series_data = HashMap::new();
739        let mut correlation_matrix = Vec::new();
740        let mut performance_distribution = HashMap::new();
741
742        // Generate heatmap data for different metric categories
743        for (category, data) in
744            self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).iter()
745        {
746            if data.len() >= 10 {
747                let recent_data: Vec<f64> = data.iter().rev().take(10).map(|d| d.value).collect();
748                let avg_value = recent_data.iter().sum::<f64>() / recent_data.len() as f64;
749
750                heatmap_data.insert(
751                    category.clone(),
752                    HeatmapData {
753                        intensity: avg_value,
754                        normalized_intensity: (avg_value / (avg_value + 1.0)).min(1.0), // Normalize to 0-1
755                        data_points: recent_data.len(),
756                        timestamp: SystemTime::now()
757                            .duration_since(UNIX_EPOCH)
758                            .unwrap_or_default()
759                            .as_secs(),
760                    },
761                );
762
763                // Time series data for trend visualization
764                let time_series: Vec<TimeSeriesPoint> = data
765                    .iter()
766                    .map(|d| TimeSeriesPoint {
767                        timestamp: d.timestamp,
768                        value: d.value,
769                        label: d.label.clone(),
770                    })
771                    .collect();
772
773                time_series_data.insert(category.clone(), time_series);
774
775                // Performance distribution data
776                let values: Vec<f64> = data.iter().map(|d| d.value).collect();
777                let histogram = self.create_histogram(&values, 10);
778                performance_distribution.insert(category.clone(), histogram);
779            }
780        }
781
782        // Generate correlation matrix for different metrics
783        let categories: Vec<&MetricCategory> = heatmap_data.keys().collect();
784        for (i, cat1) in categories.iter().enumerate() {
785            let mut row = Vec::new();
786            for (j, cat2) in categories.iter().enumerate() {
787                if i == j {
788                    row.push(1.0); // Perfect correlation with itself
789                } else {
790                    // Real Pearson correlation over the aligned recent
791                    // samples; see `calculate_correlation_coefficient` for the
792                    // pairing rule it assumes.
793                    let corr = self.calculate_correlation_coefficient(cat1, cat2);
794                    row.push(corr);
795                }
796            }
797            correlation_matrix.push(row);
798        }
799
800        Ok(DashboardVisualizationData {
801            heatmap_data,
802            time_series_data,
803            correlation_matrix,
804            performance_distribution,
805            generated_at: SystemTime::now()
806                .duration_since(UNIX_EPOCH)
807                .unwrap_or_default()
808                .as_secs(),
809            session_id: self.session_id.clone(),
810        })
811    }
812
813    /// AI-powered performance prediction based on historical trends
814    pub async fn predict_performance_trends(
815        &self,
816        category: &MetricCategory,
817        hours_ahead: u64,
818    ) -> Result<PerformancePrediction> {
819        let data = self.get_historical_data(category);
820
821        if data.len() < 20 {
822            return Err(anyhow::anyhow!(
823                "Insufficient data for prediction (need at least 20 points)"
824            ));
825        }
826
827        let values: Vec<f64> = data.iter().map(|d| d.value).collect();
828        let timestamps: Vec<u64> = data.iter().map(|d| d.timestamp).collect();
829
830        // Simple linear regression for trend prediction
831        let n = values.len() as f64;
832        let sum_x = timestamps.iter().sum::<u64>() as f64;
833        let sum_y = values.iter().sum::<f64>();
834        let sum_xy = timestamps.iter().zip(&values).map(|(x, y)| *x as f64 * y).sum::<f64>();
835        let sum_x2 = timestamps.iter().map(|x| (*x as f64).powi(2)).sum::<f64>();
836
837        let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x.powi(2));
838        let intercept = (sum_y - slope * sum_x) / n;
839
840        // Generate predictions
841        let current_time =
842            SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
843        let prediction_time = current_time + (hours_ahead * 3600);
844        let predicted_value = slope * prediction_time as f64 + intercept;
845
846        // 95% interval around the fitted line from the standard error of the
847        // MEAN (`sigma / sqrt(n)`) times the normal 1.96 quantile.
848        //
849        // Explicitly NOT a prediction interval: a real one would add the
850        // residual variance and the leverage term for a point `hours_ahead`
851        // outside the observed range, and would therefore be wider -- see
852        // `PerformancePrediction::confidence_interval`.
853        let mean = sum_y / n;
854        let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
855        let std_error = (variance / n).sqrt();
856        let confidence_interval = std_error * 1.96;
857
858        // Analyze trend direction and strength
859        let trend_strength = slope.abs() / mean.abs();
860        let trend_direction = if slope > 0.01 {
861            TrendDirection::Increasing
862        } else if slope < -0.01 {
863            TrendDirection::Decreasing
864        } else {
865            TrendDirection::Stable
866        };
867
868        Ok(PerformancePrediction {
869            category: category.clone(),
870            predicted_value,
871            confidence_interval: (
872                predicted_value - confidence_interval,
873                predicted_value + confidence_interval,
874            ),
875            trend_direction,
876            trend_strength,
877            prediction_horizon_hours: hours_ahead,
878            model_accuracy: 1.0 - (std_error / mean.abs()).min(1.0), // Simplified accuracy
879            generated_at: current_time,
880            recommendations: self.generate_performance_recommendations(
881                &trend_direction,
882                trend_strength,
883                predicted_value,
884            ),
885        })
886    }
887
888    /// Advanced dashboard theme and customization support
889    pub fn apply_dashboard_theme(&self, theme: DashboardTheme) -> Result<()> {
890        // This would typically update UI styling, but we'll store theme preferences
891        let theme_message = WebSocketMessage::Generic {
892            message_type: "theme_update".to_string(),
893            data: serde_json::to_value(&theme)?,
894            timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
895            session_id: self.session_id.clone(),
896        };
897
898        if self.websocket_sender.send(theme_message).is_err() {
899            // No active subscribers, but that's okay
900        }
901
902        Ok(())
903    }
904
905    /// Export dashboard data in various formats
906    pub async fn export_dashboard_data(
907        &self,
908        format: ExportFormat,
909        time_range: Option<(u64, u64)>,
910    ) -> Result<Vec<u8>> {
911        let data = if let Some((start, end)) = time_range {
912            self.get_filtered_data(start, end)
913        } else {
914            self.get_all_data()
915        };
916
917        match format {
918            ExportFormat::JSON => {
919                let json_data = serde_json::to_string_pretty(&data)?;
920                Ok(json_data.into_bytes())
921            },
922            ExportFormat::CSV => {
923                let mut csv_data = String::from("timestamp,category,label,value\n");
924                for (category, points) in data {
925                    for point in points {
926                        csv_data.push_str(&format!(
927                            "{},{:?},{},{}\n",
928                            point.timestamp, category, point.label, point.value
929                        ));
930                    }
931                }
932                Ok(csv_data.into_bytes())
933            },
934            ExportFormat::MessagePack => {
935                // Refuse rather than hand back JSON bytes labelled MessagePack:
936                // a caller that feeds those to a MessagePack decoder gets a
937                // parse failure, or worse, silently misreads them.
938                Err(anyhow::anyhow!(
939                    "MessagePack export is not implemented: trustformers-debug links no \
940                     MessagePack encoder. Use ExportFormat::Json or ExportFormat::CSV."
941                ))
942            },
943        }
944    }
945
946    // Helper methods for advanced features
947
948    fn create_histogram(&self, values: &[f64], bins: usize) -> HistogramData {
949        if values.is_empty() {
950            return HistogramData {
951                bins: Vec::new(),
952                max_frequency: 0,
953            };
954        }
955
956        let min_val = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
957        let max_val = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
958        let bin_width = (max_val - min_val) / bins as f64;
959
960        let mut histogram_bins = vec![0; bins];
961
962        for &value in values {
963            let bin_idx = ((value - min_val) / bin_width).floor() as usize;
964            let bin_idx = bin_idx.min(bins - 1); // Ensure we don't go out of bounds
965            histogram_bins[bin_idx] += 1;
966        }
967
968        let max_frequency = *histogram_bins.iter().max().unwrap_or(&0);
969
970        let bins_data: Vec<HistogramBin> = histogram_bins
971            .into_iter()
972            .enumerate()
973            .map(|(i, count)| HistogramBin {
974                range_start: min_val + i as f64 * bin_width,
975                range_end: min_val + (i + 1) as f64 * bin_width,
976                frequency: count,
977            })
978            .collect();
979
980        HistogramData {
981            bins: bins_data,
982            max_frequency,
983        }
984    }
985
986    /// Pearson correlation between the most recent samples of two metric
987    /// categories.
988    ///
989    /// The two series are paired by RECENCY RANK (newest with newest, and so
990    /// on) over at most 50 points, not by timestamp. That is exact while both
991    /// categories are sampled on the dashboard's single refresh tick -- the
992    /// only way [`ingest_metrics`] feeds them -- and would misalign if a caller
993    /// ever ingested two categories at different cadences.
994    ///
995    /// Returns `0.0` when either category has fewer than two samples or has
996    /// zero variance, i.e. when the coefficient is undefined.
997    fn calculate_correlation_coefficient(
998        &self,
999        cat1: &MetricCategory,
1000        cat2: &MetricCategory,
1001    ) -> f64 {
1002        let data = self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1003
1004        let data1 = match data.get(cat1) {
1005            Some(d) => d,
1006            None => return 0.0,
1007        };
1008
1009        let data2 = match data.get(cat2) {
1010            Some(d) => d,
1011            None => return 0.0,
1012        };
1013
1014        if data1.len() < 2 || data2.len() < 2 {
1015            return 0.0;
1016        }
1017
1018        // Take the minimum length to align the datasets
1019        let min_len = data1.len().min(data2.len()).min(50); // Use at most 50 points for performance
1020        let values1: Vec<f64> = data1.iter().rev().take(min_len).map(|d| d.value).collect();
1021        let values2: Vec<f64> = data2.iter().rev().take(min_len).map(|d| d.value).collect();
1022
1023        // Calculate Pearson correlation coefficient
1024        let n = values1.len() as f64;
1025        let mean1 = values1.iter().sum::<f64>() / n;
1026        let mean2 = values2.iter().sum::<f64>() / n;
1027
1028        let covariance = values1
1029            .iter()
1030            .zip(&values2)
1031            .map(|(v1, v2)| (v1 - mean1) * (v2 - mean2))
1032            .sum::<f64>()
1033            / n;
1034
1035        let std1 = (values1.iter().map(|v| (v - mean1).powi(2)).sum::<f64>() / n).sqrt();
1036        let std2 = (values2.iter().map(|v| (v - mean2).powi(2)).sum::<f64>() / n).sqrt();
1037
1038        if std1 == 0.0 || std2 == 0.0 {
1039            0.0
1040        } else {
1041            covariance / (std1 * std2)
1042        }
1043    }
1044
1045    fn generate_performance_recommendations(
1046        &self,
1047        trend: &TrendDirection,
1048        strength: f64,
1049        predicted_value: f64,
1050    ) -> Vec<String> {
1051        let mut recommendations = Vec::new();
1052
1053        match trend {
1054            TrendDirection::Increasing => {
1055                if strength > 0.1 {
1056                    recommendations.push(
1057                        "Monitor for potential resource exhaustion due to increasing trend"
1058                            .to_string(),
1059                    );
1060                    recommendations.push("Consider scaling resources proactively".to_string());
1061                }
1062                if predicted_value > 90.0 {
1063                    recommendations.push(
1064                        "Critical threshold approaching - immediate action recommended".to_string(),
1065                    );
1066                }
1067            },
1068            TrendDirection::Decreasing => {
1069                if strength > 0.05 {
1070                    recommendations
1071                        .push("Investigate potential performance degradation".to_string());
1072                    recommendations.push("Check for resource leaks or inefficiencies".to_string());
1073                }
1074            },
1075            TrendDirection::Stable => {
1076                recommendations
1077                    .push("Performance trend is stable - continue monitoring".to_string());
1078            },
1079        }
1080
1081        if recommendations.is_empty() {
1082            recommendations.push("No specific recommendations at this time".to_string());
1083        }
1084
1085        recommendations
1086    }
1087
1088    fn get_filtered_data(
1089        &self,
1090        start: u64,
1091        end: u64,
1092    ) -> HashMap<MetricCategory, VecDeque<MetricDataPoint>> {
1093        let data = self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1094        let mut filtered_data = HashMap::new();
1095
1096        for (category, points) in data.iter() {
1097            let filtered_points: VecDeque<MetricDataPoint> = points
1098                .iter()
1099                .filter(|p| p.timestamp >= start && p.timestamp <= end)
1100                .cloned()
1101                .collect();
1102
1103            if !filtered_points.is_empty() {
1104                filtered_data.insert(category.clone(), filtered_points);
1105            }
1106        }
1107
1108        filtered_data
1109    }
1110
1111    fn get_all_data(&self) -> HashMap<MetricCategory, VecDeque<MetricDataPoint>> {
1112        self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).clone()
1113    }
1114}
1115
1116// ============================================================================
1117// Free functions
1118//
1119// These take explicit `&Mutex<_>` / `&Arc<_>` references rather than `&self`
1120// so they can be shared between `&self` methods (`add_metric`, `get_system_stats`,
1121// ...) and the background tasks spawned by `start()`, which only hold cloned
1122// `Arc`s of individual fields (not the whole `RealtimeDashboard`).
1123// ============================================================================
1124
1125/// Store metric points (bounded by `max_data_points`), broadcast the batch,
1126/// and evaluate each real value against the alert thresholds. The single
1127/// path used by `add_metric`/`add_metrics` and by the periodic host-telemetry
1128/// collector, so manually-supplied and automatically-collected metrics are
1129/// treated identically.
1130#[allow(clippy::too_many_arguments)]
1131fn ingest_metrics(
1132    metric_data: &Mutex<HashMap<MetricCategory, VecDeque<MetricDataPoint>>>,
1133    total_data_points: &Mutex<usize>,
1134    max_data_points: usize,
1135    alert_history: &Mutex<VecDeque<DashboardAlert>>,
1136    thresholds: &AlertThresholds,
1137    websocket_sender: &broadcast::Sender<WebSocketMessage>,
1138    metrics: Vec<(MetricCategory, String, f64)>,
1139) -> Result<()> {
1140    let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
1141    let mut data_points = Vec::with_capacity(metrics.len());
1142
1143    for (category, label, value) in metrics {
1144        let data_point = MetricDataPoint {
1145            timestamp,
1146            value,
1147            label,
1148            category: category.clone(),
1149        };
1150
1151        {
1152            let mut data = metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1153            let category_data = data.entry(category.clone()).or_default();
1154            category_data.push_back(data_point.clone());
1155            while category_data.len() > max_data_points {
1156                category_data.pop_front();
1157            }
1158        }
1159
1160        data_points.push(data_point);
1161        evaluate_alert(
1162            alert_history,
1163            websocket_sender,
1164            thresholds,
1165            &category,
1166            value,
1167        );
1168    }
1169
1170    {
1171        let mut total = total_data_points.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1172        *total += data_points.len();
1173    }
1174
1175    let message = WebSocketMessage::MetricUpdate { data: data_points };
1176    let _ = websocket_sender.send(message);
1177
1178    Ok(())
1179}
1180
1181/// Evaluate one real metric value against the configured thresholds and, on
1182/// breach, record + broadcast a real alert (real value, real threshold --
1183/// never derived from randomness).
1184fn evaluate_alert(
1185    alert_history: &Mutex<VecDeque<DashboardAlert>>,
1186    websocket_sender: &broadcast::Sender<WebSocketMessage>,
1187    thresholds: &AlertThresholds,
1188    category: &MetricCategory,
1189    value: f64,
1190) {
1191    let (severity, title, message, threshold) = match category {
1192        MetricCategory::Memory if value > thresholds.memory_threshold => (
1193            AlertSeverity::Warning,
1194            "High Memory Usage".to_string(),
1195            format!(
1196                "Memory usage is {:.1}% (threshold: {:.1}%)",
1197                value, thresholds.memory_threshold
1198            ),
1199            thresholds.memory_threshold,
1200        ),
1201        MetricCategory::GPU if value > thresholds.gpu_utilization_threshold => (
1202            AlertSeverity::Warning,
1203            "High GPU Utilization".to_string(),
1204            format!(
1205                "GPU utilization is {:.1}% (threshold: {:.1}%)",
1206                value, thresholds.gpu_utilization_threshold
1207            ),
1208            thresholds.gpu_utilization_threshold,
1209        ),
1210        MetricCategory::Training if value > thresholds.loss_spike_threshold => (
1211            AlertSeverity::Error,
1212            "Training Loss Spike".to_string(),
1213            format!(
1214                "Loss spike detected: {:.4} (threshold: {:.4})",
1215                value, thresholds.loss_spike_threshold
1216            ),
1217            thresholds.loss_spike_threshold,
1218        ),
1219        _ => return,
1220    };
1221
1222    let alert = DashboardAlert {
1223        id: Uuid::new_v4().to_string(),
1224        timestamp: SystemTime::now()
1225            .duration_since(UNIX_EPOCH)
1226            .map(|d| d.as_millis() as u64)
1227            .unwrap_or(0),
1228        severity,
1229        category: category.clone(),
1230        title,
1231        message,
1232        value: Some(value),
1233        threshold: Some(threshold),
1234    };
1235
1236    {
1237        let mut history = alert_history.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1238        history.push_back(alert.clone());
1239        while history.len() > 100 {
1240            history.pop_front();
1241        }
1242    }
1243
1244    let _ = websocket_sender.send(WebSocketMessage::Alert { alert });
1245}
1246
1247/// Look for a *sustained* threshold breach: the last [`SUSTAINED_BREACH_WINDOW`]
1248/// real samples of a category all above threshold (as opposed to a single
1249/// noisy spike, which `evaluate_alert` already covers per-sample). Reads the
1250/// real, now-populated `metric_data` history -- this used to lock both
1251/// mutexes and do nothing.
1252fn check_threshold_breaches(
1253    metric_data: &Mutex<HashMap<MetricCategory, VecDeque<MetricDataPoint>>>,
1254    thresholds: &AlertThresholds,
1255    alert_history: &Mutex<VecDeque<DashboardAlert>>,
1256    websocket_sender: &broadcast::Sender<WebSocketMessage>,
1257) {
1258    let breaches: Vec<(MetricCategory, f64, f64)> = {
1259        let data = metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1260        [
1261            (MetricCategory::Memory, thresholds.memory_threshold),
1262            (MetricCategory::GPU, thresholds.gpu_utilization_threshold),
1263        ]
1264        .into_iter()
1265        .filter_map(|(category, threshold)| {
1266            let points = data.get(&category)?;
1267            if points.len() < SUSTAINED_BREACH_WINDOW {
1268                return None;
1269            }
1270            let recent: Vec<f64> =
1271                points.iter().rev().take(SUSTAINED_BREACH_WINDOW).map(|p| p.value).collect();
1272            if recent.iter().all(|&v| v > threshold) {
1273                let avg = recent.iter().sum::<f64>() / recent.len() as f64;
1274                Some((category, avg, threshold))
1275            } else {
1276                None
1277            }
1278        })
1279        .collect()
1280    };
1281
1282    for (category, avg_value, threshold) in breaches {
1283        let alert = DashboardAlert {
1284            id: Uuid::new_v4().to_string(),
1285            timestamp: SystemTime::now()
1286                .duration_since(UNIX_EPOCH)
1287                .map(|d| d.as_millis() as u64)
1288                .unwrap_or(0),
1289            severity: AlertSeverity::Error,
1290            category: category.clone(),
1291            title: format!("Sustained {category:?} threshold breach"),
1292            message: format!(
1293                "{category:?} has stayed above {threshold:.1} for the last \
1294                 {SUSTAINED_BREACH_WINDOW} samples (avg {avg_value:.1})"
1295            ),
1296            value: Some(avg_value),
1297            threshold: Some(threshold),
1298        };
1299
1300        {
1301            let mut history = alert_history.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1302            history.push_back(alert.clone());
1303            while history.len() > 100 {
1304                history.pop_front();
1305            }
1306        }
1307        let _ = websocket_sender.send(WebSocketMessage::Alert { alert });
1308    }
1309}
1310
1311/// Collect real host telemetry (`sysinfo`) for the categories enabled in
1312/// `cfg`, as `(category, label, value)` tuples ready for [`ingest_metrics`].
1313/// GPU readings are honestly omitted (not fabricated) when no GPU telemetry
1314/// backend is available -- see [`get_gpu_utilization`].
1315fn collect_system_metrics(
1316    cfg: &DashboardConfig,
1317    system_info: &Mutex<System>,
1318) -> Vec<(MetricCategory, String, f64)> {
1319    let mut metrics = Vec::new();
1320
1321    if cfg.enable_memory_profiling {
1322        if let Some(memory_usage) = get_memory_usage(system_info) {
1323            metrics.push((
1324                MetricCategory::Memory,
1325                "Memory Usage".to_string(),
1326                memory_usage,
1327            ));
1328        }
1329    }
1330
1331    if cfg.enable_gpu_monitoring {
1332        match get_gpu_utilization() {
1333            Some(gpu_utilization) => {
1334                metrics.push((
1335                    MetricCategory::GPU,
1336                    "GPU Utilization".to_string(),
1337                    gpu_utilization,
1338                ));
1339            },
1340            None => tracing::debug!(
1341                "GPU monitoring is enabled but no GPU telemetry backend is available on this \
1342                 build/machine; skipping (not fabricating a reading)."
1343            ),
1344        }
1345        if let Some(gpu_memory) = get_gpu_memory_usage() {
1346            metrics.push((MetricCategory::GPU, "GPU Memory".to_string(), gpu_memory));
1347        }
1348    }
1349
1350    metrics
1351}
1352
1353/// Real system memory usage, as a percentage of total memory.
1354fn get_memory_usage(system_info: &Mutex<System>) -> Option<f64> {
1355    let mut sys = system_info.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1356    sys.refresh_memory();
1357    let total = sys.total_memory();
1358    if total == 0 {
1359        return None;
1360    }
1361    Some(sys.used_memory() as f64 / total as f64 * 100.0)
1362}
1363
1364/// Real GPU utilization percentage -- always `None`. No pure-Rust,
1365/// C/C++-free GPU telemetry backend (NVML/ROCm-SMI) is wired into this
1366/// build (see the `cuda`/`rocm`/`tpu` Cargo features, which are placeholders
1367/// today), so this honestly reports "unavailable on this machine" rather
1368/// than fabricating a number.
1369fn get_gpu_utilization() -> Option<f64> {
1370    None
1371}
1372
1373/// Real GPU memory usage percentage -- see [`get_gpu_utilization`].
1374fn get_gpu_memory_usage() -> Option<f64> {
1375    None
1376}
1377
1378/// Real host-wide CPU usage percentage via `sysinfo`. CPU usage is
1379/// delta-based: the very first reading in a process is not meaningful, but
1380/// `system_info` is long-lived on `RealtimeDashboard` and refreshed on every
1381/// call, so accuracy improves as the dashboard runs.
1382fn estimate_cpu_usage(system_info: &Mutex<System>) -> f64 {
1383    let mut sys = system_info.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1384    sys.refresh_cpu_usage();
1385    sys.global_cpu_usage() as f64
1386}
1387
1388/// Rough estimate (in MB) of the dashboard's own retained metric-history
1389/// footprint, not host telemetry.
1390fn dashboard_footprint_mb(
1391    metric_data: &Mutex<HashMap<MetricCategory, VecDeque<MetricDataPoint>>>,
1392) -> f64 {
1393    let data = metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1394    let total_points: usize = data.values().map(|deque| deque.len()).sum();
1395    // Rough estimate: ~100 bytes per data point.
1396    (total_points * 100) as f64 / (1024.0 * 1024.0)
1397}
1398
1399/// Dashboard builder for easier configuration
1400#[derive(Debug, Default)]
1401pub struct DashboardBuilder {
1402    config: DashboardConfig,
1403}
1404
1405impl DashboardBuilder {
1406    /// Create new dashboard builder
1407    pub fn new() -> Self {
1408        Self::default()
1409    }
1410
1411    /// Set WebSocket port
1412    pub fn port(mut self, port: u16) -> Self {
1413        self.config.websocket_port = port;
1414        self
1415    }
1416
1417    /// Set update frequency
1418    pub fn update_frequency(mut self, frequency_ms: u64) -> Self {
1419        self.config.update_frequency_ms = frequency_ms;
1420        self
1421    }
1422
1423    /// Set maximum data points
1424    pub fn max_data_points(mut self, max_points: usize) -> Self {
1425        self.config.max_data_points = max_points;
1426        self
1427    }
1428
1429    /// Enable/disable GPU monitoring
1430    pub fn gpu_monitoring(mut self, enabled: bool) -> Self {
1431        self.config.enable_gpu_monitoring = enabled;
1432        self
1433    }
1434
1435    /// Enable/disable memory profiling
1436    pub fn memory_profiling(mut self, enabled: bool) -> Self {
1437        self.config.enable_memory_profiling = enabled;
1438        self
1439    }
1440
1441    /// Set alert thresholds
1442    pub fn alert_thresholds(mut self, thresholds: AlertThresholds) -> Self {
1443        self.config.alert_thresholds = thresholds;
1444        self
1445    }
1446
1447    /// Build the dashboard
1448    pub fn build(self) -> RealtimeDashboard {
1449        RealtimeDashboard::new(self.config)
1450    }
1451}
1452
1453#[cfg(test)]
1454#[path = "realtime_dashboard_tests.rs"]
1455mod realtime_dashboard_tests;
1456
1457#[cfg(test)]
1458mod tests {
1459    use super::*;
1460
1461    #[tokio::test]
1462    async fn messagepack_export_is_refused_rather_than_returning_json_bytes() {
1463        let dashboard = RealtimeDashboard::new(DashboardConfig::default());
1464        let err = dashboard
1465            .export_dashboard_data(ExportFormat::MessagePack, None)
1466            .await
1467            .expect_err("MessagePack must be refused");
1468        let msg = err.to_string();
1469        assert!(msg.contains("not implemented"), "{msg}");
1470        assert!(
1471            msg.contains("MessagePack encoder"),
1472            "must name what is missing: {msg}"
1473        );
1474    }
1475    use futures::StreamExt;
1476    use std::time::Duration;
1477
1478    #[tokio::test]
1479    async fn test_dashboard_creation() {
1480        let dashboard = DashboardBuilder::new()
1481            .port(8081)
1482            .update_frequency(50)
1483            .max_data_points(500)
1484            .build();
1485
1486        assert_eq!(dashboard.get_config().websocket_port, 8081);
1487        assert_eq!(dashboard.get_config().update_frequency_ms, 50);
1488        assert_eq!(dashboard.get_config().max_data_points, 500);
1489    }
1490
1491    #[tokio::test]
1492    async fn test_metric_addition() {
1493        let dashboard = DashboardBuilder::new().build();
1494
1495        let result = dashboard.add_metric(MetricCategory::Training, "loss".to_string(), 0.5);
1496
1497        assert!(result.is_ok());
1498
1499        let historical_data = dashboard.get_historical_data(&MetricCategory::Training);
1500        assert_eq!(historical_data.len(), 1);
1501        assert_eq!(historical_data[0].value, 0.5);
1502        assert_eq!(historical_data[0].label, "loss");
1503    }
1504
1505    #[tokio::test]
1506    async fn test_batch_metrics() {
1507        let dashboard = DashboardBuilder::new().build();
1508
1509        let metrics = vec![
1510            (MetricCategory::Training, "loss".to_string(), 0.5),
1511            (MetricCategory::Training, "accuracy".to_string(), 0.9),
1512            (MetricCategory::GPU, "utilization".to_string(), 75.0),
1513        ];
1514
1515        let result = dashboard.add_metrics(metrics);
1516        assert!(result.is_ok());
1517
1518        let training_data = dashboard.get_historical_data(&MetricCategory::Training);
1519        assert_eq!(training_data.len(), 2);
1520
1521        let gpu_data = dashboard.get_historical_data(&MetricCategory::GPU);
1522        assert_eq!(gpu_data.len(), 1);
1523    }
1524
1525    #[tokio::test]
1526    async fn test_alert_creation() {
1527        let dashboard = DashboardBuilder::new().build();
1528
1529        let result = dashboard.create_alert(
1530            AlertSeverity::Warning,
1531            MetricCategory::Memory,
1532            "High Memory".to_string(),
1533            "Memory usage is high".to_string(),
1534            Some(95.0),
1535            Some(90.0),
1536        );
1537
1538        assert!(result.is_ok());
1539
1540        let history = dashboard.alert_history.lock().expect("lock should not be poisoned");
1541        assert_eq!(history.len(), 1);
1542        assert_eq!(history[0].title, "High Memory");
1543    }
1544
1545    #[tokio::test]
1546    async fn test_websocket_subscription() {
1547        let dashboard = DashboardBuilder::new().build();
1548
1549        let mut stream = dashboard.subscribe();
1550
1551        // Start the dashboard
1552        let dashboard_clone = Arc::new(dashboard);
1553        let dashboard_for_task = dashboard_clone.clone();
1554
1555        tokio::spawn(async move {
1556            let _ = dashboard_for_task.start().await;
1557        });
1558
1559        // Add a metric to trigger a message
1560        let _ =
1561            dashboard_clone.add_metric(MetricCategory::Training, "test_metric".to_string(), 42.0);
1562
1563        // The subscriber legitimately also sees other real broadcasts (host
1564        // telemetry from the periodic collector, `SystemStats` updates, ...),
1565        // so scan the stream for the specific update rather than assuming
1566        // it's the very first message -- a real client would do the same.
1567        let found = tokio::time::timeout(Duration::from_millis(500), async {
1568            loop {
1569                match stream.next().await {
1570                    Some(Ok(WebSocketMessage::MetricUpdate { data })) => {
1571                        if let Some(point) = data.iter().find(|p| p.label == "test_metric") {
1572                            return Some(point.clone());
1573                        }
1574                    },
1575                    Some(_) => continue,
1576                    None => return None,
1577                }
1578            }
1579        })
1580        .await;
1581
1582        dashboard_clone.stop();
1583
1584        let point = found
1585            .expect("should not time out waiting for the test_metric update")
1586            .expect("stream should not end before the test_metric update arrives");
1587        assert_eq!(point.value, 42.0);
1588        assert_eq!(point.label, "test_metric");
1589    }
1590
1591    #[tokio::test]
1592    async fn test_system_stats() {
1593        let dashboard = DashboardBuilder::new().build();
1594
1595        // Add some data
1596        let _ = dashboard.add_metric(MetricCategory::Training, "loss".to_string(), 0.5);
1597        let _ = dashboard.create_alert(
1598            AlertSeverity::Info,
1599            MetricCategory::Training,
1600            "Test Alert".to_string(),
1601            "Test message".to_string(),
1602            None,
1603            None,
1604        );
1605
1606        let stats = dashboard.get_system_stats();
1607
1608        assert_eq!(stats.data_points_collected, 1);
1609        assert_eq!(stats.total_alerts, 1);
1610        // uptime is a Duration which is always >= 0
1611    }
1612
1613    #[tokio::test]
1614    async fn test_data_point_limit() {
1615        let dashboard = DashboardBuilder::new().max_data_points(2).build();
1616
1617        // Add 3 data points
1618        let _ = dashboard.add_metric(MetricCategory::Training, "metric1".to_string(), 1.0);
1619        let _ = dashboard.add_metric(MetricCategory::Training, "metric2".to_string(), 2.0);
1620        let _ = dashboard.add_metric(MetricCategory::Training, "metric3".to_string(), 3.0);
1621
1622        let data = dashboard.get_historical_data(&MetricCategory::Training);
1623
1624        // Should only keep the last 2 data points
1625        assert_eq!(data.len(), 2);
1626        assert_eq!(data[0].value, 2.0); // First of the remaining two
1627        assert_eq!(data[1].value, 3.0); // Last added
1628    }
1629
1630    #[test]
1631    fn test_get_memory_usage_is_real_not_random() {
1632        let system_info = Mutex::new(System::new_all());
1633        let a = get_memory_usage(&system_info).expect("memory usage should be available");
1634        let b = get_memory_usage(&system_info).expect("memory usage should be available");
1635        assert!((0.0..=100.0).contains(&a));
1636        assert!((0.0..=100.0).contains(&b));
1637        // The old implementation was `50.0 + thread_rng().random::<f64>() * 40.0`,
1638        // i.e. uniformly random over a 40-point-wide band on every call. Two
1639        // real readings taken back-to-back on the same host must be far more
1640        // stable than that.
1641        assert!(
1642            (a - b).abs() < 10.0,
1643            "consecutive real memory readings should be close: {a} vs {b}"
1644        );
1645    }
1646
1647    #[test]
1648    fn test_estimate_cpu_usage_is_real_not_random() {
1649        let system_info = Mutex::new(System::new_all());
1650        let cpu = estimate_cpu_usage(&system_info);
1651        // A real reading is a finite, non-negative percentage. The old
1652        // implementation was a fixed `5.0..=15.0` band regardless of the
1653        // host; a real one is unbounded above (though in practice well
1654        // under a few hundred) since it comes straight from the OS.
1655        assert!(cpu.is_finite() && cpu >= 0.0, "got {cpu}");
1656    }
1657
1658    #[test]
1659    fn test_gpu_metrics_are_honestly_absent_not_fabricated() {
1660        // The old implementation always returned a random number (e.g. in
1661        // [30, 90] for utilization) regardless of whether a GPU -- or any
1662        // GPU telemetry backend -- was actually present.
1663        assert_eq!(get_gpu_utilization(), None);
1664        assert_eq!(get_gpu_memory_usage(), None);
1665
1666        let cfg = DashboardConfig {
1667            enable_gpu_monitoring: true,
1668            ..Default::default()
1669        };
1670        let system_info = Mutex::new(System::new_all());
1671        let metrics = collect_system_metrics(&cfg, &system_info);
1672        assert!(
1673            metrics.iter().all(|(category, _, _)| *category != MetricCategory::GPU),
1674            "no fabricated GPU metric should be emitted when no GPU backend is available"
1675        );
1676    }
1677
1678    #[test]
1679    fn test_check_threshold_breaches_raises_a_real_alert() {
1680        let metric_data: Mutex<HashMap<MetricCategory, VecDeque<MetricDataPoint>>> =
1681            Mutex::new(HashMap::new());
1682        let alert_history: Mutex<VecDeque<DashboardAlert>> = Mutex::new(VecDeque::new());
1683        let (websocket_sender, _rx) = broadcast::channel(16);
1684        let thresholds = AlertThresholds::default();
1685
1686        {
1687            let mut data = metric_data.lock().expect("lock should not be poisoned");
1688            let points = data.entry(MetricCategory::Memory).or_default();
1689            for i in 0..SUSTAINED_BREACH_WINDOW {
1690                points.push_back(MetricDataPoint {
1691                    timestamp: i as u64,
1692                    value: thresholds.memory_threshold + 5.0,
1693                    label: "Memory Usage".to_string(),
1694                    category: MetricCategory::Memory,
1695                });
1696            }
1697        }
1698
1699        // The old implementation locked both mutexes and returned without
1700        // ever inspecting the data or creating an alert.
1701        check_threshold_breaches(&metric_data, &thresholds, &alert_history, &websocket_sender);
1702
1703        let history = alert_history.lock().expect("lock should not be poisoned");
1704        assert_eq!(history.len(), 1);
1705        assert_eq!(history[0].category, MetricCategory::Memory);
1706        assert!(
1707            history[0].value.expect("alert should carry a value") > thresholds.memory_threshold
1708        );
1709    }
1710
1711    #[test]
1712    fn test_check_threshold_breaches_ignores_non_sustained_spikes() {
1713        let metric_data: Mutex<HashMap<MetricCategory, VecDeque<MetricDataPoint>>> =
1714            Mutex::new(HashMap::new());
1715        let alert_history: Mutex<VecDeque<DashboardAlert>> = Mutex::new(VecDeque::new());
1716        let (websocket_sender, _rx) = broadcast::channel(16);
1717        let thresholds = AlertThresholds::default();
1718
1719        {
1720            let mut data = metric_data.lock().expect("lock should not be poisoned");
1721            let points = data.entry(MetricCategory::Memory).or_default();
1722            // One spike above threshold, rest comfortably below it.
1723            points.push_back(MetricDataPoint {
1724                timestamp: 0,
1725                value: thresholds.memory_threshold + 5.0,
1726                label: "Memory Usage".to_string(),
1727                category: MetricCategory::Memory,
1728            });
1729            for i in 1..SUSTAINED_BREACH_WINDOW {
1730                points.push_back(MetricDataPoint {
1731                    timestamp: i as u64,
1732                    value: thresholds.memory_threshold - 20.0,
1733                    label: "Memory Usage".to_string(),
1734                    category: MetricCategory::Memory,
1735                });
1736            }
1737        }
1738
1739        check_threshold_breaches(&metric_data, &thresholds, &alert_history, &websocket_sender);
1740
1741        let history = alert_history.lock().expect("lock should not be poisoned");
1742        assert!(
1743            history.is_empty(),
1744            "a single spike must not be treated as a sustained breach"
1745        );
1746    }
1747
1748    #[tokio::test]
1749    async fn test_automatic_collection_actually_records_real_memory_metrics() {
1750        let dashboard =
1751            Arc::new(DashboardBuilder::new().update_frequency(20).memory_profiling(true).build());
1752        let handle = dashboard.clone();
1753
1754        tokio::spawn(async move {
1755            let _ = handle.start().await;
1756        });
1757
1758        // Long enough for several 20ms collection ticks.
1759        tokio::time::sleep(Duration::from_millis(150)).await;
1760        dashboard.stop();
1761
1762        // The old implementation captured `_metric_data` (a deliberately
1763        // unused clone) in the collection task and only ever broadcast
1764        // metrics over the WebSocket channel, so `metric_data` -- and
1765        // therefore `get_historical_data` -- stayed empty forever for
1766        // automatically-collected categories.
1767        let memory_data = dashboard.get_historical_data(&MetricCategory::Memory);
1768        assert!(
1769            !memory_data.is_empty(),
1770            "the periodic collector must actually store real telemetry, not just broadcast it"
1771        );
1772        for point in &memory_data {
1773            assert!((0.0..=100.0).contains(&point.value));
1774        }
1775    }
1776}