1use 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
17const SUSTAINED_BREACH_WINDOW: usize = 5;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct DashboardConfig {
24 pub websocket_port: u16,
26 pub update_frequency_ms: u64,
28 pub max_data_points: usize,
30 pub enable_gpu_monitoring: bool,
32 pub enable_memory_profiling: bool,
34 pub enable_network_monitoring: bool,
36 pub enable_performance_alerts: bool,
38 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#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct AlertThresholds {
60 pub memory_threshold: f64,
62 pub gpu_utilization_threshold: f64,
64 pub temperature_threshold: f64,
66 pub loss_spike_threshold: f64,
68 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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub enum AlertSeverity {
120 Info,
121 Warning,
122 Error,
123 Critical,
124}
125
126#[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#[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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
173pub enum AnomalyType {
174 Spike,
175 Drop,
176 GradualIncrease,
177 GradualDecrease,
178 Outlier,
179}
180
181#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct TimeSeriesPoint {
204 pub timestamp: u64,
205 pub value: f64,
206 pub label: String,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct HistogramData {
212 pub bins: Vec<HistogramBin>,
213 pub max_frequency: usize,
214}
215
216#[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#[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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
240pub enum TrendDirection {
241 Increasing,
242 Decreasing,
243 Stable,
244}
245
246#[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#[derive(Debug, Clone, Serialize, Deserialize)]
286pub enum ExportFormat {
287 JSON,
288 CSV,
289 MessagePack,
290}
291
292#[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#[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 system_info: Arc<Mutex<System>>,
320}
321
322impl RealtimeDashboard {
323 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 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 self.start_data_collection().await?;
356
357 self.start_system_stats_updates().await?;
359
360 self.start_alert_monitoring().await?;
362
363 Ok(())
364 }
365
366 pub fn stop(&self) {
368 if let Ok(mut running) = self.is_running.lock() {
369 *running = false;
370 }
371 }
372
373 pub fn add_metric(&self, category: MetricCategory, label: String, value: f64) -> Result<()> {
375 self.add_metrics(vec![(category, label, value)])
376 }
377
378 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 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 {
430 let mut history =
431 self.alert_history.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
432 history.push_back(alert.clone());
433
434 while history.len() > 100 {
436 history.pop_front();
437 }
438 }
439
440 let message = WebSocketMessage::Alert { alert };
442 let _ = self.websocket_sender.send(message);
443
444 Ok(())
445 }
446
447 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 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 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 pub fn subscribe(&self) -> BroadcastStream<WebSocketMessage> {
481 {
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 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 let message = WebSocketMessage::ConfigUpdate { config: new_config };
500 let _ = self.websocket_sender.send(message);
501
502 Ok(())
503 }
504
505 pub fn get_config(&self) -> DashboardConfig {
507 self.config.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).clone()
508 }
509
510 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 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 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)); 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 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 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 fn estimate_memory_usage(&self) -> f64 {
633 dashboard_footprint_mb(&self.metric_data)
634 }
635
636 fn estimate_cpu_usage(&self) -> f64 {
638 estimate_cpu_usage(&self.system_info)
639 }
640
641 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); }
652
653 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 let z_threshold = 2.0; 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 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 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 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 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), 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 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 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 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); } else {
790 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 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 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 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 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 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), generated_at: current_time,
880 recommendations: self.generate_performance_recommendations(
881 &trend_direction,
882 trend_strength,
883 predicted_value,
884 ),
885 })
886 }
887
888 pub fn apply_dashboard_theme(&self, theme: DashboardTheme) -> Result<()> {
890 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 }
901
902 Ok(())
903 }
904
905 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 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 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); 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 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 let min_len = data1.len().min(data2.len()).min(50); 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 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#[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
1181fn 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
1247fn 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
1311fn 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
1353fn 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
1364fn get_gpu_utilization() -> Option<f64> {
1370 None
1371}
1372
1373fn get_gpu_memory_usage() -> Option<f64> {
1375 None
1376}
1377
1378fn 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
1388fn 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 (total_points * 100) as f64 / (1024.0 * 1024.0)
1397}
1398
1399#[derive(Debug, Default)]
1401pub struct DashboardBuilder {
1402 config: DashboardConfig,
1403}
1404
1405impl DashboardBuilder {
1406 pub fn new() -> Self {
1408 Self::default()
1409 }
1410
1411 pub fn port(mut self, port: u16) -> Self {
1413 self.config.websocket_port = port;
1414 self
1415 }
1416
1417 pub fn update_frequency(mut self, frequency_ms: u64) -> Self {
1419 self.config.update_frequency_ms = frequency_ms;
1420 self
1421 }
1422
1423 pub fn max_data_points(mut self, max_points: usize) -> Self {
1425 self.config.max_data_points = max_points;
1426 self
1427 }
1428
1429 pub fn gpu_monitoring(mut self, enabled: bool) -> Self {
1431 self.config.enable_gpu_monitoring = enabled;
1432 self
1433 }
1434
1435 pub fn memory_profiling(mut self, enabled: bool) -> Self {
1437 self.config.enable_memory_profiling = enabled;
1438 self
1439 }
1440
1441 pub fn alert_thresholds(mut self, thresholds: AlertThresholds) -> Self {
1443 self.config.alert_thresholds = thresholds;
1444 self
1445 }
1446
1447 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 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 let _ =
1561 dashboard_clone.add_metric(MetricCategory::Training, "test_metric".to_string(), 42.0);
1562
1563 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 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 }
1612
1613 #[tokio::test]
1614 async fn test_data_point_limit() {
1615 let dashboard = DashboardBuilder::new().max_data_points(2).build();
1616
1617 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 assert_eq!(data.len(), 2);
1626 assert_eq!(data[0].value, 2.0); assert_eq!(data[1].value, 3.0); }
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 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 assert!(cpu.is_finite() && cpu >= 0.0, "got {cpu}");
1656 }
1657
1658 #[test]
1659 fn test_gpu_metrics_are_honestly_absent_not_fabricated() {
1660 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 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 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 tokio::time::sleep(Duration::from_millis(150)).await;
1760 dashboard.stop();
1761
1762 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}