Skip to main content

ferroui_base/rendering/composition/server/
fps_counter.rs

1use super::diagnostic_text_renderer::ShapedText;
2use super::DiagnosticTextRenderer;
3use crate::media::immutable::ImmutableSolidColorBrush;
4use crate::media::{BoxShadows, Brushes, Colors, IBrush};
5use crate::platform::IDrawingContextImpl;
6use crate::utilities::ByteSizeHelper;
7use crate::{Matrix, Rect};
8use std::cell::{Cell, RefCell};
9use std::rc::Rc;
10use std::time::Duration;
11
12const FRAME_DIGITS: i32 = 8;
13const FPS_DIGITS: i32 = 3;
14const VISUAL_DIGITS: i32 = 5;
15
16/// An FPS counter helper that can draw itself on the render thread.
17/// The labels are shaped once and the changing numbers are drawn in a fixed digit grid,
18/// so a frame neither formats nor measures any text.
19///
20/// The C# class owns a stopwatch started at construction. Here the caller
21/// supplies the time instead: [`new`](Self::new) and
22/// [`render_fps`](Self::render_fps) take the current reading of a monotonic
23/// clock (any epoch), and the counter works with the time elapsed between
24/// the two.
25pub struct FpsCounter {
26    start: Duration,
27    text_renderer: Rc<DiagnosticTextRenderer>,
28    frame_label: Rc<ShapedText>,
29    fps_label: Rc<ShapedText>,
30    visited_label: Rc<ShapedText>,
31    rendered_label: Rc<ShapedText>,
32    digits_width: f64,
33    line_height: f64,
34
35    /// The memory text, `None` while no memory is reported.
36    memory_text: RefCell<Option<ShapedText>>,
37    memory: Cell<Option<(u64, u64)>>,
38    text_width: Cell<f64>,
39
40    frames_this_second: Cell<i32>,
41    total_frames: Cell<i32>,
42    fps: Cell<i32>,
43    last_fps_update: Cell<Duration>,
44}
45
46impl FpsCounter {
47    /// The fixed labels of the counter. The compositor shapes them with the text renderer on
48    /// the UI thread (`ServerCompositor::diagnostic_text_renderer`), so that a counter created
49    /// by the first frame of the render thread finds them shaped: shaping creates glyph runs,
50    /// whose platform implementation comes from the services of the UI thread (the render
51    /// thread of the browser has none). Upstream shapes them in the constructor.
52    pub const LABELS: [&'static str; 4] = ["Frame #", " FPS: ", " V:", " R:"];
53
54    /// `now` is the current reading of the monotonic clock that is also
55    /// passed to [`render_fps`](Self::render_fps).
56    pub fn new(text_renderer: Rc<DiagnosticTextRenderer>, now: Duration) -> Self {
57        let counter = Self {
58            start: now,
59            frame_label: text_renderer.shape_label(Self::LABELS[0]),
60            fps_label: text_renderer.shape_label(Self::LABELS[1]),
61            visited_label: text_renderer.shape_label(Self::LABELS[2]),
62            rendered_label: text_renderer.shape_label(Self::LABELS[3]),
63            digits_width: (FRAME_DIGITS + FPS_DIGITS + 2 * VISUAL_DIGITS) as f64 * text_renderer.digit_cell_width(),
64            line_height: text_renderer.get_max_height(),
65            text_renderer,
66            memory_text: RefCell::new(None),
67            memory: Cell::new(None),
68            text_width: Cell::new(0.0),
69            frames_this_second: Cell::new(0),
70            total_frames: Cell::new(0),
71            fps: Cell::new(0),
72            last_fps_update: Cell::new(Duration::ZERO),
73        };
74        counter.update_text_width();
75        counter
76    }
77
78    pub fn fps_tick(&self) {
79        self.frames_this_second.set(self.frames_this_second.get().wrapping_add(1));
80    }
81
82    /// Counts a frame and draws the counter at the origin of the current
83    /// transform of `context`. Returns the rectangle that was covered.
84    ///
85    /// `memory` is the managed and the native size of the batch pools of
86    /// the compositor in bytes (`M:` and `N:`), or `None` to draw no memory
87    /// text: the compositor of the port has no such pools (see the member
88    /// waivers of `ServerCompositor`).
89    // Deviation (DEVIATIONS.md, Server composition visuals): upstream takes the two sizes and always
90    // draws the memory text.
91    #[allow(clippy::too_many_arguments)]
92    pub fn render_fps(
93        &self,
94        context: &mut dyn IDrawingContextImpl,
95        memory: Option<(u64, u64)>,
96        visited_visuals: i32,
97        rendered_visuals: i32,
98        has_layer: bool,
99        old_rect: Option<Rect>,
100        now: Duration,
101    ) -> Rect {
102        let now = now.saturating_sub(self.start);
103        let elapsed = now.saturating_sub(self.last_fps_update.get());
104
105        self.frames_this_second.set(self.frames_this_second.get().wrapping_add(1));
106        self.total_frames.set(self.total_frames.get().wrapping_add(1));
107
108        if elapsed.as_secs_f64() > 1.0 {
109            self.fps.set((self.frames_this_second.get() as f64 / elapsed.as_secs_f64()) as i32);
110            self.frames_this_second.set(0);
111            self.last_fps_update.set(now);
112        }
113
114        if memory != self.memory.get() {
115            self.memory.set(memory);
116            let memory_text = self.create_memory_text();
117            if let Some(old) = self.memory_text.replace(memory_text) {
118                old.dispose();
119            }
120            self.update_text_width();
121        }
122
123        let mut rect = Rect::new(0.0, 0.0, self.text_width.get() + 3.0, self.line_height + 3.0);
124        if let (true, Some(old_rect)) = (has_layer, old_rect) {
125            rect = rect.union(old_rect);
126        }
127
128        let layer_brush = ImmutableSolidColorBrush::with_opacity(Colors::BLACK, 0.5);
129        let black = Brushes::black();
130        let background: &dyn IBrush = if has_layer { &layer_brush } else { &*black };
131        context.draw_rectangle(Some(background), None, rect.into(), &BoxShadows::default());
132
133        let mut x = 0.0;
134        self.draw_label(context, &*self.frame_label, &mut x);
135        self.draw_number(context, self.total_frames.get(), FRAME_DIGITS, &mut x);
136        self.draw_label(context, &*self.fps_label, &mut x);
137        self.draw_number(context, self.fps.get(), FPS_DIGITS, &mut x);
138        if let Some(memory_text) = &*self.memory_text.borrow() {
139            self.draw_label(context, memory_text, &mut x);
140        }
141        self.draw_label(context, &*self.visited_label, &mut x);
142        self.draw_number(context, visited_visuals, VISUAL_DIGITS, &mut x);
143        self.draw_label(context, &*self.rendered_label, &mut x);
144        self.draw_number(context, rendered_visuals, VISUAL_DIGITS, &mut x);
145
146        rect
147    }
148
149    fn draw_label(&self, context: &mut dyn IDrawingContextImpl, text: &ShapedText, x: &mut f64) {
150        let old_transform = context.transform();
151        context.set_transform(Matrix::create_translation(*x, 0.0) * old_transform);
152        self.text_renderer.draw_shaped_text(context, text, &*Brushes::white());
153        context.set_transform(old_transform);
154
155        *x += text.width();
156    }
157
158    fn draw_number(&self, context: &mut dyn IDrawingContextImpl, value: i32, digit_count: i32, x: &mut f64) {
159        let old_transform = context.transform();
160        context.set_transform(Matrix::create_translation(*x, 0.0) * old_transform);
161        self.text_renderer.draw_digits(context, value, digit_count, &*Brushes::white());
162        context.set_transform(old_transform);
163
164        *x += digit_count as f64 * self.text_renderer.digit_cell_width();
165    }
166
167    fn create_memory_text(&self) -> Option<ShapedText> {
168        let (managed_bytes, native_bytes) = self.memory.get()?;
169        Some(self.text_renderer.shape(&format!(
170            " M:{} / N:{}",
171            ByteSizeHelper::to_string(managed_bytes, false),
172            ByteSizeHelper::to_string(native_bytes, false)
173        )))
174    }
175
176    fn update_text_width(&self) {
177        let memory_width = self.memory_text.borrow().as_ref().map_or(0.0, ShapedText::width);
178        self.text_width.set(
179            self.frame_label.width()
180                + self.fps_label.width()
181                + self.visited_label.width()
182                + self.rendered_label.width()
183                + memory_width
184                + self.digits_width,
185        );
186    }
187
188    pub fn reset(&self) {
189        self.frames_this_second.set(0);
190        self.total_frames.set(0);
191        self.fps.set(0);
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::super::diagnostic_text_renderer::tests::create_renderer;
198    use super::super::dirty_rects::test_mocks::MockDrawingContext;
199    use super::*;
200
201    fn ms(value: u64) -> Duration {
202        Duration::from_millis(value)
203    }
204
205    /// The text drawn by the last `render_fps`, rebuilt from the glyph log:
206    /// single glyphs by their character, shaped runs by their text.
207    fn drawn_text(ctx: &MockDrawingContext) -> String {
208        ctx.log
209            .iter()
210            .filter_map(|l| {
211                if let Some(glyph) = l.strip_prefix("glyph ") {
212                    let code = glyph.split(' ').next().unwrap().parse::<u32>().unwrap();
213                    Some(char::from_u32(code).unwrap().to_string())
214                } else {
215                    let text = l.strip_prefix("text ")?;
216                    let at = text.rfind(" at ").unwrap();
217                    Some(text[..at].rsplit_once(' ').unwrap().0.to_string())
218                }
219            })
220            .collect()
221    }
222
223    fn render(counter: &FpsCounter, visited: i32, rendered: i32, now: Duration) -> String {
224        let mut ctx = MockDrawingContext::default();
225        counter.render_fps(&mut ctx, None, visited, rendered, false, None, now);
226        drawn_text(&ctx)
227    }
228
229    #[test]
230    fn counts_frames_and_updates_fps_once_more_than_a_second_elapsed() {
231        // The clock does not start at zero: only the elapsed time matters.
232        let start = ms(5000);
233        let counter = FpsCounter::new(Rc::new(create_renderer()), start);
234
235        assert_eq!("Frame #00000001 FPS: 000 V:00003 R:00002", render(&counter, 3, 2, start + ms(100)));
236        assert_eq!("Frame #00000002 FPS: 000 V:00000 R:00000", render(&counter, 0, 0, start + ms(500)));
237        // Exactly one second is not enough.
238        assert_eq!("Frame #00000003 FPS: 000 V:00000 R:00000", render(&counter, 0, 0, start + ms(1000)));
239        // 4 frames in 2 seconds.
240        assert_eq!("Frame #00000004 FPS: 002 V:00000 R:00000", render(&counter, 0, 0, start + ms(2000)));
241        // The window restarted at 2s; fps is kept until the next update.
242        assert_eq!("Frame #00000005 FPS: 002 V:00000 R:00000", render(&counter, 0, 0, start + ms(2500)));
243        // 2 frames in 1.25 seconds: 1.6 truncates to 1.
244        assert_eq!("Frame #00000006 FPS: 001 V:00000 R:00000", render(&counter, 0, 0, start + ms(3250)));
245    }
246
247    #[test]
248    fn fps_tick_counts_extra_frames() {
249        let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
250        for _ in 0..119 {
251            counter.fps_tick();
252        }
253        assert_eq!("Frame #00000001 FPS: 060 V:00000 R:00000", render(&counter, 0, 0, ms(2000)));
254    }
255
256    #[test]
257    fn reset_clears_counters_but_not_the_update_time() {
258        let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
259        render(&counter, 0, 0, ms(1500));
260        render(&counter, 0, 0, ms(1600));
261        counter.reset();
262        assert_eq!("Frame #00000001 FPS: 000 V:00000 R:00000", render(&counter, 0, 0, ms(1700)));
263        // 2 frames since the reset, 1.5s since the last update at 1.5s.
264        assert_eq!("Frame #00000002 FPS: 001 V:00000 R:00000", render(&counter, 0, 0, ms(3000)));
265    }
266
267    #[test]
268    fn numbers_are_clamped_to_their_digits() {
269        let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
270        assert_eq!("Frame #00000001 FPS: 000 V:99999 R:00000", render(&counter, 123456, -1, ms(10)));
271    }
272
273    #[test]
274    fn the_memory_text_follows_the_frames_per_second() {
275        let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
276        let mut ctx = MockDrawingContext::default();
277        counter.render_fps(&mut ctx, Some((1024, 2048)), 1, 2, false, None, ms(10));
278        let expected = format!(
279            "Frame #00000001 FPS: 000 M:{} / N:{} V:00001 R:00002",
280            ByteSizeHelper::to_string(1024, false),
281            ByteSizeHelper::to_string(2048, false)
282        );
283        assert_eq!(expected, drawn_text(&ctx));
284    }
285
286    #[test]
287    fn draws_background_then_text_and_returns_the_rect() {
288        let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
289        let mut ctx = MockDrawingContext::default();
290        // "Frame #" (7), " FPS: " (6), " V:" (3), " R:" (3) and 21 digit cells, all 6 wide:
291        // 40 glyphs; the line is 14 high.
292        let rect = counter.render_fps(&mut ctx, None, 0, 0, false, Some(Rect::new(0.0, 0.0, 500.0, 500.0)), ms(10));
293        assert_eq!(Rect::new(0.0, 0.0, 243.0, 17.0), rect);
294        assert_eq!("rect #ff000000@1 0, 0, 243, 17", ctx.log[0]);
295        // The background, four labels and 21 digits.
296        assert_eq!(26, ctx.log.len());
297        assert_eq!("text Frame # #ffffffff@1 at 0,0", ctx.log[1]);
298        assert_eq!("glyph 48 #ffffffff@1 at 42,0", ctx.log[2]);
299    }
300
301    #[test]
302    fn with_a_layer_the_old_rect_is_covered_with_a_translucent_background() {
303        let counter = FpsCounter::new(Rc::new(create_renderer()), Duration::ZERO);
304        let mut ctx = MockDrawingContext::default();
305        let rect = counter.render_fps(&mut ctx, None, 0, 0, true, Some(Rect::new(0.0, 0.0, 300.0, 10.0)), ms(10));
306        assert_eq!(Rect::new(0.0, 0.0, 300.0, 17.0), rect);
307        assert_eq!("rect #ff000000@0.5 0, 0, 300, 17", ctx.log[0]);
308
309        let mut ctx = MockDrawingContext::default();
310        let rect = counter.render_fps(&mut ctx, None, 0, 0, true, None, ms(20));
311        assert_eq!(Rect::new(0.0, 0.0, 243.0, 17.0), rect);
312    }
313}