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::{NatTraversalEvent, NatTraversalStatistics, PeerId},
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 {
155                peer_id,
156                remote_address,
157            } => {
158                let mut connections = self.connections.write().await;
159                connections.insert(
160                    *peer_id,
161                    ConnectionInfo {
162                        peer_id: *peer_id,
163                        remote_address: *remote_address,
164                        connected_at: Instant::now(),
165                        bytes_sent: 0,
166                        bytes_received: 0,
167                        last_activity: Instant::now(),
168                        rtt: None,
169                        packet_loss: 0.0,
170                        nat_type: "Unknown".to_string(),
171                    },
172                );
173            }
174            NatTraversalEvent::TraversalFailed { peer_id, .. } => {
175                let mut connections = self.connections.write().await;
176                connections.remove(peer_id);
177            }
178            _ => {}
179        }
180    }
181
182    /// Update connection metrics
183    pub async fn update_connection_metrics(
184        &self,
185        peer_id: PeerId,
186        bytes_sent: u64,
187        bytes_received: u64,
188        rtt: Option<Duration>,
189    ) {
190        let mut connections = self.connections.write().await;
191        if let Some(conn) = connections.get_mut(&peer_id) {
192            conn.bytes_sent = bytes_sent;
193            conn.bytes_received = bytes_received;
194            conn.rtt = rtt;
195            conn.last_activity = Instant::now();
196        }
197    }
198
199    /// Record historical data point
200    async fn record_data_point(&self) {
201        let _node_stats = self.node_stats.read().await;
202        let nat_stats = self.nat_stats.read().await;
203        let connections = self.connections.read().await;
204
205        let success_rate = if nat_stats.total_attempts > 0 {
206            nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64
207        } else {
208            0.0
209        };
210
211        let total_bytes: u64 = connections
212            .values()
213            .map(|c| c.bytes_sent + c.bytes_received)
214            .sum();
215
216        let avg_rtt = if connections.is_empty() {
217            Duration::from_millis(0)
218        } else {
219            let total_rtt: Duration = connections.values().filter_map(|c| c.rtt).sum();
220            let count = connections.values().filter(|c| c.rtt.is_some()).count();
221            if count > 0 {
222                total_rtt / count as u32
223            } else {
224                Duration::from_millis(0)
225            }
226        };
227
228        let data_point = DataPoint {
229            timestamp: Instant::now(),
230            active_connections: connections.len(),
231            nat_success_rate: success_rate,
232            bytes_per_second: total_bytes,
233            avg_rtt,
234        };
235
236        let mut history = self.history.write().await;
237        if history.len() >= self.config.history_size {
238            history.pop_front();
239        }
240        history.push_back(data_point);
241    }
242
243    /// Render the dashboard
244    pub async fn render(&self) -> String {
245        // Record current data point
246        self.record_data_point().await;
247
248        let mut output = String::new();
249
250        // Clear screen and move to top
251        output.push_str("\x1B[2J\x1B[H");
252
253        // Title
254        output.push_str(&format!(
255            "{}🚀 ant-quic Connection Statistics Dashboard\n\n{}",
256            terminal_ui::colors::BOLD,
257            terminal_ui::colors::RESET
258        ));
259
260        // System uptime
261        let uptime = self.start_time.elapsed();
262        output.push_str(&format!("⏱️  Uptime: {}\n\n", format_duration(uptime)));
263
264        // Render sections
265        output.push_str(&self.render_overview_section().await);
266        output.push_str(&self.render_nat_section().await);
267        output.push_str(&self.render_connections_section().await);
268
269        if self.config.show_graphs {
270            output.push_str(&self.render_graphs_section().await);
271        }
272
273        output.push_str(&self.render_footer().await);
274
275        output
276    }
277
278    /// Render overview section
279    async fn render_overview_section(&self) -> String {
280        let node_stats = self.node_stats.read().await;
281        let _connections = self.connections.read().await;
282
283        let mut section = String::new();
284
285        section.push_str(&draw_box(
286            "📊 Overview",
287            &format!(
288                "Active Connections: {}\n\
289                 Total Successful: {}\n\
290                 Total Failed: {}\n\
291                 Success Rate: {:.1}%",
292                format!(
293                    "{}{}{}",
294                    terminal_ui::colors::GREEN,
295                    node_stats.active_connections,
296                    terminal_ui::colors::RESET
297                ),
298                node_stats.successful_connections,
299                node_stats.failed_connections,
300                if node_stats.successful_connections + node_stats.failed_connections > 0 {
301                    (node_stats.successful_connections as f64
302                        / (node_stats.successful_connections + node_stats.failed_connections)
303                            as f64)
304                        * 100.0
305                } else {
306                    0.0
307                }
308            ),
309            BoxStyle::Single,
310            50,
311        ));
312
313        section.push('\n');
314        section
315    }
316
317    /// Render NAT traversal section
318    async fn render_nat_section(&self) -> String {
319        let nat_stats = self.nat_stats.read().await;
320
321        let mut section = String::new();
322
323        section.push_str(&draw_box(
324            "🌐 NAT Traversal",
325            &format!(
326                "Total Attempts: {}\n\
327                 Successful: {} ({:.1}%)\n\
328                 Direct Connections: {}\n\
329                 Relayed: {}\n\
330                 Average Time: {:?}\n\
331                 Active Sessions: {}",
332                nat_stats.total_attempts,
333                nat_stats.successful_connections,
334                if nat_stats.total_attempts > 0 {
335                    (nat_stats.successful_connections as f64 / nat_stats.total_attempts as f64)
336                        * 100.0
337                } else {
338                    0.0
339                },
340                nat_stats.direct_connections,
341                nat_stats.relayed_connections,
342                nat_stats.average_coordination_time,
343                nat_stats.active_sessions,
344            ),
345            BoxStyle::Single,
346            50,
347        ));
348
349        section.push('\n');
350        section
351    }
352
353    /// Render connections section
354    async fn render_connections_section(&self) -> String {
355        let connections = self.connections.read().await;
356
357        let mut section = String::new();
358
359        if connections.is_empty() {
360            section.push_str(&draw_box(
361                "🔗 Active Connections",
362                "No active connections",
363                BoxStyle::Single,
364                50,
365            ));
366        } else {
367            let mut content = String::new();
368            for (i, (peer_id, conn)) in connections.iter().enumerate() {
369                if i > 0 {
370                    content.push_str("\n─────────────────────────────────────────────\n");
371                }
372
373                content.push_str(&format!(
374                    "Peer: {}\n\
375                     Address: {}\n\
376                     Duration: {}\n\
377                     Sent: {} | Received: {}\n\
378                     RTT: {} | Loss: {:.1}%",
379                    format!(
380                        "{}{}{}",
381                        terminal_ui::colors::DIM,
382                        hex::encode(&peer_id.0[..8]),
383                        terminal_ui::colors::RESET
384                    ),
385                    conn.remote_address,
386                    format_duration(conn.connected_at.elapsed()),
387                    format_bytes(conn.bytes_sent),
388                    format_bytes(conn.bytes_received),
389                    conn.rtt
390                        .map(|d| format!("{d:?}"))
391                        .unwrap_or_else(|| "N/A".to_string()),
392                    conn.packet_loss * 100.0,
393                ));
394            }
395
396            section.push_str(&draw_box(
397                &format!("🔗 Active Connections ({})", connections.len()),
398                &content,
399                BoxStyle::Single,
400                50,
401            ));
402        }
403
404        section.push('\n');
405        section
406    }
407
408    /// Render graphs section
409    async fn render_graphs_section(&self) -> String {
410        let history = self.history.read().await;
411
412        if history.len() < 2 {
413            return String::new();
414        }
415
416        let mut section = String::new();
417
418        // Connection count graph
419        let conn_data: Vec<usize> = history.iter().map(|d| d.active_connections).collect();
420
421        section.push_str(&draw_box(
422            "📈 Connection History",
423            &render_mini_graph(&conn_data, 20, 50),
424            BoxStyle::Single,
425            50,
426        ));
427        section.push('\n');
428
429        // Success rate graph
430        let success_data: Vec<f64> = history.iter().map(|d| d.nat_success_rate * 100.0).collect();
431
432        section.push_str(&draw_box(
433            "📈 NAT Success Rate %",
434            &render_mini_graph_float(&success_data, 20, 50),
435            BoxStyle::Single,
436            50,
437        ));
438        section.push('\n');
439
440        section
441    }
442
443    /// Render footer
444    async fn render_footer(&self) -> String {
445        let last_update = *self.last_update.read().await;
446
447        format!(
448            "\n{}\n{}",
449            format!(
450                "{}Last updated: {:?} ago{}",
451                terminal_ui::colors::DIM,
452                last_update.elapsed(),
453                terminal_ui::colors::RESET
454            ),
455            format!(
456                "{}Press Ctrl+C to exit{}",
457                terminal_ui::colors::DIM,
458                terminal_ui::colors::RESET
459            ),
460        )
461    }
462}
463
464/// Format duration in human-readable format
465fn format_duration(duration: Duration) -> String {
466    let secs = duration.as_secs();
467    if secs < 60 {
468        format!("{secs}s")
469    } else if secs < 3600 {
470        format!("{}m {}s", secs / 60, secs % 60)
471    } else {
472        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
473    }
474}
475
476/// Format bytes in human-readable format
477fn format_bytes(bytes: u64) -> String {
478    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
479    let mut size = bytes as f64;
480    let mut unit_index = 0;
481
482    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
483        size /= 1024.0;
484        unit_index += 1;
485    }
486
487    format!("{:.2} {}", size, UNITS[unit_index])
488}
489
490/// Render a simple ASCII graph
491fn render_mini_graph(data: &[usize], height: usize, width: usize) -> String {
492    if data.is_empty() {
493        return "No data".to_string();
494    }
495
496    let max_val = *data.iter().max().unwrap_or(&1).max(&1) as f64;
497    let step = data.len().max(1) / width.min(data.len()).max(1);
498
499    let mut graph = vec![vec![' '; width]; height];
500
501    for (i, chunk) in data.chunks(step).enumerate() {
502        if i >= width {
503            break;
504        }
505
506        let avg = chunk.iter().sum::<usize>() as f64 / chunk.len() as f64;
507        let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
508
509        for y in 0..=normalized {
510            let row = height - 1 - y;
511            graph[row][i] = '█';
512        }
513    }
514
515    let mut output = String::new();
516    for row in graph {
517        output.push_str(&row.iter().collect::<String>());
518        output.push('\n');
519    }
520
521    output.push_str(&format!(
522        "Max: {} | Latest: {}",
523        data.iter().max().unwrap_or(&0),
524        data.last().unwrap_or(&0)
525    ));
526
527    output
528}
529
530/// Render a simple ASCII graph for float values
531fn render_mini_graph_float(data: &[f64], height: usize, width: usize) -> String {
532    if data.is_empty() {
533        return "No data".to_string();
534    }
535
536    let max_val = data
537        .iter()
538        .cloned()
539        .fold(f64::NEG_INFINITY, f64::max)
540        .max(1.0);
541    let step = data.len().max(1) / width.min(data.len()).max(1);
542
543    let mut graph = vec![vec![' '; width]; height];
544
545    for (i, chunk) in data.chunks(step).enumerate() {
546        if i >= width {
547            break;
548        }
549
550        let avg = chunk.iter().sum::<f64>() / chunk.len() as f64;
551        let normalized = (avg / max_val * (height - 1) as f64).round() as usize;
552
553        for y in 0..=normalized {
554            let row = height - 1 - y;
555            graph[row][i] = '█';
556        }
557    }
558
559    let mut output = String::new();
560    for row in graph {
561        output.push_str(&row.iter().collect::<String>());
562        output.push('\n');
563    }
564
565    output.push_str(&format!(
566        "Max: {:.1}% | Latest: {:.1}%",
567        data.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
568        data.last().unwrap_or(&0.0)
569    ));
570
571    output
572}