Skip to main content

entrenar/monitor/gpu/
render.rs

1//! GPU metrics rendering utilities for terminal display.
2
3use super::GpuMetrics;
4
5/// Render a progress bar for terminal display
6pub fn render_progress_bar(value: f64, width: usize) -> String {
7    let filled = ((value / 100.0) * width as f64).round() as usize;
8    let filled = filled.min(width);
9    let empty = width - filled;
10
11    let bar: String = std::iter::repeat_n('\u{2588}', filled).collect();
12    let empty_bar: String = std::iter::repeat_n('\u{2591}', empty).collect();
13
14    format!("{bar}{empty_bar}")
15}
16
17/// Render a sparkline from values
18pub fn render_sparkline(values: &[u32], max_val: u32) -> String {
19    const CHARS: &[char] = &[
20        '\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
21        '\u{2588}',
22    ];
23
24    if values.is_empty() || max_val == 0 {
25        return String::new();
26    }
27
28    values
29        .iter()
30        .map(|&v| {
31            let idx =
32                ((f64::from(v) / f64::from(max_val)) * (CHARS.len() - 1) as f64).round() as usize;
33            CHARS[idx.min(CHARS.len() - 1)]
34        })
35        .collect()
36}
37
38/// Format GPU metrics for terminal display
39pub fn format_gpu_panel(metrics: &GpuMetrics, width: usize) -> Vec<String> {
40    let bar_width = width.saturating_sub(25);
41
42    vec![
43        format!(
44            "───── GPU {}: {} ─────",
45            metrics.device_id,
46            metrics.name.chars().take(width - 20).collect::<String>()
47        ),
48        format!(
49            "Util: {} {:>3}%  │  Temp: {}°C",
50            render_progress_bar(f64::from(metrics.utilization_percent), bar_width),
51            metrics.utilization_percent,
52            metrics.temperature_celsius
53        ),
54        format!(
55            "VRAM: {} {:.1}/{:.1} GB ({:.0}%)",
56            render_progress_bar(metrics.memory_percent(), bar_width),
57            metrics.memory_used_mb as f64 / 1024.0,
58            metrics.memory_total_mb as f64 / 1024.0,
59            metrics.memory_percent()
60        ),
61        format!(
62            "Pow:  {} {:.0}W/{:.0}W",
63            render_progress_bar(metrics.power_percent(), bar_width),
64            metrics.power_watts,
65            metrics.power_limit_watts
66        ),
67    ]
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_render_progress_bar() {
76        let bar = render_progress_bar(50.0, 10);
77        assert_eq!(bar.chars().filter(|&c| c == '\u{2588}').count(), 5);
78        assert_eq!(bar.chars().filter(|&c| c == '\u{2591}').count(), 5);
79    }
80
81    #[test]
82    fn test_render_progress_bar_full() {
83        let bar = render_progress_bar(100.0, 10);
84        assert_eq!(bar.chars().filter(|&c| c == '\u{2588}').count(), 10);
85    }
86
87    #[test]
88    fn test_render_progress_bar_empty() {
89        let bar = render_progress_bar(0.0, 10);
90        assert_eq!(bar.chars().filter(|&c| c == '\u{2591}').count(), 10);
91    }
92
93    #[test]
94    fn test_render_sparkline() {
95        let sparkline = render_sparkline(&[0, 50, 100], 100);
96        assert_eq!(sparkline.chars().count(), 3);
97        assert!(sparkline.starts_with('\u{2581}'));
98        assert!(sparkline.ends_with('\u{2588}'));
99    }
100
101    #[test]
102    fn test_render_sparkline_empty() {
103        let sparkline = render_sparkline(&[], 100);
104        assert!(sparkline.is_empty());
105    }
106
107    #[test]
108    fn test_format_gpu_panel() {
109        let metrics = GpuMetrics::mock(0);
110        let lines = format_gpu_panel(&metrics, 60);
111        assert!(!lines.is_empty());
112        assert!(lines[0].contains("GPU 0"));
113    }
114
115    #[test]
116    fn test_render_sparkline_max_val_zero() {
117        let sparkline = render_sparkline(&[1, 2, 3], 0);
118        assert!(sparkline.is_empty());
119    }
120
121    #[test]
122    fn test_render_progress_bar_over_100() {
123        // Should handle values > 100
124        let bar = render_progress_bar(150.0, 10);
125        assert_eq!(bar.chars().filter(|&c| c == '\u{2588}').count(), 10);
126    }
127
128    #[test]
129    fn test_render_progress_bar_negative() {
130        // Should handle negative values
131        let bar = render_progress_bar(-10.0, 10);
132        // Negative rounds to 0
133        assert!(bar.chars().filter(|&c| c == '\u{2588}').count() == 0);
134    }
135}
136
137#[cfg(test)]
138mod property_tests {
139    use super::*;
140    use proptest::prelude::*;
141
142    proptest! {
143        #![proptest_config(ProptestConfig::with_cases(200))]
144
145        #[test]
146        fn prop_sparkline_length(values in prop::collection::vec(0u32..100, 0..50)) {
147            let sparkline = render_sparkline(&values, 100);
148            prop_assert_eq!(sparkline.chars().count(), values.len());
149        }
150
151        #[test]
152        fn prop_progress_bar_length(value in 0.0f64..100.0, width in 1usize..50) {
153            let bar = render_progress_bar(value, width);
154            let char_count: usize = bar.chars().count();
155            prop_assert_eq!(char_count, width);
156        }
157    }
158}