1use anyhow::Result;
7use scirs2_core::random::*; use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, VecDeque};
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12use tokio::sync::broadcast;
13use tokio::time::interval;
14use tokio_stream::wrappers::BroadcastStream;
15use uuid::Uuid;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct DashboardConfig {
20 pub websocket_port: u16,
22 pub update_frequency_ms: u64,
24 pub max_data_points: usize,
26 pub enable_gpu_monitoring: bool,
28 pub enable_memory_profiling: bool,
30 pub enable_network_monitoring: bool,
32 pub enable_performance_alerts: bool,
34 pub alert_thresholds: AlertThresholds,
36}
37
38impl Default for DashboardConfig {
39 fn default() -> Self {
40 Self {
41 websocket_port: 8080,
42 update_frequency_ms: 100,
43 max_data_points: 1000,
44 enable_gpu_monitoring: true,
45 enable_memory_profiling: true,
46 enable_network_monitoring: false,
47 enable_performance_alerts: true,
48 alert_thresholds: AlertThresholds::default(),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct AlertThresholds {
56 pub memory_threshold: f64,
58 pub gpu_utilization_threshold: f64,
60 pub temperature_threshold: f64,
62 pub loss_spike_threshold: f64,
64 pub gradient_norm_threshold: f64,
66}
67
68impl Default for AlertThresholds {
69 fn default() -> Self {
70 Self {
71 memory_threshold: 90.0,
72 gpu_utilization_threshold: 95.0,
73 temperature_threshold: 80.0,
74 loss_spike_threshold: 2.0,
75 gradient_norm_threshold: 10.0,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct MetricDataPoint {
83 pub timestamp: u64,
84 pub value: f64,
85 pub label: String,
86 pub category: MetricCategory,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
91pub enum MetricCategory {
92 Training,
93 Memory,
94 GPU,
95 Network,
96 Performance,
97 Custom(String),
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct DashboardAlert {
103 pub id: String,
104 pub timestamp: u64,
105 pub severity: AlertSeverity,
106 pub category: MetricCategory,
107 pub title: String,
108 pub message: String,
109 pub value: Option<f64>,
110 pub threshold: Option<f64>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
115pub enum AlertSeverity {
116 Info,
117 Warning,
118 Error,
119 Critical,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(tag = "type")]
125pub enum WebSocketMessage {
126 MetricUpdate {
127 data: Vec<MetricDataPoint>,
128 },
129 Alert {
130 alert: DashboardAlert,
131 },
132 ConfigUpdate {
133 config: DashboardConfig,
134 },
135 SessionInfo {
136 session_id: String,
137 uptime: u64,
138 },
139 HistoricalData {
140 category: MetricCategory,
141 data: Vec<MetricDataPoint>,
142 },
143 SystemStats {
144 stats: SystemStats,
145 },
146 #[serde(untagged)]
147 Generic {
148 message_type: String,
149 data: serde_json::Value,
150 timestamp: u64,
151 session_id: String,
152 },
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct AnomalyDetection {
158 pub timestamp: u64,
159 pub value: f64,
160 pub expected_range: (f64, f64),
161 pub anomaly_type: AnomalyType,
162 pub confidence_score: f64,
163 pub category: MetricCategory,
164 pub description: String,
165}
166
167#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
169pub enum AnomalyType {
170 Spike,
171 Drop,
172 GradualIncrease,
173 GradualDecrease,
174 Outlier,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct DashboardVisualizationData {
180 pub heatmap_data: HashMap<MetricCategory, HeatmapData>,
181 pub time_series_data: HashMap<MetricCategory, Vec<TimeSeriesPoint>>,
182 pub correlation_matrix: Vec<Vec<f64>>,
183 pub performance_distribution: HashMap<MetricCategory, HistogramData>,
184 pub generated_at: u64,
185 pub session_id: String,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct HeatmapData {
191 pub intensity: f64,
192 pub normalized_intensity: f64,
193 pub data_points: usize,
194 pub timestamp: u64,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct TimeSeriesPoint {
200 pub timestamp: u64,
201 pub value: f64,
202 pub label: String,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct HistogramData {
208 pub bins: Vec<HistogramBin>,
209 pub max_frequency: usize,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct HistogramBin {
215 pub range_start: f64,
216 pub range_end: f64,
217 pub frequency: usize,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct PerformancePrediction {
223 pub category: MetricCategory,
224 pub predicted_value: f64,
225 pub confidence_interval: (f64, f64),
226 pub trend_direction: TrendDirection,
227 pub trend_strength: f64,
228 pub prediction_horizon_hours: u64,
229 pub model_accuracy: f64,
230 pub generated_at: u64,
231 pub recommendations: Vec<String>,
232}
233
234#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
236pub enum TrendDirection {
237 Increasing,
238 Decreasing,
239 Stable,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct DashboardTheme {
245 pub name: String,
246 pub primary_color: String,
247 pub secondary_color: String,
248 pub background_color: String,
249 pub text_color: String,
250 pub accent_color: String,
251 pub chart_colors: Vec<String>,
252 pub dark_mode: bool,
253 pub font_family: String,
254 pub border_radius: u8,
255}
256
257impl Default for DashboardTheme {
258 fn default() -> Self {
259 Self {
260 name: "Default".to_string(),
261 primary_color: "#3b82f6".to_string(),
262 secondary_color: "#64748b".to_string(),
263 background_color: "#ffffff".to_string(),
264 text_color: "#1f2937".to_string(),
265 accent_color: "#10b981".to_string(),
266 chart_colors: vec![
267 "#3b82f6".to_string(),
268 "#ef4444".to_string(),
269 "#10b981".to_string(),
270 "#f59e0b".to_string(),
271 "#8b5cf6".to_string(),
272 ],
273 dark_mode: false,
274 font_family: "Inter, sans-serif".to_string(),
275 border_radius: 8,
276 }
277 }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub enum ExportFormat {
283 JSON,
284 CSV,
285 MessagePack,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct SystemStats {
291 pub uptime: u64,
292 pub total_alerts: usize,
293 pub active_connections: usize,
294 pub data_points_collected: usize,
295 pub memory_usage_mb: f64,
296 pub cpu_usage_percent: f64,
297}
298
299#[derive(Debug)]
301pub struct RealtimeDashboard {
302 config: Arc<Mutex<DashboardConfig>>,
303 session_id: String,
304 start_time: Instant,
305 metric_data: Arc<Mutex<HashMap<MetricCategory, VecDeque<MetricDataPoint>>>>,
306 alert_history: Arc<Mutex<VecDeque<DashboardAlert>>>,
307 websocket_sender: broadcast::Sender<WebSocketMessage>,
308 active_connections: Arc<Mutex<usize>>,
309 total_data_points: Arc<Mutex<usize>>,
310 is_running: Arc<Mutex<bool>>,
311}
312
313impl RealtimeDashboard {
314 pub fn new(config: DashboardConfig) -> Self {
316 let (websocket_sender, _) = broadcast::channel(1000);
317
318 Self {
319 config: Arc::new(Mutex::new(config)),
320 session_id: Uuid::new_v4().to_string(),
321 start_time: Instant::now(),
322 metric_data: Arc::new(Mutex::new(HashMap::new())),
323 alert_history: Arc::new(Mutex::new(VecDeque::new())),
324 websocket_sender,
325 active_connections: Arc::new(Mutex::new(0)),
326 total_data_points: Arc::new(Mutex::new(0)),
327 is_running: Arc::new(Mutex::new(false)),
328 }
329 }
330
331 pub async fn start(&self) -> Result<()> {
333 {
334 let mut running = self
335 .is_running
336 .lock()
337 .map_err(|_| anyhow::anyhow!("Failed to acquire running state lock"))?;
338 if *running {
339 return Ok(());
340 }
341 *running = true;
342 }
343
344 self.start_data_collection().await?;
346
347 self.start_system_stats_updates().await?;
349
350 self.start_alert_monitoring().await?;
352
353 Ok(())
354 }
355
356 pub fn stop(&self) {
358 if let Ok(mut running) = self.is_running.lock() {
359 *running = false;
360 }
361 }
362
363 pub fn add_metric(&self, category: MetricCategory, label: String, value: f64) -> Result<()> {
365 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
366
367 let data_point = MetricDataPoint {
368 timestamp,
369 value,
370 label,
371 category: category.clone(),
372 };
373
374 {
376 let mut data = self
377 .metric_data
378 .lock()
379 .map_err(|_| anyhow::anyhow!("Failed to acquire metric data lock"))?;
380 let category_data = data.entry(category.clone()).or_insert_with(VecDeque::new);
381
382 category_data.push_back(data_point.clone());
383
384 let max_points = self
385 .config
386 .lock()
387 .map_err(|_| anyhow::anyhow!("Failed to acquire config lock"))?
388 .max_data_points;
389 while category_data.len() > max_points {
390 category_data.pop_front();
391 }
392 }
393
394 {
396 if let Ok(mut total) = self.total_data_points.lock() {
397 *total += 1;
398 }
399 }
400
401 let message = WebSocketMessage::MetricUpdate {
403 data: vec![data_point],
404 };
405
406 let _ = self.websocket_sender.send(message);
407
408 self.check_for_alerts(&category, value);
410
411 Ok(())
412 }
413
414 pub fn add_metrics(&self, metrics: Vec<(MetricCategory, String, f64)>) -> Result<()> {
416 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
417
418 let mut data_points = Vec::new();
419
420 for (category, label, value) in metrics {
422 let data_point = MetricDataPoint {
423 timestamp,
424 value,
425 label,
426 category: category.clone(),
427 };
428
429 {
431 let mut data =
432 self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
433 let category_data = data.entry(category.clone()).or_default();
434 category_data.push_back(data_point.clone());
435
436 let max_points = self
437 .config
438 .lock()
439 .unwrap_or_else(|poisoned| poisoned.into_inner())
440 .max_data_points;
441 while category_data.len() > max_points {
442 category_data.pop_front();
443 }
444 }
445
446 data_points.push(data_point);
447
448 self.check_for_alerts(&category, value);
450 }
451
452 {
454 let mut total =
455 self.total_data_points.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
456 *total += data_points.len();
457 }
458
459 let message = WebSocketMessage::MetricUpdate { data: data_points };
461 let _ = self.websocket_sender.send(message);
462
463 Ok(())
464 }
465
466 pub fn create_alert(
468 &self,
469 severity: AlertSeverity,
470 category: MetricCategory,
471 title: String,
472 message: String,
473 value: Option<f64>,
474 threshold: Option<f64>,
475 ) -> Result<()> {
476 let alert = DashboardAlert {
477 id: Uuid::new_v4().to_string(),
478 timestamp: SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
479 severity,
480 category,
481 title,
482 message,
483 value,
484 threshold,
485 };
486
487 {
489 let mut history =
490 self.alert_history.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
491 history.push_back(alert.clone());
492
493 while history.len() > 100 {
495 history.pop_front();
496 }
497 }
498
499 let message = WebSocketMessage::Alert { alert };
501 let _ = self.websocket_sender.send(message);
502
503 Ok(())
504 }
505
506 pub fn get_historical_data(&self, category: &MetricCategory) -> Vec<MetricDataPoint> {
508 let data = self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
509 data.get(category)
510 .map(|deque| deque.iter().cloned().collect())
511 .unwrap_or_default()
512 }
513
514 pub fn get_system_stats(&self) -> SystemStats {
516 let uptime = self.start_time.elapsed().as_secs();
517 let total_alerts =
518 self.alert_history.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).len();
519 let active_connections =
520 *self.active_connections.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
521 let data_points_collected =
522 *self.total_data_points.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
523
524 let memory_usage_mb = self.estimate_memory_usage();
526 let cpu_usage_percent = self.estimate_cpu_usage();
527
528 SystemStats {
529 uptime,
530 total_alerts,
531 active_connections,
532 data_points_collected,
533 memory_usage_mb,
534 cpu_usage_percent,
535 }
536 }
537
538 pub fn subscribe(&self) -> BroadcastStream<WebSocketMessage> {
540 {
542 let mut connections =
543 self.active_connections.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
544 *connections += 1;
545 }
546
547 BroadcastStream::new(self.websocket_sender.subscribe())
548 }
549
550 pub fn update_config(&self, new_config: DashboardConfig) -> Result<()> {
552 {
553 let mut config = self.config.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
554 *config = new_config.clone();
555 }
556
557 let message = WebSocketMessage::ConfigUpdate { config: new_config };
559 let _ = self.websocket_sender.send(message);
560
561 Ok(())
562 }
563
564 pub fn get_config(&self) -> DashboardConfig {
566 self.config.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).clone()
567 }
568
569 async fn start_data_collection(&self) -> Result<()> {
571 let config = self.config.clone();
572 let _metric_data = self.metric_data.clone();
573 let websocket_sender = self.websocket_sender.clone();
574 let is_running = self.is_running.clone();
575
576 tokio::spawn(async move {
577 let mut interval = interval(Duration::from_millis(
578 config
579 .lock()
580 .unwrap_or_else(|poisoned| poisoned.into_inner())
581 .update_frequency_ms,
582 ));
583
584 while *is_running.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) {
585 interval.tick().await;
586
587 if let Ok(metrics) = Self::collect_system_metrics(&config).await {
589 let message = WebSocketMessage::MetricUpdate { data: metrics };
590 let _ = websocket_sender.send(message);
591 }
592 }
593 });
594
595 Ok(())
596 }
597
598 async fn start_system_stats_updates(&self) -> Result<()> {
600 let websocket_sender = self.websocket_sender.clone();
601 let start_time = self.start_time;
602 let alert_history = self.alert_history.clone();
603 let active_connections = self.active_connections.clone();
604 let total_data_points = self.total_data_points.clone();
605 let is_running = self.is_running.clone();
606
607 tokio::spawn(async move {
608 let mut interval = interval(Duration::from_secs(5)); while *is_running.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) {
611 interval.tick().await;
612
613 let stats = SystemStats {
614 uptime: start_time.elapsed().as_secs(),
615 total_alerts: alert_history
616 .lock()
617 .unwrap_or_else(|poisoned| poisoned.into_inner())
618 .len(),
619 active_connections: *active_connections
620 .lock()
621 .unwrap_or_else(|poisoned| poisoned.into_inner()),
622 data_points_collected: *total_data_points
623 .lock()
624 .unwrap_or_else(|poisoned| poisoned.into_inner()),
625 memory_usage_mb: 0.0, cpu_usage_percent: 0.0, };
628
629 let message = WebSocketMessage::SystemStats { stats };
630 let _ = websocket_sender.send(message);
631 }
632 });
633
634 Ok(())
635 }
636
637 async fn start_alert_monitoring(&self) -> Result<()> {
639 let config = self.config.clone();
640 let metric_data = self.metric_data.clone();
641 let is_running = self.is_running.clone();
642
643 tokio::spawn(async move {
644 let mut interval = interval(Duration::from_secs(1));
645
646 while *is_running.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) {
647 interval.tick().await;
648
649 Self::check_threshold_breaches(&config, &metric_data).await;
651 }
652 });
653
654 Ok(())
655 }
656
657 async fn collect_system_metrics(
659 config: &Arc<Mutex<DashboardConfig>>,
660 ) -> Result<Vec<MetricDataPoint>> {
661 let mut metrics = Vec::new();
662 let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
663
664 let cfg = config.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
665
666 if cfg.enable_memory_profiling {
667 let memory_usage = Self::get_memory_usage();
669 metrics.push(MetricDataPoint {
670 timestamp,
671 value: memory_usage,
672 label: "Memory Usage".to_string(),
673 category: MetricCategory::Memory,
674 });
675 }
676
677 if cfg.enable_gpu_monitoring {
678 let gpu_utilization = Self::get_gpu_utilization();
680 metrics.push(MetricDataPoint {
681 timestamp,
682 value: gpu_utilization,
683 label: "GPU Utilization".to_string(),
684 category: MetricCategory::GPU,
685 });
686
687 let gpu_memory = Self::get_gpu_memory_usage();
688 metrics.push(MetricDataPoint {
689 timestamp,
690 value: gpu_memory,
691 label: "GPU Memory".to_string(),
692 category: MetricCategory::GPU,
693 });
694 }
695
696 Ok(metrics)
697 }
698
699 fn check_for_alerts(&self, category: &MetricCategory, value: f64) {
701 let config = self.config.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
702 let thresholds = &config.alert_thresholds;
703
704 match category {
705 MetricCategory::Memory if value > thresholds.memory_threshold => {
706 let _ = self.create_alert(
707 AlertSeverity::Warning,
708 category.clone(),
709 "High Memory Usage".to_string(),
710 format!(
711 "Memory usage is {:.1}% (threshold: {:.1}%)",
712 value, thresholds.memory_threshold
713 ),
714 Some(value),
715 Some(thresholds.memory_threshold),
716 );
717 },
718 MetricCategory::GPU if value > thresholds.gpu_utilization_threshold => {
719 let _ = self.create_alert(
720 AlertSeverity::Warning,
721 category.clone(),
722 "High GPU Utilization".to_string(),
723 format!(
724 "GPU utilization is {:.1}% (threshold: {:.1}%)",
725 value, thresholds.gpu_utilization_threshold
726 ),
727 Some(value),
728 Some(thresholds.gpu_utilization_threshold),
729 );
730 },
731 MetricCategory::Training if value > thresholds.loss_spike_threshold => {
732 let _ = self.create_alert(
733 AlertSeverity::Error,
734 category.clone(),
735 "Training Loss Spike".to_string(),
736 format!(
737 "Loss spike detected: {:.4} (threshold: {:.4})",
738 value, thresholds.loss_spike_threshold
739 ),
740 Some(value),
741 Some(thresholds.loss_spike_threshold),
742 );
743 },
744 _ => {},
745 }
746 }
747
748 async fn check_threshold_breaches(
750 config: &Arc<Mutex<DashboardConfig>>,
751 metric_data: &Arc<Mutex<HashMap<MetricCategory, VecDeque<MetricDataPoint>>>>,
752 ) {
753 let _config = config.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
754 let _data = metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
755
756 }
759
760 fn get_memory_usage() -> f64 {
762 50.0 + (thread_rng().random::<f64>() * 40.0)
764 }
765
766 fn get_gpu_utilization() -> f64 {
768 30.0 + (thread_rng().random::<f64>() * 60.0)
770 }
771
772 fn get_gpu_memory_usage() -> f64 {
774 40.0 + (thread_rng().random::<f64>() * 50.0)
776 }
777
778 fn estimate_memory_usage(&self) -> f64 {
780 let data = self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
781 let mut total_points = 0;
782
783 for deque in data.values() {
784 total_points += deque.len();
785 }
786
787 (total_points * 100) as f64 / (1024.0 * 1024.0)
789 }
790
791 fn estimate_cpu_usage(&self) -> f64 {
793 5.0 + (thread_rng().random::<f64>() * 10.0)
795 }
796
797 pub async fn detect_metric_anomalies(
799 &self,
800 category: &MetricCategory,
801 ) -> Result<Vec<AnomalyDetection>> {
802 let data = self.get_historical_data(category);
803 let mut anomalies = Vec::new();
804
805 if data.len() < 10 {
806 return Ok(anomalies); }
808
809 let values: Vec<f64> = data.iter().map(|d| d.value).collect();
811 let mean = values.iter().sum::<f64>() / values.len() as f64;
812 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
813 let std_dev = variance.sqrt();
814
815 let z_threshold = 2.0; for point in data.iter() {
818 let z_score = (point.value - mean).abs() / std_dev;
819 if z_score > z_threshold {
820 let anomaly_type =
821 if point.value > mean { AnomalyType::Spike } else { AnomalyType::Drop };
822
823 anomalies.push(AnomalyDetection {
824 timestamp: point.timestamp,
825 value: point.value,
826 expected_range: (mean - std_dev, mean + std_dev),
827 anomaly_type,
828 confidence_score: (z_score - z_threshold) / z_threshold,
829 category: category.clone(),
830 description: format!(
831 "Detected {} in {} metrics: value {} (Z-score: {:.2})",
832 match anomaly_type {
833 AnomalyType::Spike => "spike",
834 AnomalyType::Drop => "drop",
835 _ => "anomaly",
836 },
837 match category {
838 MetricCategory::Training => "training",
839 MetricCategory::Memory => "memory",
840 MetricCategory::GPU => "GPU",
841 MetricCategory::Network => "network",
842 MetricCategory::Performance => "performance",
843 MetricCategory::Custom(name) => name,
844 },
845 point.value,
846 z_score
847 ),
848 });
849 }
850 }
851
852 if data.len() >= 20 {
854 let recent_window = &data[data.len() - 10..];
855 let earlier_window = &data[data.len() - 20..data.len() - 10];
856
857 let recent_avg =
858 recent_window.iter().map(|d| d.value).sum::<f64>() / recent_window.len() as f64;
859 let earlier_avg =
860 earlier_window.iter().map(|d| d.value).sum::<f64>() / earlier_window.len() as f64;
861
862 let trend_change = (recent_avg - earlier_avg) / earlier_avg;
863
864 if trend_change.abs() > 0.3 {
865 if let Some(last_point) = recent_window.last() {
867 anomalies.push(AnomalyDetection {
868 timestamp: last_point.timestamp,
869 value: recent_avg,
870 expected_range: (earlier_avg * 0.9, earlier_avg * 1.1),
871 anomaly_type: if trend_change > 0.0 {
872 AnomalyType::GradualIncrease
873 } else {
874 AnomalyType::GradualDecrease
875 },
876 confidence_score: trend_change.abs(),
877 category: category.clone(),
878 description: format!(
879 "Detected gradual {} trend: {:.1}% change over recent measurements",
880 if trend_change > 0.0 { "increase" } else { "decrease" },
881 trend_change.abs() * 100.0
882 ),
883 });
884 }
885 }
886 }
887
888 Ok(anomalies)
889 }
890
891 pub fn generate_advanced_visualizations(&self) -> Result<DashboardVisualizationData> {
893 let mut heatmap_data = HashMap::new();
894 let mut time_series_data = HashMap::new();
895 let mut correlation_matrix = Vec::new();
896 let mut performance_distribution = HashMap::new();
897
898 for (category, data) in
900 self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).iter()
901 {
902 if data.len() >= 10 {
903 let recent_data: Vec<f64> = data.iter().rev().take(10).map(|d| d.value).collect();
904 let avg_value = recent_data.iter().sum::<f64>() / recent_data.len() as f64;
905
906 heatmap_data.insert(
907 category.clone(),
908 HeatmapData {
909 intensity: avg_value,
910 normalized_intensity: (avg_value / (avg_value + 1.0)).min(1.0), data_points: recent_data.len(),
912 timestamp: SystemTime::now()
913 .duration_since(UNIX_EPOCH)
914 .unwrap_or_default()
915 .as_secs(),
916 },
917 );
918
919 let time_series: Vec<TimeSeriesPoint> = data
921 .iter()
922 .map(|d| TimeSeriesPoint {
923 timestamp: d.timestamp,
924 value: d.value,
925 label: d.label.clone(),
926 })
927 .collect();
928
929 time_series_data.insert(category.clone(), time_series);
930
931 let values: Vec<f64> = data.iter().map(|d| d.value).collect();
933 let histogram = self.create_histogram(&values, 10);
934 performance_distribution.insert(category.clone(), histogram);
935 }
936 }
937
938 let categories: Vec<&MetricCategory> = heatmap_data.keys().collect();
940 for (i, cat1) in categories.iter().enumerate() {
941 let mut row = Vec::new();
942 for (j, cat2) in categories.iter().enumerate() {
943 if i == j {
944 row.push(1.0); } else {
946 let corr = self.calculate_correlation_coefficient(cat1, cat2);
948 row.push(corr);
949 }
950 }
951 correlation_matrix.push(row);
952 }
953
954 Ok(DashboardVisualizationData {
955 heatmap_data,
956 time_series_data,
957 correlation_matrix,
958 performance_distribution,
959 generated_at: SystemTime::now()
960 .duration_since(UNIX_EPOCH)
961 .unwrap_or_default()
962 .as_secs(),
963 session_id: self.session_id.clone(),
964 })
965 }
966
967 pub async fn predict_performance_trends(
969 &self,
970 category: &MetricCategory,
971 hours_ahead: u64,
972 ) -> Result<PerformancePrediction> {
973 let data = self.get_historical_data(category);
974
975 if data.len() < 20 {
976 return Err(anyhow::anyhow!(
977 "Insufficient data for prediction (need at least 20 points)"
978 ));
979 }
980
981 let values: Vec<f64> = data.iter().map(|d| d.value).collect();
982 let timestamps: Vec<u64> = data.iter().map(|d| d.timestamp).collect();
983
984 let n = values.len() as f64;
986 let sum_x = timestamps.iter().sum::<u64>() as f64;
987 let sum_y = values.iter().sum::<f64>();
988 let sum_xy = timestamps.iter().zip(&values).map(|(x, y)| *x as f64 * y).sum::<f64>();
989 let sum_x2 = timestamps.iter().map(|x| (*x as f64).powi(2)).sum::<f64>();
990
991 let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x.powi(2));
992 let intercept = (sum_y - slope * sum_x) / n;
993
994 let current_time =
996 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
997 let prediction_time = current_time + (hours_ahead * 3600);
998 let predicted_value = slope * prediction_time as f64 + intercept;
999
1000 let mean = sum_y / n;
1002 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
1003 let std_error = (variance / n).sqrt();
1004 let confidence_interval = std_error * 1.96; let trend_strength = slope.abs() / mean.abs();
1008 let trend_direction = if slope > 0.01 {
1009 TrendDirection::Increasing
1010 } else if slope < -0.01 {
1011 TrendDirection::Decreasing
1012 } else {
1013 TrendDirection::Stable
1014 };
1015
1016 Ok(PerformancePrediction {
1017 category: category.clone(),
1018 predicted_value,
1019 confidence_interval: (
1020 predicted_value - confidence_interval,
1021 predicted_value + confidence_interval,
1022 ),
1023 trend_direction,
1024 trend_strength,
1025 prediction_horizon_hours: hours_ahead,
1026 model_accuracy: 1.0 - (std_error / mean.abs()).min(1.0), generated_at: current_time,
1028 recommendations: self.generate_performance_recommendations(
1029 &trend_direction,
1030 trend_strength,
1031 predicted_value,
1032 ),
1033 })
1034 }
1035
1036 pub fn apply_dashboard_theme(&self, theme: DashboardTheme) -> Result<()> {
1038 let theme_message = WebSocketMessage::Generic {
1040 message_type: "theme_update".to_string(),
1041 data: serde_json::to_value(&theme)?,
1042 timestamp: SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
1043 session_id: self.session_id.clone(),
1044 };
1045
1046 if self.websocket_sender.send(theme_message).is_err() {
1047 }
1049
1050 Ok(())
1051 }
1052
1053 pub async fn export_dashboard_data(
1055 &self,
1056 format: ExportFormat,
1057 time_range: Option<(u64, u64)>,
1058 ) -> Result<Vec<u8>> {
1059 let data = if let Some((start, end)) = time_range {
1060 self.get_filtered_data(start, end)
1061 } else {
1062 self.get_all_data()
1063 };
1064
1065 match format {
1066 ExportFormat::JSON => {
1067 let json_data = serde_json::to_string_pretty(&data)?;
1068 Ok(json_data.into_bytes())
1069 },
1070 ExportFormat::CSV => {
1071 let mut csv_data = String::from("timestamp,category,label,value\n");
1072 for (category, points) in data {
1073 for point in points {
1074 csv_data.push_str(&format!(
1075 "{},{:?},{},{}\n",
1076 point.timestamp, category, point.label, point.value
1077 ));
1078 }
1079 }
1080 Ok(csv_data.into_bytes())
1081 },
1082 ExportFormat::MessagePack => {
1083 let json_data = serde_json::to_string(&data)?;
1086 Ok(json_data.into_bytes())
1087 },
1088 }
1089 }
1090
1091 fn create_histogram(&self, values: &[f64], bins: usize) -> HistogramData {
1094 if values.is_empty() {
1095 return HistogramData {
1096 bins: Vec::new(),
1097 max_frequency: 0,
1098 };
1099 }
1100
1101 let min_val = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
1102 let max_val = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1103 let bin_width = (max_val - min_val) / bins as f64;
1104
1105 let mut histogram_bins = vec![0; bins];
1106
1107 for &value in values {
1108 let bin_idx = ((value - min_val) / bin_width).floor() as usize;
1109 let bin_idx = bin_idx.min(bins - 1); histogram_bins[bin_idx] += 1;
1111 }
1112
1113 let max_frequency = *histogram_bins.iter().max().unwrap_or(&0);
1114
1115 let bins_data: Vec<HistogramBin> = histogram_bins
1116 .into_iter()
1117 .enumerate()
1118 .map(|(i, count)| HistogramBin {
1119 range_start: min_val + i as f64 * bin_width,
1120 range_end: min_val + (i + 1) as f64 * bin_width,
1121 frequency: count,
1122 })
1123 .collect();
1124
1125 HistogramData {
1126 bins: bins_data,
1127 max_frequency,
1128 }
1129 }
1130
1131 fn calculate_correlation_coefficient(
1132 &self,
1133 cat1: &MetricCategory,
1134 cat2: &MetricCategory,
1135 ) -> f64 {
1136 let data = self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1137
1138 let data1 = match data.get(cat1) {
1139 Some(d) => d,
1140 None => return 0.0,
1141 };
1142
1143 let data2 = match data.get(cat2) {
1144 Some(d) => d,
1145 None => return 0.0,
1146 };
1147
1148 if data1.len() < 2 || data2.len() < 2 {
1149 return 0.0;
1150 }
1151
1152 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();
1155 let values2: Vec<f64> = data2.iter().rev().take(min_len).map(|d| d.value).collect();
1156
1157 let n = values1.len() as f64;
1159 let mean1 = values1.iter().sum::<f64>() / n;
1160 let mean2 = values2.iter().sum::<f64>() / n;
1161
1162 let covariance = values1
1163 .iter()
1164 .zip(&values2)
1165 .map(|(v1, v2)| (v1 - mean1) * (v2 - mean2))
1166 .sum::<f64>()
1167 / n;
1168
1169 let std1 = (values1.iter().map(|v| (v - mean1).powi(2)).sum::<f64>() / n).sqrt();
1170 let std2 = (values2.iter().map(|v| (v - mean2).powi(2)).sum::<f64>() / n).sqrt();
1171
1172 if std1 == 0.0 || std2 == 0.0 {
1173 0.0
1174 } else {
1175 covariance / (std1 * std2)
1176 }
1177 }
1178
1179 fn generate_performance_recommendations(
1180 &self,
1181 trend: &TrendDirection,
1182 strength: f64,
1183 predicted_value: f64,
1184 ) -> Vec<String> {
1185 let mut recommendations = Vec::new();
1186
1187 match trend {
1188 TrendDirection::Increasing => {
1189 if strength > 0.1 {
1190 recommendations.push(
1191 "Monitor for potential resource exhaustion due to increasing trend"
1192 .to_string(),
1193 );
1194 recommendations.push("Consider scaling resources proactively".to_string());
1195 }
1196 if predicted_value > 90.0 {
1197 recommendations.push(
1198 "Critical threshold approaching - immediate action recommended".to_string(),
1199 );
1200 }
1201 },
1202 TrendDirection::Decreasing => {
1203 if strength > 0.05 {
1204 recommendations
1205 .push("Investigate potential performance degradation".to_string());
1206 recommendations.push("Check for resource leaks or inefficiencies".to_string());
1207 }
1208 },
1209 TrendDirection::Stable => {
1210 recommendations
1211 .push("Performance trend is stable - continue monitoring".to_string());
1212 },
1213 }
1214
1215 if recommendations.is_empty() {
1216 recommendations.push("No specific recommendations at this time".to_string());
1217 }
1218
1219 recommendations
1220 }
1221
1222 fn get_filtered_data(
1223 &self,
1224 start: u64,
1225 end: u64,
1226 ) -> HashMap<MetricCategory, VecDeque<MetricDataPoint>> {
1227 let data = self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
1228 let mut filtered_data = HashMap::new();
1229
1230 for (category, points) in data.iter() {
1231 let filtered_points: VecDeque<MetricDataPoint> = points
1232 .iter()
1233 .filter(|p| p.timestamp >= start && p.timestamp <= end)
1234 .cloned()
1235 .collect();
1236
1237 if !filtered_points.is_empty() {
1238 filtered_data.insert(category.clone(), filtered_points);
1239 }
1240 }
1241
1242 filtered_data
1243 }
1244
1245 fn get_all_data(&self) -> HashMap<MetricCategory, VecDeque<MetricDataPoint>> {
1246 self.metric_data.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).clone()
1247 }
1248}
1249
1250#[derive(Debug, Default)]
1252pub struct DashboardBuilder {
1253 config: DashboardConfig,
1254}
1255
1256impl DashboardBuilder {
1257 pub fn new() -> Self {
1259 Self::default()
1260 }
1261
1262 pub fn port(mut self, port: u16) -> Self {
1264 self.config.websocket_port = port;
1265 self
1266 }
1267
1268 pub fn update_frequency(mut self, frequency_ms: u64) -> Self {
1270 self.config.update_frequency_ms = frequency_ms;
1271 self
1272 }
1273
1274 pub fn max_data_points(mut self, max_points: usize) -> Self {
1276 self.config.max_data_points = max_points;
1277 self
1278 }
1279
1280 pub fn gpu_monitoring(mut self, enabled: bool) -> Self {
1282 self.config.enable_gpu_monitoring = enabled;
1283 self
1284 }
1285
1286 pub fn memory_profiling(mut self, enabled: bool) -> Self {
1288 self.config.enable_memory_profiling = enabled;
1289 self
1290 }
1291
1292 pub fn alert_thresholds(mut self, thresholds: AlertThresholds) -> Self {
1294 self.config.alert_thresholds = thresholds;
1295 self
1296 }
1297
1298 pub fn build(self) -> RealtimeDashboard {
1300 RealtimeDashboard::new(self.config)
1301 }
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306 use super::*;
1307 use futures::StreamExt;
1308 use std::time::Duration;
1309
1310 #[tokio::test]
1311 async fn test_dashboard_creation() {
1312 let dashboard = DashboardBuilder::new()
1313 .port(8081)
1314 .update_frequency(50)
1315 .max_data_points(500)
1316 .build();
1317
1318 assert_eq!(dashboard.get_config().websocket_port, 8081);
1319 assert_eq!(dashboard.get_config().update_frequency_ms, 50);
1320 assert_eq!(dashboard.get_config().max_data_points, 500);
1321 }
1322
1323 #[tokio::test]
1324 async fn test_metric_addition() {
1325 let dashboard = DashboardBuilder::new().build();
1326
1327 let result = dashboard.add_metric(MetricCategory::Training, "loss".to_string(), 0.5);
1328
1329 assert!(result.is_ok());
1330
1331 let historical_data = dashboard.get_historical_data(&MetricCategory::Training);
1332 assert_eq!(historical_data.len(), 1);
1333 assert_eq!(historical_data[0].value, 0.5);
1334 assert_eq!(historical_data[0].label, "loss");
1335 }
1336
1337 #[tokio::test]
1338 async fn test_batch_metrics() {
1339 let dashboard = DashboardBuilder::new().build();
1340
1341 let metrics = vec![
1342 (MetricCategory::Training, "loss".to_string(), 0.5),
1343 (MetricCategory::Training, "accuracy".to_string(), 0.9),
1344 (MetricCategory::GPU, "utilization".to_string(), 75.0),
1345 ];
1346
1347 let result = dashboard.add_metrics(metrics);
1348 assert!(result.is_ok());
1349
1350 let training_data = dashboard.get_historical_data(&MetricCategory::Training);
1351 assert_eq!(training_data.len(), 2);
1352
1353 let gpu_data = dashboard.get_historical_data(&MetricCategory::GPU);
1354 assert_eq!(gpu_data.len(), 1);
1355 }
1356
1357 #[tokio::test]
1358 async fn test_alert_creation() {
1359 let dashboard = DashboardBuilder::new().build();
1360
1361 let result = dashboard.create_alert(
1362 AlertSeverity::Warning,
1363 MetricCategory::Memory,
1364 "High Memory".to_string(),
1365 "Memory usage is high".to_string(),
1366 Some(95.0),
1367 Some(90.0),
1368 );
1369
1370 assert!(result.is_ok());
1371
1372 let history = dashboard.alert_history.lock().expect("lock should not be poisoned");
1373 assert_eq!(history.len(), 1);
1374 assert_eq!(history[0].title, "High Memory");
1375 }
1376
1377 #[tokio::test]
1378 async fn test_websocket_subscription() {
1379 let dashboard = DashboardBuilder::new().build();
1380
1381 let mut stream = dashboard.subscribe();
1382
1383 let dashboard_clone = Arc::new(dashboard);
1385 let dashboard_for_task = dashboard_clone.clone();
1386
1387 tokio::spawn(async move {
1388 let _ = dashboard_for_task.start().await;
1389 });
1390
1391 let _ =
1393 dashboard_clone.add_metric(MetricCategory::Training, "test_metric".to_string(), 42.0);
1394
1395 let message_result = tokio::time::timeout(Duration::from_millis(100), stream.next()).await;
1397
1398 dashboard_clone.stop();
1399
1400 assert!(message_result.is_ok());
1402 if let Ok(Some(Ok(message))) = message_result {
1403 match message {
1404 WebSocketMessage::MetricUpdate { data } => {
1405 assert!(!data.is_empty());
1406 assert_eq!(data[0].value, 42.0);
1407 assert_eq!(data[0].label, "test_metric");
1408 },
1409 _ => panic!("Expected MetricUpdate message"),
1410 }
1411 }
1412 }
1413
1414 #[tokio::test]
1415 async fn test_system_stats() {
1416 let dashboard = DashboardBuilder::new().build();
1417
1418 let _ = dashboard.add_metric(MetricCategory::Training, "loss".to_string(), 0.5);
1420 let _ = dashboard.create_alert(
1421 AlertSeverity::Info,
1422 MetricCategory::Training,
1423 "Test Alert".to_string(),
1424 "Test message".to_string(),
1425 None,
1426 None,
1427 );
1428
1429 let stats = dashboard.get_system_stats();
1430
1431 assert_eq!(stats.data_points_collected, 1);
1432 assert_eq!(stats.total_alerts, 1);
1433 }
1435
1436 #[tokio::test]
1437 async fn test_data_point_limit() {
1438 let dashboard = DashboardBuilder::new().max_data_points(2).build();
1439
1440 let _ = dashboard.add_metric(MetricCategory::Training, "metric1".to_string(), 1.0);
1442 let _ = dashboard.add_metric(MetricCategory::Training, "metric2".to_string(), 2.0);
1443 let _ = dashboard.add_metric(MetricCategory::Training, "metric3".to_string(), 3.0);
1444
1445 let data = dashboard.get_historical_data(&MetricCategory::Training);
1446
1447 assert_eq!(data.len(), 2);
1449 assert_eq!(data[0].value, 2.0); assert_eq!(data[1].value, 3.0); }
1452}