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::RenderStats;
21use khora_core::renderer::GpuHook;
22use khora_core::telemetry::monitoring::{
23    GpuReport, MonitoredResourceType, ResourceMonitor, ResourceUsageReport,
24};
25
26/// GPU performance monitor that works with any RenderSystem implementation.
27#[derive(Debug)]
28pub struct GpuMonitor {
29    system_name: String,
30    last_frame_stats: Mutex<Option<GpuReport>>,
31}
32
33impl GpuMonitor {
34    /// Create a new GPU performance monitor
35    pub fn new(system_name: String) -> Self {
36        Self {
37            system_name,
38            last_frame_stats: Mutex::new(None),
39        }
40    }
41
42    /// Returns the latest detailed GPU performance report.
43    pub fn get_gpu_report(&self) -> Option<GpuReport> {
44        *self.last_frame_stats.lock().unwrap()
45    }
46
47    /// Update performance stats from frame timing data
48    pub fn update_from_frame_stats(&self, render_stats: &RenderStats) {
49        // Create hook timings based on render stats
50        // We simulate the timeline: FrameStart -> MainPassBegin -> MainPassEnd -> FrameEnd
51        let frame_start_us = 0u32; // Start of frame timeline
52        let frame_end_us = (render_stats.gpu_frame_total_time_ms * 1000.0) as u32;
53        let main_pass_duration_us = (render_stats.gpu_main_pass_time_ms * 1000.0) as u32;
54
55        // Place main pass in the middle of the frame for simplicity
56        let main_pass_begin_us = (frame_end_us - main_pass_duration_us) / 2;
57        let main_pass_end_us = main_pass_begin_us + main_pass_duration_us;
58
59        let mut hook_timings = [None; 4];
60        hook_timings[GpuHook::FrameStart as usize] = Some(frame_start_us);
61        hook_timings[GpuHook::MainPassBegin as usize] = Some(main_pass_begin_us);
62        hook_timings[GpuHook::MainPassEnd as usize] = Some(main_pass_end_us);
63        hook_timings[GpuHook::FrameEnd as usize] = Some(frame_end_us);
64
65        let report = GpuReport {
66            frame_number: render_stats.frame_number,
67            hook_timings_us: hook_timings,
68            // Convert milliseconds to microseconds
69            cpu_preparation_time_us: Some((render_stats.cpu_preparation_time_ms * 1000.0) as u32),
70            cpu_submission_time_us: Some(
71                (render_stats.cpu_render_submission_time_ms * 1000.0) as u32,
72            ),
73        };
74
75        let mut last_stats = self.last_frame_stats.lock().unwrap();
76        *last_stats = Some(report);
77    }
78}
79
80impl ResourceMonitor for GpuMonitor {
81    fn monitor_id(&self) -> Cow<'static, str> {
82        Cow::Owned(format!("Gpu_{}", self.system_name))
83    }
84
85    fn resource_type(&self) -> MonitoredResourceType {
86        MonitoredResourceType::Gpu
87    }
88
89    fn get_usage_report(&self) -> ResourceUsageReport {
90        // GPU performance doesn't have byte-based usage, so return default
91        ResourceUsageReport::default()
92    }
93
94    fn as_any(&self) -> &dyn std::any::Any {
95        self
96    }
97
98    fn update(&self) {
99        // GPU monitor updates are handled by update_from_frame_stats()
100        // when called from the render system, so no additional work needed here
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn gpu_monitor_creation() {
110        let monitor = GpuMonitor::new("TestGPU".to_string());
111        assert_eq!(monitor.monitor_id(), "Gpu_TestGPU");
112        assert_eq!(monitor.resource_type(), MonitoredResourceType::Gpu);
113    }
114
115    #[test]
116    fn gpu_monitor_update_stats() {
117        let monitor = GpuMonitor::new("TestGPU".to_string());
118
119        // Initially no performance report
120        assert!(monitor.get_gpu_report().is_none());
121
122        // Create sample render stats
123        let render_stats = RenderStats {
124            frame_number: 42,
125            cpu_preparation_time_ms: 1.0,
126            cpu_render_submission_time_ms: 0.5,
127            gpu_main_pass_time_ms: 16.67,
128            gpu_frame_total_time_ms: 16.67,
129            draw_calls: 100,
130            triangles_rendered: 1000,
131            vram_usage_estimate_mb: 256.0,
132        };
133
134        // Update stats
135        monitor.update_from_frame_stats(&render_stats);
136
137        // Should now have a performance report
138        let report = monitor.get_gpu_report();
139        assert!(report.is_some());
140
141        let report = report.unwrap();
142        assert_eq!(report.frame_number, 42);
143        assert_eq!(report.cpu_preparation_time_us, Some(1000)); // 1ms = 1000μs
144        assert_eq!(report.cpu_submission_time_us, Some(500)); // 0.5ms = 500μs
145    }
146
147    #[test]
148    fn gpu_report_hook_methods() {
149        let monitor = GpuMonitor::new("TestGPU".to_string());
150
151        let render_stats = RenderStats {
152            frame_number: 1,
153            cpu_preparation_time_ms: 0.1,
154            cpu_render_submission_time_ms: 0.05,
155            gpu_main_pass_time_ms: 16.67,
156            gpu_frame_total_time_ms: 17.0,
157            draw_calls: 50,
158            triangles_rendered: 500,
159            vram_usage_estimate_mb: 128.0,
160        };
161
162        monitor.update_from_frame_stats(&render_stats);
163        let report = monitor.get_gpu_report().unwrap();
164
165        // Test frame number
166        assert_eq!(report.frame_number, 1);
167
168        // With our RenderStats-based hook calculation, we now have hook timings
169        assert_eq!(report.frame_total_duration_us(), Some(17000)); // 17ms = 17000μs
170        assert_eq!(report.main_pass_duration_us(), Some(16670)); // 16.67ms = 16670μs
171    }
172
173    #[test]
174    fn gpu_report_missing_data() {
175        let monitor = GpuMonitor::new("TestGPU".to_string());
176
177        let render_stats = RenderStats {
178            frame_number: 1,
179            cpu_preparation_time_ms: 0.0,
180            cpu_render_submission_time_ms: 0.0,
181            gpu_main_pass_time_ms: 0.0,
182            gpu_frame_total_time_ms: 0.0,
183            draw_calls: 0,
184            triangles_rendered: 0,
185            vram_usage_estimate_mb: 0.0,
186        };
187
188        monitor.update_from_frame_stats(&render_stats);
189        let report = monitor.get_gpu_report().unwrap();
190
191        // With zero timing values, hook timings will still be calculated (starting at 0)
192        assert_eq!(report.frame_total_duration_us(), Some(0)); // 0ms = 0μs
193        assert_eq!(report.main_pass_duration_us(), Some(0)); // 0ms = 0μs
194    }
195}