ant_quic/
stats_dashboard.rs

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