ant_quic/
stats_dashboard.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Connection Statistics Dashboard
9//!
10//! This module provides a real-time dashboard for monitoring connection
11//! statistics, NAT traversal performance, and network health metrics.
12
13use crate::{
14    nat_traversal_api::{NatTraversalEvent, NatTraversalStatistics, PeerId},
15    quic_node::NodeStats,
16    terminal_ui,
17};
18use std::{
19    collections::{HashMap, VecDeque},
20    net::SocketAddr,
21    sync::Arc,
22    time::{Duration, Instant},
23};
24use tokio::sync::RwLock;
25
26/// Box drawing style
27#[derive(Debug, Clone, Copy)]
28pub enum BoxStyle {
29    /// Single line borders
30    Single,
31    /// Double line borders
32    Double,
33    /// Rounded corners
34    Rounded,
35}
36
37/// Draw a box with title and content
38fn draw_box(title: &str, content: &str, _style: BoxStyle, width: usize) -> String {
39    let mut result = String::new();
40
41    // Top border with title
42    let padding = width.saturating_sub(title.len() + 4);
43    let left_pad = padding / 2;
44    let right_pad = padding - left_pad;
45
46    result.push_str(&format!(
47        "╭{} {} {}╮\n",
48        "─".repeat(left_pad),
49        title,
50        "─".repeat(right_pad)
51    ));
52
53    // Content lines
54    for line in content.lines() {
55        let line_len = line.chars().count();
56        let padding = width.saturating_sub(line_len + 2);
57        result.push_str(&format!("│ {}{} │\n", line, " ".repeat(padding)));
58    }
59
60    // Bottom border
61    result.push_str(&format!("╰{}╯", "─".repeat(width - 2)));
62
63    result
64}
65
66/// Dashboard configuration
67#[derive(Debug, Clone)]
68pub struct DashboardConfig {
69    /// Update interval for the dashboard
70    pub update_interval: Duration,
71    /// Maximum number of historical data points
72    pub history_size: usize,
73    /// Enable detailed connection tracking
74    pub detailed_tracking: bool,
75    /// Enable performance graphs
76    pub show_graphs: bool,
77}
78
79impl Default for DashboardConfig {
80    fn default() -> Self {
81        Self {
82            update_interval: Duration::from_secs(1),
83            history_size: 60, // 1 minute of second-by-second data
84            detailed_tracking: true,
85            show_graphs: true,
86        }
87    }
88}
89
90/// Connection information
91#[derive(Debug, Clone)]
92pub struct ConnectionInfo {
93    /// Peer identifier
94    pub peer_id: PeerId,
95    /// Remote socket address
96    pub remote_address: SocketAddr,
97    /// Timestamp when the connection was established
98    pub connected_at: Instant,
99    /// Total bytes sent
100    pub bytes_sent: u64,
101    /// Total bytes received
102    pub bytes_received: u64,
103    /// Timestamp of last activity
104    pub last_activity: Instant,
105    /// Measured round trip time
106    pub rtt: Option<Duration>,
107    /// Packet loss ratio [0.0-1.0]
108    pub packet_loss: f64,
109    /// NAT type inferred for the peer
110    pub nat_type: String,
111}
112
113/// Historical data point
114#[derive(Debug, Clone)]
115struct DataPoint {
116    #[allow(dead_code)]
117    timestamp: Instant,
118    active_connections: usize,
119    nat_success_rate: f64,
120    #[allow(dead_code)]
121    bytes_per_second: u64,
122    #[allow(dead_code)]
123    avg_rtt: Duration,
124}
125
126/// Statistics dashboard
127pub struct StatsDashboard {
128    config: DashboardConfig,
129    /// Current node statistics
130    node_stats: Arc<RwLock<NodeStats>>,
131    /// NAT traversal statistics
132    nat_stats: Arc<RwLock<NatTraversalStatistics>>,
133    /// Active connections
134    connections: Arc<RwLock<HashMap<PeerId, ConnectionInfo>>>,
135    /// Historical data
136    #[allow(dead_code)]
137    history: Arc<RwLock<VecDeque<DataPoint>>>,
138    /// Dashboard start time
139    start_time: Instant,
140    /// Last update time
141    last_update: Arc<RwLock<Instant>>,
142}
143
144impl StatsDashboard {
145    /// Create new statistics dashboard
146    pub fn new(config: DashboardConfig) -> Self {
147        let history_size = config.history_size;
148        Self {
149            config,
150            node_stats: Arc::new(RwLock::new(NodeStats::default())),
151            nat_stats: Arc::new(RwLock::new(NatTraversalStatistics::default())),
152            connections: Arc::new(RwLock::new(HashMap::new())),
153            history: Arc::new(RwLock::new(VecDeque::with_capacity(history_size))),
154            start_time: Instant::now(),
155            last_update: Arc::new(RwLock::new(Instant::now())),
156        }
157    }
158
159    /// Get the dashboard configuration
160    pub fn config(&self) -> &DashboardConfig {
161        &self.config
162    }
163
164    /// Update node statistics
165    pub async fn update_node_stats(&self, stats: NodeStats) {
166        *self.node_stats.write().await = stats;
167    }
168
169    /// Update NAT traversal statistics
170    pub async fn update_nat_stats(&self, stats: NatTraversalStatistics) {
171        *self.nat_stats.write().await = stats;
172    }
173
174    /// Handle NAT traversal event
175    pub async fn handle_nat_event(&self, event: &NatTraversalEvent) {
176        match event {
177            NatTraversalEvent::ConnectionEstablished {
178                peer_id,
179                remote_address,
180            } => {
181                let mut connections = self.connections.write().await;
182                connections.insert(
183                    *peer_id,
184                    ConnectionInfo {
185                        peer_id: *peer_id,
186                        remote_address: *remote_address,
187                        connected_at: Instant::now(),
188                        bytes_sent: 0,
189                        bytes_received: 0,
190                        last_activity: Instant::now(),
191                        rtt: None,
192                        packet_loss: 0.0,
193                        nat_type: "Unknown".to_string(),
194                    },
195                );
196            }
197            NatTraversalEvent::TraversalFailed { peer_id, .. } => {
198                let mut connections = self.connections.write().await;
199                connections.remove(peer_id);
200            }
201            _ => {}
202        }
203    }
204
205    /// Update connection metrics
206    pub async fn update_connection_metrics(
207        &self,
208        peer_id: PeerId,
209        bytes_sent: u64,
210        bytes_received: u64,
211        rtt: Option<Duration>,
212    ) {
213        let mut connections = self.connections.write().await;
214        if let Some(conn) = connections.get_mut(&peer_id) {
215            conn.bytes_sent = bytes_sent;
216            conn.bytes_received = bytes_received;
217            conn.rtt = rtt;
218            conn.last_activity = Instant::now();
219        }
220    }
221
222    /// Record historical data point
223    async fn record_data_point(&self) {
224        let _node_stats = self.node_stats.read().await;
225        let nat_stats = self.nat_stats.read().await;
226        let connections = self.connections.read().await;
227
228        let success_rate = if nat_stats.total_attempts > 0 {
229            nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64
230        } else {
231            0.0
232        };
233
234        let total_bytes: u64 = connections
235            .values()
236            .map(|c| c.bytes_sent + c.bytes_received)
237            .sum();
238
239        let avg_rtt = if connections.is_empty() {
240            Duration::from_millis(0)
241        } else {
242            let total_rtt: Duration = connections.values().filter_map(|c| c.rtt).sum();
243            let count = connections.values().filter(|c| c.rtt.is_some()).count();
244            if count > 0 {
245                total_rtt / count as u32
246            } else {
247                Duration::from_millis(0)
248            }
249        };
250
251        let data_point = DataPoint {
252            timestamp: Instant::now(),
253            active_connections: connections.len(),
254            nat_success_rate: success_rate,
255            bytes_per_second: total_bytes,
256            avg_rtt,
257        };
258
259        let mut history = self.history.write().await;
260        if history.len() >= self.config.history_size {
261            history.pop_front();
262        }
263        history.push_back(data_point);
264    }
265
266    /// Render the dashboard
267    pub async fn render(&self) -> String {
268        // Record current data point
269        self.record_data_point().await;
270
271        let mut output = String::new();
272
273        // Clear screen and move to top
274        output.push_str("\x1B[2J\x1B[H");
275
276        // Title
277        output.push_str(&format!(
278            "{}🚀 ant-quic Connection Statistics Dashboard\n\n{}",
279            terminal_ui::colors::BOLD,
280            terminal_ui::colors::RESET
281        ));
282
283        // System uptime
284        let uptime = self.start_time.elapsed();
285        output.push_str(&format!("⏱️  Uptime: {}\n\n", format_duration(uptime)));
286
287        // Render sections
288        output.push_str(&self.render_overview_section().await);
289        output.push_str(&self.render_nat_section().await);
290        output.push_str(&self.render_connections_section().await);
291
292        if self.config.show_graphs {
293            output.push_str(&self.render_graphs_section().await);
294        }
295
296        output.push_str(&self.render_footer().await);
297
298        output
299    }
300
301    /// Render overview section
302    async fn render_overview_section(&self) -> String {
303        let node_stats = self.node_stats.read().await;
304        let _connections = self.connections.read().await;
305
306        let mut section = String::new();
307
308        section.push_str(&draw_box(
309            "📊 Overview",
310            &format!(
311                "Active Connections: {}\n\
312                 Total Successful: {}\n\
313                 Total Failed: {}\n\
314                 Success Rate: {:.1}%",
315                format!(
316                    "{}{}{}",
317                    terminal_ui::colors::GREEN,
318                    node_stats.active_connections,
319                    terminal_ui::colors::RESET
320                ),
321                node_stats.successful_connections,
322                node_stats.failed_connections,
323                if node_stats.successful_connections + node_stats.failed_connections > 0 {
324                    (node_stats.successful_connections as f64
325                        / (node_stats.successful_connections + node_stats.failed_connections)
326                            as f64)
327                        * 100.0
328                } else {
329                    0.0
330                }
331            ),
332            BoxStyle::Single,
333            50,
334        ));
335
336        section.push('\n');
337        section
338    }
339
340    /// Render NAT traversal section
341    async fn render_nat_section(&self) -> String {
342        let nat_stats = self.nat_stats.read().await;
343
344        let mut section = String::new();
345
346        section.push_str(&draw_box(
347            "🌐 NAT Traversal",
348            &format!(
349                "Total Attempts: {}\n\
350                 Successful: {} ({:.1}%)\n\
351                 Direct Connections: {}\n\
352                 Relayed: {}\n\
353                 Average Time: {:?}\n\
354                 Active Sessions: {}",
355                nat_stats.total_attempts,
356                nat_stats.successful_connections,
357                if nat_stats.total_attempts > 0 {
358                    (nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64)
359                        * 100.0
360                } else {
361                    0.0
362                },
363                nat_stats.direct_connections,
364                nat_stats.relayed_connections,
365                nat_stats.average_coordination_time,
366                nat_stats.active_sessions,
367            ),
368            BoxStyle::Single,
369            50,
370        ));
371
372        section.push('\n');
373        section
374    }
375
376    /// Render connections section
377    async fn render_connections_section(&self) -> String {
378        let connections = self.connections.read().await;
379
380        let mut section = String::new();
381
382        if connections.is_empty() {
383            section.push_str(&draw_box(
384                "🔗 Active Connections",
385                "No active connections",
386                BoxStyle::Single,
387                50,
388            ));
389        } else {
390            let mut content = String::new();
391            for (i, (peer_id, conn)) in connections.iter().enumerate() {
392                if i > 0 {
393                    content.push_str("\n─────────────────────────────────────────────\n");
394                }
395
396                content.push_str(&format!(
397                    "Peer: {}\n\
398                     Address: {}\n\
399                     Duration: {}\n\
400                     Sent: {} | Received: {}\n\
401                     RTT: {} | Loss: {:.1}%",
402                    format!(
403                        "{}{}{}",
404                        terminal_ui::colors::DIM,
405                        hex::encode(&peer_id.0[..8]),
406                        terminal_ui::colors::RESET
407                    ),
408                    conn.remote_address,
409                    format_duration(conn.connected_at.elapsed()),
410                    format_bytes(conn.bytes_sent),
411                    format_bytes(conn.bytes_received),
412                    conn.rtt
413                        .map(|d| format!("{d:?}"))
414                        .unwrap_or_else(|| "N/A".to_string()),
415                    conn.packet_loss * 100.0,
416                ));
417            }
418
419            section.push_str(&draw_box(
420                &format!("🔗 Active Connections ({})", connections.len()),
421                &content,
422                BoxStyle::Single,
423                50,
424            ));
425        }
426
427        section.push('\n');
428        section
429    }
430
431    /// Render graphs section
432    async fn render_graphs_section(&self) -> String {
433        let history = self.history.read().await;
434
435        if history.len() < 2 {
436            return String::new();
437        }
438
439        let mut section = String::new();
440
441        // Connection count graph
442        let conn_data: Vec<usize> = history.iter().map(|d| d.active_connections).collect();
443
444        section.push_str(&draw_box(
445            "📈 Connection History",
446            &render_mini_graph(&conn_data, 20, 50),
447            BoxStyle::Single,
448            50,
449        ));
450        section.push('\n');
451
452        // Success rate graph
453        let success_data: Vec<f64> = history.iter().map(|d| d.nat_success_rate * 100.0).collect();
454
455        section.push_str(&draw_box(
456            "📈 NAT Success Rate %",
457            &render_mini_graph_float(&success_data, 20, 50),
458            BoxStyle::Single,
459            50,
460        ));
461        section.push('\n');
462
463        section
464    }
465
466    /// Render footer
467    async fn render_footer(&self) -> String {
468        let last_update = *self.last_update.read().await;
469
470        format!(
471            "\n{}\n{}",
472            format!(
473                "{}Last updated: {:?} ago{}",
474                terminal_ui::colors::DIM,
475                last_update.elapsed(),
476                terminal_ui::colors::RESET
477            ),
478            format!(
479                "{}Press Ctrl+C to exit{}",
480                terminal_ui::colors::DIM,
481                terminal_ui::colors::RESET
482            ),
483        )
484    }
485}
486
487/// Format duration in human-readable format
488fn format_duration(duration: Duration) -> String {
489    let secs = duration.as_secs();
490    if secs < 60 {
491        format!("{secs}s")
492    } else if secs < 3600 {
493        format!("{}m {}s", secs / 60, secs % 60)
494    } else {
495        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
496    }
497}
498
499/// Format bytes in human-readable format
500fn format_bytes(bytes: u64) -> String {
501    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
502    let mut size = bytes as f64;
503    let mut unit_index = 0;
504
505    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
506        size /= 1024.0;
507        unit_index += 1;
508    }
509
510    format!("{:.2} {}", size, UNITS[unit_index])
511}
512
513/// Render a simple ASCII graph
514fn render_mini_graph(data: &[usize], height: usize, width: usize) -> String {
515    if data.is_empty() {
516        return "No data".to_string();
517    }
518
519    let max_val = *data.iter().max().unwrap_or(&1).max(&1) as f64;
520    let step = data.len().max(1) / width.min(data.len()).max(1);
521
522    let mut graph = vec![vec![' '; width]; height];
523
524    for (i, chunk) in data.chunks(step).enumerate() {
525        if i >= width {
526            break;
527        }
528
529        let avg = chunk.iter().sum::<usize>() as f64 / chunk.len() as f64;
530        let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
531
532        for y in 0..=normalized {
533            let row = height - 1 - y;
534            graph[row][i] = '█';
535        }
536    }
537
538    let mut output = String::new();
539    for row in graph {
540        output.push_str(&row.iter().collect::<String>());
541        output.push('\n');
542    }
543
544    output.push_str(&format!(
545        "Max: {} | Latest: {}",
546        data.iter().max().unwrap_or(&0),
547        data.last().unwrap_or(&0)
548    ));
549
550    output
551}
552
553/// Render a simple ASCII graph for float values
554fn render_mini_graph_float(data: &[f64], height: usize, width: usize) -> String {
555    if data.is_empty() {
556        return "No data".to_string();
557    }
558
559    let max_val = data
560        .iter()
561        .cloned()
562        .fold(f64::NEG_INFINITY, f64::max)
563        .max(1.0);
564    let step = data.len().max(1) / width.min(data.len()).max(1);
565
566    let mut graph = vec![vec![' '; width]; height];
567
568    for (i, chunk) in data.chunks(step).enumerate() {
569        if i >= width {
570            break;
571        }
572
573        let avg = chunk.iter().sum::<f64>() / chunk.len() as f64;
574        let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
575
576        for y in 0..=normalized {
577            let row = height - 1 - y;
578            graph[row][i] = '█';
579        }
580    }
581
582    let mut output = String::new();
583    for row in graph {
584        output.push_str(&row.iter().collect::<String>());
585        output.push('\n');
586    }
587
588    output.push_str(&format!(
589        "Max: {:.1}% | Latest: {:.1}%",
590        data.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
591        data.last().unwrap_or(&0.0)
592    ));
593
594    output
595}