/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 | | } |