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
12pub 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
29pub enum GraphRenderInterface {
32 Owned(Rc<dyn IPlatformRenderInterface>),
34 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 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 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 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 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 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
240fn 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 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 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 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 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 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 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 ("Min: 10.00ms".to_string(), "#ff000000@1 at 72.4,4".to_string()),
384 ("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 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 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 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 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 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 assert_eq!(1, drawn_texts(&ctx).len());
460 }
461}