Skip to main content

ferroui_base/rendering/composition/server/
frame_time_graph.rs

1use super::DiagnosticTextRenderer;
2use crate::media::immutable::{ImmutablePen, ImmutableSolidColorBrush};
3use crate::media::{BoxShadows, Brushes};
4use crate::platform::{self, IDrawingContextImpl, IGeometryImpl, IPlatformRenderInterface, IStreamGeometryBuilder};
5use crate::{Matrix, Point, Rect, Size};
6use std::cell::{Cell, RefCell};
7use std::rc::Rc;
8use std::sync::Arc;
9
10const HEADER_PADDING: f64 = 2.0;
11
12/// Represents a simple time graph for diagnostics purpose, used to show layout and render times.
13pub struct FrameTimeGraph {
14    render_interface: GraphRenderInterface,
15    border_brush: ImmutableSolidColorBrush,
16    graph_pen: ImmutablePen,
17    frame_values: RefCell<Box<[f64]>>,
18    size: Size,
19    header_size: Size,
20    graph_size: Size,
21    default_max_y: f64,
22    title: String,
23    text_renderer: Rc<DiagnosticTextRenderer>,
24
25    start_frame_index: Cell<usize>,
26    frame_count: Cell<usize>,
27}
28
29/// Where a graph gets the render interface from when it builds its
30/// geometry.
31pub enum GraphRenderInterface {
32    /// A handle of the calling thread.
33    Owned(Rc<dyn IPlatformRenderInterface>),
34    /// The render interface of a server compositor, lent by its context
35    /// manager for each call: a graph drawn by the render thread must not
36    /// hold a handle that is shared with the UI thread.
37    Compositor(std::rc::Weak<super::ServerCompositor>),
38}
39
40impl From<Rc<dyn IPlatformRenderInterface>> for GraphRenderInterface {
41    fn from(render_interface: Rc<dyn IPlatformRenderInterface>) -> Self {
42        GraphRenderInterface::Owned(render_interface)
43    }
44}
45
46impl GraphRenderInterface {
47    fn create_stream_geometry_builder(&self) -> Box<dyn IStreamGeometryBuilder> {
48        match self {
49            GraphRenderInterface::Owned(render_interface) => render_interface.create_stream_geometry_builder(None),
50            GraphRenderInterface::Compositor(compositor) => compositor
51                .upgrade()
52                .and_then(|compositor| {
53                    compositor.render_interface().with_platform_render_interface(|render_interface| {
54                        render_interface.create_stream_geometry_builder(None)
55                    })
56                })
57                .expect("a time graph is drawn by a compositor that has a render interface"),
58        }
59    }
60}
61
62impl FrameTimeGraph {
63    pub fn size(&self) -> Size {
64        self.size
65    }
66
67    /// Creates a graph using the platform render interface registered in the
68    /// service locator.
69    pub fn new(
70        max_frames: i32,
71        size: Size,
72        default_max_y: f64,
73        title: &str,
74        text_renderer: Rc<DiagnosticTextRenderer>,
75    ) -> Self {
76        Self::with_render_interface(
77            platform::render_interface(),
78            max_frames,
79            size,
80            default_max_y,
81            title,
82            text_renderer,
83        )
84    }
85
86    /// Creates a graph that builds its geometry with `render_interface`
87    /// instead of looking the render interface up in the service locator.
88    pub fn with_render_interface(
89        render_interface: impl Into<GraphRenderInterface>,
90        max_frames: i32,
91        size: Size,
92        default_max_y: f64,
93        title: &str,
94        text_renderer: Rc<DiagnosticTextRenderer>,
95    ) -> Self {
96        debug_assert!(max_frames >= 1);
97        debug_assert!(size.width > 0.0);
98        debug_assert!(size.height > 0.0);
99
100        let header_size = Size::new(size.width, text_renderer.get_max_height() + HEADER_PADDING * 2.0);
101        Self {
102            render_interface: render_interface.into(),
103            border_brush: ImmutableSolidColorBrush::from_uint32(0x80808080),
104            graph_pen: ImmutablePen::with_brush(Some(Brushes::blue()), 1.0),
105            frame_values: RefCell::new(vec![0.0; max_frames.max(0) as usize].into_boxed_slice()),
106            size,
107            header_size,
108            graph_size: Size::new(size.width, size.height - header_size.height),
109            default_max_y,
110            title: title.to_string(),
111            text_renderer,
112            start_frame_index: Cell::new(0),
113            frame_count: Cell::new(0),
114        }
115    }
116
117    pub fn add_frame_value(&self, value: f64) {
118        let mut frame_values = self.frame_values.borrow_mut();
119        if self.frame_count.get() < frame_values.len() {
120            frame_values[self.start_frame_index.get() + self.frame_count.get()] = value;
121            self.frame_count.set(self.frame_count.get() + 1);
122        } else {
123            // overwrite oldest value
124            frame_values[self.start_frame_index.get()] = value;
125            self.start_frame_index.set(self.start_frame_index.get() + 1);
126            if self.start_frame_index.get() == frame_values.len() {
127                self.start_frame_index.set(0);
128            }
129        }
130    }
131
132    pub fn reset(&self) {
133        self.start_frame_index.set(0);
134        self.frame_count.set(0);
135    }
136
137    /// Draws the graph at the origin of the current transform of `context`.
138    pub fn render(&self, context: &mut dyn IDrawingContextImpl) {
139        context.push_clip(Rect::from_size(self.size));
140
141        let no_shadows = BoxShadows::default();
142        context.draw_rectangle(Some(&self.border_brush), None, Rect::from_size(self.size).into(), &no_shadows);
143        context.draw_rectangle(Some(&self.border_brush), None, Rect::from_size(self.header_size).into(), &no_shadows);
144
145        let outer_transform = context.transform();
146        context.set_transform(Matrix::create_translation(HEADER_PADDING, HEADER_PADDING) * outer_transform);
147        self.text_renderer.draw_ascii_text(context, &self.title, &*Brushes::black());
148
149        if self.frame_count.get() > 0 {
150            let (min, avg, max) = self.get_y_values();
151
152            self.draw_labelled_value(context, "Min", min, self.header_size.width * 0.19);
153            self.draw_labelled_value(context, "Avg", avg, self.header_size.width * 0.46);
154            self.draw_labelled_value(context, "Max", max, self.header_size.width * 0.73);
155
156            let header_transform = context.transform();
157            context.set_transform(Matrix::create_translation(0.0, self.header_size.height) * header_transform);
158            let geometry = self.build_graph_geometry(max.max(self.default_max_y));
159            let geometry: &dyn IGeometryImpl = &*geometry;
160            context.draw_geometry(None, Some(&self.graph_pen), geometry);
161            context.set_transform(header_transform);
162        }
163
164        context.set_transform(outer_transform);
165        context.pop_clip();
166    }
167
168    fn draw_labelled_value(&self, context: &mut dyn IDrawingContextImpl, label: &str, value: f64, left: f64) {
169        let old_transform = context.transform();
170        context.set_transform(Matrix::create_translation(left + HEADER_PADDING, HEADER_PADDING) * old_transform);
171
172        let brush = if value <= self.default_max_y { Brushes::black() } else { Brushes::red() };
173
174        // The text is written into a 24 character buffer; when it does not
175        // fit, nothing is written at all.
176        let mut text = format!("{label}: {:>5}ms", format_f2(value));
177        if text.encode_utf16().count() > 24 {
178            text.clear();
179        }
180        self.text_renderer.draw_ascii_text(context, &text, &*brush);
181
182        context.set_transform(old_transform);
183    }
184
185    fn build_graph_geometry(&self, max_y: f64) -> Arc<dyn IGeometryImpl> {
186        debug_assert!(self.frame_count.get() > 0);
187
188        let mut geometry_context = self.render_interface.create_stream_geometry_builder();
189
190        let frame_values = self.frame_values.borrow();
191        let x_ratio = self.graph_size.width / frame_values.len() as f64;
192        let y_ratio = self.graph_size.height / max_y;
193
194        geometry_context
195            .begin_figure(Point::new(0.0, self.graph_size.height - self.get_frame_value(&frame_values, 0) * y_ratio), false);
196
197        for i in 1..self.frame_count.get() {
198            let x = (i as f64 * x_ratio).round_ties_even();
199            let y = self.graph_size.height - self.get_frame_value(&frame_values, i) * y_ratio;
200            geometry_context.line_to(Point::new(x, y), true);
201        }
202
203        geometry_context.end_figure(false);
204        let graph_geometry = geometry_context.to_geometry();
205        geometry_context.dispose();
206        graph_geometry
207    }
208
209    fn get_y_values(&self) -> (f64, f64, f64) {
210        debug_assert!(self.frame_count.get() > 0);
211
212        let frame_values = self.frame_values.borrow();
213        let mut min = f64::MAX;
214        let mut max = f64::MIN;
215        let mut total = 0.0;
216
217        for i in 0..self.frame_count.get() {
218            let y = self.get_frame_value(&frame_values, i);
219
220            total += y;
221
222            if y < min {
223                min = y;
224            }
225
226            if y > max {
227                max = y;
228            }
229        }
230
231        (min, total / self.frame_count.get() as f64, max)
232    }
233
234    #[inline]
235    fn get_frame_value(&self, frame_values: &[f64], frame_offset: usize) -> f64 {
236        frame_values[(self.start_frame_index.get() + frame_offset) % frame_values.len()]
237    }
238}
239
240/// Formats `value` with two decimals the way the invariant culture `F2`
241/// format does. Finite values come out the same as with the standard
242/// formatter (correctly rounded, exact ties to even); only the non-finite
243/// values are spelled differently: `NaN`, `Infinity` and `-Infinity`.
244fn format_f2(value: f64) -> String {
245    if value.is_nan() {
246        return "NaN".to_string();
247    }
248    if value.is_infinite() {
249        return if value > 0.0 { "Infinity".to_string() } else { "-Infinity".to_string() };
250    }
251
252    format!("{value:.2}")
253}
254
255#[cfg(test)]
256mod tests {
257    use super::super::diagnostic_text_renderer::tests::create_renderer;
258    use super::super::dirty_rects::test_mocks::{MockDrawingContext, MockRenderInterface};
259    use super::*;
260
261    fn create_graph(max_frames: i32) -> (FrameTimeGraph, Rc<MockRenderInterface>) {
262        let platform = Rc::new(MockRenderInterface::default());
263        // The mock glyphs are at most 14 high: the header is 18 high and
264        // the graph area 360 x 46.
265        let graph = FrameTimeGraph::with_render_interface(
266            platform.clone() as Rc<dyn IPlatformRenderInterface>,
267            max_frames,
268            Size::new(360.0, 64.0),
269            20.0,
270            "Render",
271            Rc::new(create_renderer()),
272        );
273        (graph, platform)
274    }
275
276    /// Splits the glyph log into runs of text, keyed by the y/x origin and brush of their first glyph.
277    fn drawn_texts(ctx: &MockDrawingContext) -> Vec<(String, String)> {
278        let mut texts: Vec<(String, String, f64)> = Vec::new();
279        for line in ctx.log.iter().filter_map(|l| l.strip_prefix("glyph ")) {
280            let parts: Vec<&str> = line.split(' ').collect();
281            let c = char::from_u32(parts[0].parse::<u32>().unwrap()).unwrap();
282            let x: f64 = parts[3].split(',').next().unwrap().parse().unwrap();
283            match texts.last_mut() {
284                // Mock glyphs are 6 wide: a glyph continues the run when it
285                // starts where the previous one ended.
286                Some((text, _, next_x)) if *next_x == x => {
287                    text.push(c);
288                    *next_x += 6.0;
289                }
290                _ => texts.push((c.to_string(), format!("{} at {}", parts[1], parts[3]), x + 6.0)),
291            }
292        }
293        texts.into_iter().map(|(text, origin, _)| (text, origin)).collect()
294    }
295
296    #[test]
297    fn format_f2_matches_invariant_fixed_point_formatting() {
298        // Reference values produced by the C# formatter.
299        assert_eq!("0.00", format_f2(0.0));
300        assert_eq!("16.67", format_f2(1000.0 / 60.0));
301        assert_eq!("0.12", format_f2(0.125));
302        assert_eq!("0.38", format_f2(0.375));
303        assert_eq!("2.62", format_f2(2.625));
304        assert_eq!("-0.12", format_f2(-0.125));
305        assert_eq!("1234.88", format_f2(1234.875));
306        assert_eq!("0.99", format_f2(0.994));
307        assert_eq!("0.99", format_f2(0.995));
308        assert_eq!("2.50", format_f2(2.5));
309        assert_eq!("0.01", format_f2(0.005));
310        assert_eq!("-0.00", format_f2(-0.0));
311        assert_eq!("-0.00", format_f2(-0.001));
312        assert_eq!("100000000000000000000.00", format_f2(1.0e20));
313        assert_eq!("NaN", format_f2(f64::NAN));
314        assert_eq!("Infinity", format_f2(f64::INFINITY));
315        assert_eq!("-Infinity", format_f2(f64::NEG_INFINITY));
316    }
317
318    #[test]
319    fn size_is_the_requested_size() {
320        let (graph, _) = create_graph(4);
321        assert_eq!(Size::new(360.0, 64.0), graph.size());
322    }
323
324    #[test]
325    fn values_fill_up_and_then_overwrite_the_oldest() {
326        let (graph, _) = create_graph(3);
327        graph.add_frame_value(1.0);
328        graph.add_frame_value(2.0);
329        assert_eq!((1.0, 1.5, 2.0), graph.get_y_values());
330
331        graph.add_frame_value(3.0);
332        graph.add_frame_value(4.0);
333        // 1.0 was dropped; the window is 2, 3, 4 in that order.
334        assert_eq!((2.0, 3.0, 4.0), graph.get_y_values());
335        let values = graph.frame_values.borrow();
336        assert_eq!(
337            vec![2.0, 3.0, 4.0],
338            (0..3).map(|i| graph.get_frame_value(&values, i)).collect::<Vec<_>>()
339        );
340        drop(values);
341
342        graph.add_frame_value(5.0);
343        graph.add_frame_value(6.0);
344        // The start index wrapped around.
345        assert_eq!(0, graph.start_frame_index.get());
346        assert_eq!((4.0, 5.0, 6.0), graph.get_y_values());
347
348        graph.reset();
349        assert_eq!(0, graph.frame_count.get());
350        graph.add_frame_value(9.0);
351        assert_eq!((9.0, 9.0, 9.0), graph.get_y_values());
352    }
353
354    #[test]
355    fn renders_only_the_frame_and_title_without_values() {
356        let (graph, platform) = create_graph(4);
357        let mut ctx =
358            MockDrawingContext { transform: Matrix::create_translation(10.0, 100.0), ..Default::default() };
359        graph.render(&mut ctx);
360
361        assert_eq!("push_clip 0, 0, 360, 64", ctx.log[0]);
362        assert_eq!("rect #80808080@1 0, 0, 360, 64", ctx.log[1]);
363        assert_eq!("rect #80808080@1 0, 0, 360, 18", ctx.log[2]);
364        assert_eq!("pop_clip", ctx.log.last().unwrap());
365        assert_eq!(vec![("Render".to_string(), "#ff000000@1 at 12,102".to_string())], drawn_texts(&ctx));
366        assert!(platform.streams.borrow().is_empty());
367        assert_eq!(Matrix::create_translation(10.0, 100.0), ctx.transform);
368    }
369
370    #[test]
371    fn renders_labels_and_the_graph_geometry() {
372        let (graph, platform) = create_graph(4);
373        graph.add_frame_value(10.0);
374        graph.add_frame_value(40.0);
375        graph.add_frame_value(25.125);
376        let mut ctx = MockDrawingContext::default();
377        graph.render(&mut ctx);
378
379        assert_eq!(
380            vec![
381                ("Render".to_string(), "#ff000000@1 at 2,2".to_string()),
382                // Label origins: header padding twice plus 19% / 46% / 73% of the width.
383                ("Min: 10.00ms".to_string(), "#ff000000@1 at 72.4,4".to_string()),
384                // The average is above the default maximum of 20: red.
385                ("Avg: 25.04ms".to_string(), "#ffff0000@1 at 169.6,4".to_string()),
386                ("Max: 40.00ms".to_string(), "#ffff0000@1 at 266.8,4".to_string()),
387            ],
388            drawn_texts(&ctx)
389        );
390
391        // The geometry is drawn below the header, stroked blue.
392        assert!(ctx.log.contains(&"geometry none pen #ff0000ff@1 at 2,20".to_string()), "{:?}", ctx.log);
393        assert_eq!("pop_clip", ctx.log.last().unwrap());
394        assert_eq!(Matrix::IDENTITY, ctx.transform);
395
396        // 4 frames over 360: 90 per frame. Max y is 40 over a height of 46.
397        let streams = platform.streams.borrow();
398        assert_eq!(1, streams.len());
399        assert_eq!(
400            vec![
401                format!("begin {} false", Point::new(0.0, 46.0 - 10.0 * (46.0 / 40.0))),
402                format!("line {} true", Point::new(90.0, 46.0 - 40.0 * (46.0 / 40.0))),
403                format!("line {} true", Point::new(180.0, 46.0 - 25.125 * (46.0 / 40.0))),
404                "end false".to_string(),
405                "dispose".to_string(),
406            ],
407            *streams[0].lock().unwrap()
408        );
409    }
410
411    #[test]
412    fn graph_is_scaled_to_the_default_maximum_when_values_are_small() {
413        let (graph, platform) = create_graph(8);
414        graph.add_frame_value(5.0);
415        graph.add_frame_value(10.0);
416        let mut ctx = MockDrawingContext::default();
417        graph.render(&mut ctx);
418        let streams = platform.streams.borrow();
419        // x = round(1 * 45) = 45; y scale is 46 / 20.
420        assert_eq!(format!("begin {} false", Point::new(0.0, 46.0 - 5.0 * 2.3)), streams[0].lock().unwrap()[0]);
421        assert_eq!(format!("line {} true", Point::new(45.0, 46.0 - 10.0 * 2.3)), streams[0].lock().unwrap()[1]);
422        let texts = drawn_texts(&ctx);
423        // The value is right-aligned in 5 characters.
424        assert_eq!("Min:  5.00ms", texts[1].0);
425    }
426
427    #[test]
428    fn x_positions_round_half_to_even() {
429        let platform = Rc::new(MockRenderInterface::default());
430        // 5 frames over 2.5: ratio 0.5, so x is 0.5, 1, 1.5, 2 -> 0, 1, 2, 2.
431        let graph = FrameTimeGraph::with_render_interface(
432            platform.clone() as Rc<dyn IPlatformRenderInterface>,
433            5,
434            Size::new(2.5, 64.0),
435            20.0,
436            "",
437            Rc::new(create_renderer()),
438        );
439        for _ in 0..5 {
440            graph.add_frame_value(0.0);
441        }
442        graph.render(&mut MockDrawingContext::default());
443        let streams = platform.streams.borrow();
444        let xs: Vec<String> =
445            streams[0].lock().unwrap().iter().filter(|l| l.starts_with("line")).map(|l| l.to_string()).collect();
446        assert_eq!(
447            [0.0, 1.0, 2.0, 2.0].iter().map(|x| format!("line {} true", Point::new(*x, 46.0))).collect::<Vec<_>>(),
448            xs
449        );
450    }
451
452    #[test]
453    fn labels_that_do_not_fit_the_buffer_are_not_drawn() {
454        let (graph, _) = create_graph(4);
455        graph.add_frame_value(1.0e20);
456        let mut ctx = MockDrawingContext::default();
457        graph.render(&mut ctx);
458        // "Min: 100000000000000000000.00ms" is longer than 24 characters.
459        assert_eq!(1, drawn_texts(&ctx).len());
460    }
461}