Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/tui.rs
Line
Count
Source
1
//! TUI Monitoring for LLM Inference
2
//!
3
//! Real-time terminal UI for monitoring inference performance.
4
//! Provides visual feedback on throughput, latency, and GPU utilization.
5
//!
6
//! # Usage
7
//!
8
//! ```rust,ignore
9
//! use realizar::tui::{InferenceTui, TuiConfig, InferenceMetrics};
10
//!
11
//! let config = TuiConfig::default();
12
//! let mut tui = InferenceTui::new(config);
13
//!
14
//! // Update with metrics during inference
15
//! tui.update(&metrics);
16
//!
17
//! // Render to string (for testing)
18
//! let output = tui.render_to_string();
19
//! ```
20
//!
21
//! # Visual Elements
22
//!
23
//! ```text
24
//! ╭─────────────────────────────────────────────────────────────╮
25
//! │             realizar Inference Monitor                       │
26
//! ├─────────────────────────────────────────────────────────────┤
27
//! │  Throughput: 64.2 tok/s   Target: 192 tok/s (M4)           │
28
//! │  Latency:    15.6 ms/tok  P95: 23.4 ms                     │
29
//! │  GPU Memory: 4.2 GB / 24 GB                                │
30
//! │  Batch Size: 4            Queue: 12 pending                │
31
//! ├─────────────────────────────────────────────────────────────┤
32
//! │  Throughput: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█                       │
33
//! │  Latency:    ▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁                         │
34
//! ├─────────────────────────────────────────────────────────────┤
35
//! │  Status: ● Running   Tokens: 1,234   Requests: 42          │
36
//! ╰─────────────────────────────────────────────────────────────╯
37
//! ```
38
39
use std::collections::VecDeque;
40
41
/// TUI configuration
42
#[derive(Debug, Clone)]
43
pub struct TuiConfig {
44
    /// Refresh rate in milliseconds
45
    pub refresh_rate_ms: u64,
46
    /// Show throughput sparkline
47
    pub show_throughput_sparkline: bool,
48
    /// Show latency sparkline
49
    pub show_latency_sparkline: bool,
50
    /// Show GPU memory usage
51
    pub show_gpu_memory: bool,
52
    /// Title for the TUI window
53
    pub title: String,
54
    /// Target throughput for M4 parity
55
    pub m4_target_tok_per_sec: f64,
56
    /// Width of the TUI display
57
    pub width: usize,
58
}
59
60
impl Default for TuiConfig {
61
5
    fn default() -> Self {
62
5
        Self {
63
5
            refresh_rate_ms: 100,
64
5
            show_throughput_sparkline: true,
65
5
            show_latency_sparkline: true,
66
5
            show_gpu_memory: true,
67
5
            title: "realizar Inference Monitor".to_string(),
68
5
            m4_target_tok_per_sec: 192.0,
69
5
            width: 65,
70
5
        }
71
5
    }
72
}
73
74
/// Real-time inference metrics
75
#[derive(Debug, Clone, Default)]
76
pub struct InferenceMetrics {
77
    /// Current throughput (tokens/second)
78
    pub throughput_tok_per_sec: f64,
79
    /// Mean latency per token (milliseconds)
80
    pub latency_ms: f64,
81
    /// P95 latency (milliseconds)
82
    pub latency_p95_ms: f64,
83
    /// GPU memory used (bytes)
84
    pub gpu_memory_bytes: u64,
85
    /// GPU memory total (bytes)
86
    pub gpu_memory_total_bytes: u64,
87
    /// Current batch size
88
    pub batch_size: usize,
89
    /// Pending requests in queue
90
    pub queue_size: usize,
91
    /// Total tokens generated
92
    pub total_tokens: u64,
93
    /// Total requests processed
94
    pub total_requests: u64,
95
    /// Is currently running
96
    pub running: bool,
97
    /// Is using GPU
98
    pub using_gpu: bool,
99
}
100
101
impl InferenceMetrics {
102
    /// Create new metrics with defaults
103
    #[must_use]
104
0
    pub fn new() -> Self {
105
0
        Self::default()
106
0
    }
107
108
    /// Check if throughput achieves M4 parity (192 tok/s)
109
    #[must_use]
110
8
    pub fn achieves_m4_parity(&self) -> bool {
111
8
        self.throughput_tok_per_sec >= 192.0
112
8
    }
113
114
    /// Calculate gap to M4 target
115
    #[must_use]
116
2
    pub fn gap_to_m4(&self) -> f64 {
117
2
        if self.throughput_tok_per_sec > 0.0 {
118
2
            192.0 / self.throughput_tok_per_sec
119
        } else {
120
0
            f64::INFINITY
121
        }
122
2
    }
123
124
    /// Format GPU memory as human-readable string
125
    #[must_use]
126
4
    pub fn format_gpu_memory(&self) -> String {
127
4
        let used_gb = self.gpu_memory_bytes as f64 / 1e9;
128
4
        let total_gb = self.gpu_memory_total_bytes as f64 / 1e9;
129
4
        format!("{:.1} GB / {:.1} GB", used_gb, total_gb)
130
4
    }
131
}
132
133
/// TUI state for rendering
134
#[derive(Debug, Clone)]
135
pub struct InferenceTui {
136
    /// Configuration
137
    config: TuiConfig,
138
    /// Current metrics
139
    metrics: InferenceMetrics,
140
    /// Throughput history (for sparkline)
141
    throughput_history: VecDeque<f64>,
142
    /// Latency history (for sparkline)
143
    latency_history: VecDeque<f64>,
144
    /// Maximum history size
145
    max_history: usize,
146
}
147
148
impl InferenceTui {
149
    /// Create new TUI with configuration
150
    #[must_use]
151
4
    pub fn new(config: TuiConfig) -> Self {
152
4
        Self {
153
4
            config,
154
4
            metrics: InferenceMetrics::default(),
155
4
            throughput_history: VecDeque::new(),
156
4
            latency_history: VecDeque::new(),
157
4
            max_history: 40,
158
4
        }
159
4
    }
160
161
    /// Update TUI with new metrics
162
62
    pub fn update(&mut self, metrics: &InferenceMetrics) {
163
62
        self.metrics = metrics.clone();
164
165
        // Add to history
166
62
        self.throughput_history
167
62
            .push_back(metrics.throughput_tok_per_sec);
168
62
        self.latency_history.push_back(metrics.latency_ms);
169
170
        // Trim history
171
72
        while self.throughput_history.len() > self.max_history {
172
10
            self.throughput_history.pop_front();
173
10
        }
174
72
        while self.latency_history.len() > self.max_history {
175
10
            self.latency_history.pop_front();
176
10
        }
177
62
    }
178
179
    /// Generate sparkline string from values
180
6
    fn sparkline(values: &VecDeque<f64>, width: usize) -> String {
181
        const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
182
183
6
        if values.is_empty() {
184
1
            return " ".repeat(width);
185
5
        }
186
187
5
        let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
188
5
        let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
189
5
        let range = (max - min).max(0.001);
190
191
5
        let mut result: String = values
192
5
            .iter()
193
5
            .take(width)
194
42
            .
map5
(|&v| {
195
42
                let normalized = (v - min) / range;
196
42
                let level = (normalized * 7.0).round().clamp(0.0, 7.0) as usize;
197
42
                BLOCKS[level]
198
42
            })
199
5
            .collect();
200
201
        // Pad to width
202
143
        while result.chars().count() < width {
203
138
            result.push(' ');
204
138
        }
205
206
5
        result
207
6
    }
208
209
    /// Render TUI to string (for testing and display)
210
    #[must_use]
211
2
    pub fn render_to_string(&self) -> String {
212
2
        let w = self.config.width;
213
2
        let inner_w = w - 2; // Account for │ borders on each side
214
215
2
        let mut lines = Vec::new();
216
217
        // Top border
218
2
        lines.push(format!("╭{}╮", "─".repeat(w - 2)));
219
220
        // Title
221
2
        let title = &self.config.title;
222
2
        let padding = (inner_w - title.len()) / 2;
223
2
        lines.push(format!(
224
2
            "│{}{}{}│",
225
2
            " ".repeat(padding),
226
            title,
227
2
            " ".repeat(inner_w - padding - title.len())
228
        ));
229
230
        // Separator
231
2
        lines.push(format!("├{}┤", "─".repeat(w - 2)));
232
233
        // Throughput line
234
2
        let status_icon = if self.metrics.achieves_m4_parity() {
235
0
            "✓"
236
        } else {
237
2
            "○"
238
        };
239
2
        let throughput_line = format!(
240
2
            "  Throughput: {:.1} tok/s {} Target: {:.0} tok/s (M4)",
241
            self.metrics.throughput_tok_per_sec, status_icon, self.config.m4_target_tok_per_sec
242
        );
243
2
        lines.push(Self::pad_line(&throughput_line, inner_w));
244
245
        // Latency line
246
2
        let latency_line = format!(
247
2
            "  Latency:    {:.1} ms/tok  P95: {:.1} ms",
248
            self.metrics.latency_ms, self.metrics.latency_p95_ms
249
        );
250
2
        lines.push(Self::pad_line(&latency_line, inner_w));
251
252
        // GPU memory line
253
2
        if self.config.show_gpu_memory {
254
2
            let gpu_line = format!("  GPU Memory: {}", self.metrics.format_gpu_memory());
255
2
            lines.push(Self::pad_line(&gpu_line, inner_w));
256
2
        
}0
257
258
        // Batch info line
259
2
        let batch_line = format!(
260
2
            "  Batch Size: {}            Queue: {} pending",
261
            self.metrics.batch_size, self.metrics.queue_size
262
        );
263
2
        lines.push(Self::pad_line(&batch_line, inner_w));
264
265
        // Separator
266
2
        lines.push(format!("├{}┤", "─".repeat(w - 2)));
267
268
        // Sparklines
269
2
        if self.config.show_throughput_sparkline {
270
2
            let sparkline = Self::sparkline(&self.throughput_history, 40);
271
2
            let spark_line = format!("  Throughput: {}", sparkline);
272
2
            lines.push(Self::pad_line(&spark_line, inner_w));
273
2
        
}0
274
275
2
        if self.config.show_latency_sparkline {
276
2
            let sparkline = Self::sparkline(&self.latency_history, 40);
277
2
            let spark_line = format!("  Latency:    {}", sparkline);
278
2
            lines.push(Self::pad_line(&spark_line, inner_w));
279
2
        
}0
280
281
        // Separator
282
2
        lines.push(format!("├{}┤", "─".repeat(w - 2)));
283
284
        // Status line
285
2
        let status = if self.metrics.running {
286
2
            "● Running"
287
        } else {
288
0
            "○ Stopped"
289
        };
290
2
        let gpu_status = if self.metrics.using_gpu { "GPU" } else { 
"CPU"0
};
291
2
        let status_line = format!(
292
2
            "  Status: {}  [{:>3}]  Tokens: {:>6}  Requests: {:>4}",
293
            status, gpu_status, self.metrics.total_tokens, self.metrics.total_requests
294
        );
295
2
        lines.push(Self::pad_line(&status_line, inner_w));
296
297
        // Bottom border
298
2
        lines.push(format!("╰{}╯", "─".repeat(w - 2)));
299
300
2
        lines.join("\n")
301
2
    }
302
303
    /// Pad line to fit within borders
304
14
    fn pad_line(content: &str, width: usize) -> String {
305
14
        let content_len = content.chars().count();
306
14
        if content_len >= width {
307
0
            format!("│{}│", &content[..width])
308
        } else {
309
14
            format!("│{}{}│", content, " ".repeat(width - content_len))
310
        }
311
14
    }
312
313
    /// Get current metrics
314
    #[must_use]
315
1
    pub fn metrics(&self) -> &InferenceMetrics {
316
1
        &self.metrics
317
1
    }
318
319
    /// Get throughput history for testing
320
    #[must_use]
321
3
    pub fn throughput_history(&self) -> &VecDeque<f64> {
322
3
        &self.throughput_history
323
3
    }
324
325
    /// Get latency history for testing
326
    #[must_use]
327
1
    pub fn latency_history(&self) -> &VecDeque<f64> {
328
1
        &self.latency_history
329
1
    }
330
}
331
332
#[cfg(test)]
333
mod tests {
334
    use super::*;
335
336
    // =========================================================================
337
    // PARITY-090: TUI Configuration Tests
338
    // =========================================================================
339
340
    /// PARITY-090a: Test TuiConfig defaults
341
    #[test]
342
1
    fn test_parity_090a_tui_config_defaults() {
343
1
        println!("PARITY-090a: TuiConfig Default Values");
344
345
1
        let config = TuiConfig::default();
346
347
1
        println!("  refresh_rate_ms: {}", config.refresh_rate_ms);
348
1
        println!("  m4_target_tok_per_sec: {}", config.m4_target_tok_per_sec);
349
1
        println!("  width: {}", config.width);
350
351
1
        assert_eq!(config.refresh_rate_ms, 100);
352
1
        assert_eq!(config.m4_target_tok_per_sec, 192.0);
353
1
        assert!(config.show_throughput_sparkline);
354
1
        assert!(config.show_latency_sparkline);
355
1
    }
356
357
    /// PARITY-090b: Test InferenceMetrics creation
358
    #[test]
359
1
    fn test_parity_090b_inference_metrics() {
360
1
        println!("PARITY-090b: InferenceMetrics");
361
362
1
        let metrics = InferenceMetrics {
363
1
            throughput_tok_per_sec: 64.0,
364
1
            latency_ms: 15.6,
365
1
            latency_p95_ms: 23.4,
366
1
            gpu_memory_bytes: 4_200_000_000,
367
1
            gpu_memory_total_bytes: 24_000_000_000,
368
1
            batch_size: 4,
369
1
            queue_size: 12,
370
1
            total_tokens: 1234,
371
1
            total_requests: 42,
372
1
            running: true,
373
1
            using_gpu: true,
374
1
        };
375
376
1
        println!("  throughput: {:.1} tok/s", metrics.throughput_tok_per_sec);
377
1
        println!("  achieves_m4: {}", metrics.achieves_m4_parity());
378
1
        println!("  gap_to_m4: {:.2}x", metrics.gap_to_m4());
379
1
        println!("  gpu_memory: {}", metrics.format_gpu_memory());
380
381
1
        assert!(!metrics.achieves_m4_parity());
382
1
        assert!((metrics.gap_to_m4() - 3.0).abs() < 0.1);
383
1
        assert!(metrics.format_gpu_memory().contains("4.2 GB"));
384
1
    }
385
386
    /// PARITY-090c: Test M4 parity detection
387
    #[test]
388
1
    fn test_parity_090c_m4_parity_detection() {
389
1
        println!("PARITY-090c: M4 Parity Detection");
390
391
1
        let test_cases = [
392
1
            (64.0, false, "Baseline - not M4"),
393
1
            (150.0, false, "Batch threshold - not M4"),
394
1
            (192.0, true, "Exactly M4"),
395
1
            (256.0, true, "Above M4"),
396
1
        ];
397
398
5
        for (
throughput4
,
expected4
,
description4
) in test_cases {
399
4
            let metrics = InferenceMetrics {
400
4
                throughput_tok_per_sec: throughput,
401
4
                ..Default::default()
402
4
            };
403
4
            let achieves = metrics.achieves_m4_parity();
404
4
            println!("  {}: {} tok/s → M4={}", description, throughput, achieves);
405
4
            assert_eq!(achieves, expected, 
"{}"0
, description);
406
        }
407
1
    }
408
409
    // =========================================================================
410
    // PARITY-091: TUI Rendering Tests
411
    // =========================================================================
412
413
    /// PARITY-091a: Test TUI creation and update
414
    #[test]
415
1
    fn test_parity_091a_tui_creation_update() {
416
1
        println!("PARITY-091a: TUI Creation and Update");
417
418
1
        let config = TuiConfig::default();
419
1
        let mut tui = InferenceTui::new(config);
420
421
1
        let metrics = InferenceMetrics {
422
1
            throughput_tok_per_sec: 64.0,
423
1
            latency_ms: 15.6,
424
1
            running: true,
425
1
            ..Default::default()
426
1
        };
427
428
1
        tui.update(&metrics);
429
430
1
        assert_eq!(tui.metrics().throughput_tok_per_sec, 64.0);
431
1
        assert_eq!(tui.throughput_history().len(), 1);
432
1
    }
433
434
    /// PARITY-091b: Test sparkline generation
435
    #[test]
436
1
    fn test_parity_091b_sparkline_generation() {
437
1
        println!("PARITY-091b: Sparkline Generation");
438
439
1
        let mut history = VecDeque::new();
440
21
        for 
i20
in 0..20 {
441
20
            history.push_back((i as f64) * 10.0);
442
20
        }
443
444
1
        let sparkline = InferenceTui::sparkline(&history, 20);
445
1
        println!("  Sparkline: {}", sparkline);
446
447
1
        assert_eq!(sparkline.chars().count(), 20);
448
1
        assert!(sparkline.contains('▁')); // Low values
449
1
        assert!(sparkline.contains('█')); // High values
450
1
    }
451
452
    /// PARITY-091c: Test TUI render output structure
453
    #[test]
454
1
    fn test_parity_091c_tui_render_structure() {
455
1
        println!("PARITY-091c: TUI Render Output Structure");
456
457
1
        let config = TuiConfig::default();
458
1
        let mut tui = InferenceTui::new(config);
459
460
        // Add some history
461
11
        for 
i10
in 0..10 {
462
10
            let metrics = InferenceMetrics {
463
10
                throughput_tok_per_sec: 50.0 + (i as f64) * 5.0,
464
10
                latency_ms: 20.0 - (i as f64),
465
10
                batch_size: 4,
466
10
                queue_size: 12,
467
10
                total_tokens: 1234,
468
10
                total_requests: 42,
469
10
                running: true,
470
10
                using_gpu: true,
471
10
                ..Default::default()
472
10
            };
473
10
            tui.update(&metrics);
474
10
        }
475
476
1
        let output = tui.render_to_string();
477
1
        println!("{}", output);
478
479
        // Verify structure
480
1
        assert!(output.contains("╭"), 
"Should have top border"0
);
481
1
        assert!(output.contains("╰"), 
"Should have bottom border"0
);
482
1
        assert!(
483
1
            output.contains("realizar Inference Monitor"),
484
0
            "Should have title"
485
        );
486
1
        assert!(output.contains("Throughput:"), 
"Should show throughput"0
);
487
1
        assert!(output.contains("Latency:"), 
"Should show latency"0
);
488
1
        assert!(output.contains("tok/s"), 
"Should show tok/s unit"0
);
489
1
        assert!(output.contains("● Running"), 
"Should show running status"0
);
490
1
    }
491
492
    /// PARITY-091d: Test TUI visual regression baseline
493
    #[test]
494
1
    fn test_parity_091d_visual_regression_baseline() {
495
1
        println!("PARITY-091d: Visual Regression Baseline");
496
497
1
        let config = TuiConfig {
498
1
            width: 65,
499
1
            ..Default::default()
500
1
        };
501
1
        let mut tui = InferenceTui::new(config);
502
503
1
        let metrics = InferenceMetrics {
504
1
            throughput_tok_per_sec: 64.2,
505
1
            latency_ms: 15.6,
506
1
            latency_p95_ms: 23.4,
507
1
            gpu_memory_bytes: 4_200_000_000,
508
1
            gpu_memory_total_bytes: 24_000_000_000,
509
1
            batch_size: 4,
510
1
            queue_size: 12,
511
1
            total_tokens: 1234,
512
1
            total_requests: 42,
513
1
            running: true,
514
1
            using_gpu: true,
515
1
        };
516
517
1
        tui.update(&metrics);
518
1
        let output = tui.render_to_string();
519
520
1
        println!("=== GOLDEN BASELINE ===");
521
1
        println!("{}", output);
522
1
        println!("=== END BASELINE ===");
523
524
        // Verify key visual elements
525
1
        let lines: Vec<&str> = output.lines().collect();
526
1
        assert!(lines.len() >= 10, 
"Should have at least 10 lines"0
);
527
528
        // Check border consistency
529
1
        assert!(lines[0].starts_with('╭'));
530
1
        assert!(lines[0].ends_with('╮'));
531
1
        assert!(lines.last().expect("test").starts_with('╰'));
532
1
        assert!(lines.last().expect("test").ends_with('╯'));
533
534
        // Check content
535
1
        assert!(
536
1
            output.contains("64.2 tok/s"),
537
0
            "Should show throughput value"
538
        );
539
1
        assert!(output.contains("15.6 ms/tok"), 
"Should show latency value"0
);
540
1
        assert!(output.contains("1234"), 
"Should show token count"0
);
541
1
    }
542
543
    /// PARITY-091e: Test history accumulation
544
    #[test]
545
1
    fn test_parity_091e_history_accumulation() {
546
1
        println!("PARITY-091e: History Accumulation");
547
548
1
        let config = TuiConfig::default();
549
1
        let mut tui = InferenceTui::new(config);
550
551
        // Add more than max_history items
552
51
        for 
i50
in 0..50 {
553
50
            let metrics = InferenceMetrics {
554
50
                throughput_tok_per_sec: (i as f64) * 2.0,
555
50
                latency_ms: 100.0 - (i as f64),
556
50
                ..Default::default()
557
50
            };
558
50
            tui.update(&metrics);
559
50
        }
560
561
        // Should be capped at max_history (40)
562
1
        assert_eq!(tui.throughput_history().len(), 40);
563
1
        assert_eq!(tui.latency_history().len(), 40);
564
565
        // Most recent should be last
566
1
        assert!((tui.throughput_history().back().expect("test") - 98.0).abs() < 0.1);
567
1
    }
568
569
    /// PARITY-091f: Test empty sparkline handling
570
    #[test]
571
1
    fn test_parity_091f_empty_sparkline() {
572
1
        println!("PARITY-091f: Empty Sparkline Handling");
573
574
1
        let empty: VecDeque<f64> = VecDeque::new();
575
1
        let sparkline = InferenceTui::sparkline(&empty, 20);
576
577
1
        println!("  Empty sparkline: '{}'", sparkline);
578
1
        assert_eq!(sparkline.len(), 20);
579
20
        
assert!1
(
sparkline.chars()1
.
all1
(|c| c == ' '));
580
1
    }
581
}