Skip to main content

khora_infra/telemetry/
gpu_monitor.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! GPU performance monitoring.
16
17use std::borrow::Cow;
18use std::sync::Mutex;
19
20use khora_core::renderer::api::core::{GpuHook, RenderStats};
21use khora_core::telemetry::monitoring::{
22    GpuReport, MonitoredResourceType, ResourceMonitor, ResourceUsageReport,
23};
24
25/// GPU performance monitor that works with any RenderSystem implementation.
26#[derive(Debug)]
27pub struct GpuMonitor {
28    system_name: String,
29    last_frame_stats: Mutex<Option<GpuReport>>,
30}
31
32impl GpuMonitor {
33    /// Create a new GPU performance monitor
34    pub fn new(system_name: String) -> Self {
35        Self {
36            system_name,
37            last_frame_stats: Mutex::new(None),
38        }
39    }
40
41    /// Returns the latest detailed GPU performance report.
42    pub fn get_gpu_report(&self) -> Option<GpuReport> {
43        *self
44            .last_frame_stats
45            .lock()
46            .unwrap_or_else(|e| e.into_inner())
47    }
48
49    /// Update performance stats from frame timing data
50    pub fn update_from_frame_stats(&self, render_stats: &RenderStats) {
51        // Create hook timings based on render stats
52        // We simulate the timeline: FrameStart -> MainPassBegin -> MainPassEnd -> FrameEnd
53        let frame_start_us = 0u32; // Start of frame timeline
54        let frame_end_us = (render_stats.gpu_frame_total_time_ms * 1000.0) as u32;
55        let main_pass_duration_us = (render_stats.gpu_main_pass_time_ms * 1000.0) as u32;
56
57        // Place main pass in the middle of the frame for simplicity
58        let main_pass_begin_us = (frame_end_us - main_pass_duration_us) / 2;
59        let main_pass_end_us = main_pass_begin_us + main_pass_duration_us;
60
61        let mut hook_timings = [None; 4];
62        hook_timings[GpuHook::FrameStart as usize] = Some(frame_start_us);
63        hook_timings[GpuHook::MainPassBegin as usize] = Some(main_pass_begin_us);
64        hook_timings[GpuHook::MainPassEnd as usize] = Some(main_pass_end_us);
65        hook_timings[GpuHook::FrameEnd as usize] = Some(frame_end_us);
66
67        let report = GpuReport {
68            frame_number: render_stats.frame_number,
69            hook_timings_us: hook_timings,
70            // Convert milliseconds to microseconds
71            cpu_preparation_time_us: Some((render_stats.cpu_preparation_time_ms * 1000.0) as u32),
72            cpu_submission_time_us: Some(
73                (render_stats.cpu_render_submission_time_ms * 1000.0) as u32,
74            ),
75            draw_calls: render_stats.draw_calls,
76            triangles_rendered: render_stats.triangles_rendered,
77        };
78
79        let mut last_stats = self
80            .last_frame_stats
81            .lock()
82            .unwrap_or_else(|e| e.into_inner());
83        *last_stats = Some(report);
84    }
85}
86
87impl ResourceMonitor for GpuMonitor {
88    fn monitor_id(&self) -> Cow<'static, str> {
89        Cow::Owned(format!("Gpu_{}", self.system_name))
90    }
91
92    fn resource_type(&self) -> MonitoredResourceType {
93        MonitoredResourceType::Gpu
94    }
95
96    fn get_usage_report(&self) -> ResourceUsageReport {
97        // GPU performance doesn't have byte-based usage, so return default
98        ResourceUsageReport::default()
99    }
100
101    fn get_gpu_report(&self) -> Option<GpuReport> {
102        self.get_gpu_report()
103    }
104
105    fn get_metrics(
106        &self,
107    ) -> Vec<(
108        khora_core::telemetry::metrics::MetricId,
109        khora_core::telemetry::metrics::MetricValue,
110    )> {
111        use khora_core::telemetry::metrics::{MetricId, MetricValue};
112        let mut metrics = Vec::new();
113
114        if let Some(report) = self.get_gpu_report() {
115            metrics.push((
116                MetricId::new("renderer", "draw_calls"),
117                MetricValue::Gauge(report.draw_calls as f64),
118            ));
119            metrics.push((
120                MetricId::new("renderer", "triangles"),
121                MetricValue::Gauge(report.triangles_rendered as f64),
122            ));
123
124            if let Some(total_ms) = report.frame_total_duration_us() {
125                metrics.push((
126                    MetricId::new("renderer", "frame_time"),
127                    MetricValue::Gauge(total_ms as f64 / 1000.0),
128                ));
129            }
130
131            if let Some(main_ms) = report.main_pass_duration_us() {
132                metrics.push((
133                    MetricId::new("renderer", "gpu_time"),
134                    MetricValue::Gauge(main_ms as f64 / 1000.0),
135                ));
136            }
137        }
138
139        metrics
140    }
141
142    fn as_any(&self) -> &dyn std::any::Any {
143        self
144    }
145
146    fn update(&self) {
147        // GPU monitor updates are handled by update_from_frame_stats()
148        // when called from the render system, so no additional work needed here
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn gpu_monitor_creation() {
158        let monitor = GpuMonitor::new("TestGPU".to_string());
159        assert_eq!(monitor.monitor_id(), "Gpu_TestGPU");
160        assert_eq!(monitor.resource_type(), MonitoredResourceType::Gpu);
161    }
162
163    #[test]
164    fn gpu_monitor_update_stats() {
165        let monitor = GpuMonitor::new("TestGPU".to_string());
166
167        // Initially no performance report
168        assert!(monitor.get_gpu_report().is_none());
169
170        // Create sample render stats
171        let render_stats = RenderStats {
172            frame_number: 42,
173            cpu_preparation_time_ms: 1.0,
174            cpu_render_submission_time_ms: 0.5,
175            gpu_main_pass_time_ms: 16.67,
176            gpu_frame_total_time_ms: 16.67,
177            draw_calls: 100,
178            triangles_rendered: 1000,
179            vram_usage_estimate_mb: 256.0,
180        };
181
182        // Update stats
183        monitor.update_from_frame_stats(&render_stats);
184
185        // Should now have a performance report
186        let report = monitor.get_gpu_report();
187        assert!(report.is_some());
188
189        let report = report.unwrap();
190        assert_eq!(report.frame_number, 42);
191        assert_eq!(report.cpu_preparation_time_us, Some(1000)); // 1ms = 1000μs
192        assert_eq!(report.cpu_submission_time_us, Some(500)); // 0.5ms = 500μs
193    }
194
195    #[test]
196    fn gpu_report_hook_methods() {
197        let monitor = GpuMonitor::new("TestGPU".to_string());
198
199        let render_stats = RenderStats {
200            frame_number: 1,
201            cpu_preparation_time_ms: 0.1,
202            cpu_render_submission_time_ms: 0.05,
203            gpu_main_pass_time_ms: 16.67,
204            gpu_frame_total_time_ms: 17.0,
205            draw_calls: 50,
206            triangles_rendered: 500,
207            vram_usage_estimate_mb: 128.0,
208        };
209
210        monitor.update_from_frame_stats(&render_stats);
211        let report = monitor.get_gpu_report().unwrap();
212
213        // Test frame number
214        assert_eq!(report.frame_number, 1);
215
216        // With our RenderStats-based hook calculation, we now have hook timings
217        assert_eq!(report.frame_total_duration_us(), Some(17000)); // 17ms = 17000μs
218        assert_eq!(report.main_pass_duration_us(), Some(16670)); // 16.67ms = 16670μs
219    }
220
221    #[test]
222    fn gpu_report_missing_data() {
223        let monitor = GpuMonitor::new("TestGPU".to_string());
224
225        let render_stats = RenderStats {
226            frame_number: 1,
227            cpu_preparation_time_ms: 0.0,
228            cpu_render_submission_time_ms: 0.0,
229            gpu_main_pass_time_ms: 0.0,
230            gpu_frame_total_time_ms: 0.0,
231            draw_calls: 0,
232            triangles_rendered: 0,
233            vram_usage_estimate_mb: 0.0,
234        };
235
236        monitor.update_from_frame_stats(&render_stats);
237        let report = monitor.get_gpu_report().unwrap();
238
239        // With zero timing values, hook timings will still be calculated (starting at 0)
240        assert_eq!(report.frame_total_duration_us(), Some(0)); // 0ms = 0μs
241        assert_eq!(report.main_pass_duration_us(), Some(0)); // 0ms = 0μs
242    }
243}