1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4/*!
5This module contains a simple helper type to measure the average number of frames rendered per second.
6*/
7
8use crate::animations::Instant;
9use crate::debug_log;
10use crate::timers::{Timer, TimerMode};
11use alloc::format;
12use alloc::rc::Rc;
13use alloc::string::String;
14use alloc::vec::Vec;
15use core::cell::RefCell;
16
17/// The method in which we refresh the window
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum RefreshMode {
20 /// render only when necessary (default)
21 Lazy,
22 /// continuously render to the screen
23 FullSpeed,
24}
25
26/// This struct is filled/provided by the ItemRenderer to return collected metrics
27/// during the rendering of the scene.
28#[derive(Default, Clone)]
29pub struct RenderingMetrics {
30 /// The number of layers that were created. None if the renderer does not create layers.
31 pub layers_created: Option<usize>,
32}
33
34impl core::fmt::Display for RenderingMetrics {
35 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36 if let Some(layer_count: usize) = self.layers_created {
37 write!(f, "[{layer_count} layers created]")
38 } else {
39 Ok(())
40 }
41 }
42}
43
44struct FrameData {
45 timestamp: Instant,
46 metrics: RenderingMetrics,
47}
48
49/// Helper class that rendering backends can use to provide an FPS counter
50pub struct RenderingMetricsCollector {
51 collected_frame_data_since_second_ago: RefCell<Vec<FrameData>>,
52 update_timer: Timer,
53 refresh_mode: RefreshMode,
54 output_console: bool,
55 output_overlay: bool,
56}
57
58impl RenderingMetricsCollector {
59 /// Returns a new instance of the counter if requested by the user (via `SLINT_DEBUG_PERFORMANCE` environment variable).
60 /// The environment variable holds a comma separated list of options:
61 /// * `refresh_lazy`: selects the lazy refresh mode, where measurements are only taken when a frame is rendered (due to user input or animations)
62 /// * `refresh_full_speed`: frames are continuously rendered
63 /// * `console`: the measurement is printed to the console
64 /// * `overlay`: the measurement is drawn as overlay on top of the scene
65 ///
66 /// If enabled, this will also print out some system information such as whether
67 /// this is a debug or release build, as well as the provided winsys_info string.
68 pub fn new(winsys_info: &str) -> Option<Rc<Self>> {
69 #[cfg(feature = "std")]
70 let options = std::env::var("SLINT_DEBUG_PERFORMANCE").ok()?;
71 #[cfg(not(feature = "std"))]
72 let options = option_env!("SLINT_DEBUG_PERFORMANCE")?;
73 let mut output_console = false;
74 let mut output_overlay = false;
75 let mut refresh_mode = None;
76 for option in options.split(',') {
77 match option {
78 "console" => output_console = true,
79 "overlay" => output_overlay = true,
80 "refresh_lazy" => refresh_mode = Some(RefreshMode::Lazy),
81 "refresh_full_speed" => refresh_mode = Some(RefreshMode::FullSpeed),
82 _ => {}
83 }
84 }
85
86 let Some(refresh_mode) = refresh_mode else {
87 debug_log!("Missing refresh mode in SLINT_DEBUG_PERFORMANCE. Please specify either refresh_full_speed or refresh_lazy");
88 return None;
89 };
90
91 if !output_console && !output_overlay {
92 debug_log!("Missing output mode in SLINT_DEBUG_PERFORMANCE. Please specify either console or overlay (or both)");
93 return None;
94 }
95
96 let collector = Rc::new(Self {
97 collected_frame_data_since_second_ago: Default::default(),
98 update_timer: Default::default(),
99 refresh_mode,
100 output_console,
101 output_overlay,
102 });
103
104 #[cfg(debug_assertions)]
105 let build_config = "debug";
106 #[cfg(not(debug_assertions))]
107 let build_config = "release";
108
109 debug_log!("Slint: Build config: {}; Backend: {}", build_config, winsys_info);
110
111 let self_weak = Rc::downgrade(&collector);
112 collector.update_timer.stop();
113 collector.update_timer.start(
114 TimerMode::Repeated,
115 core::time::Duration::from_secs(1),
116 move || {
117 let this = match self_weak.upgrade() {
118 Some(this) => this,
119 None => return,
120 };
121 this.trim_frame_data_to_second_boundary();
122
123 let mut last_frame_details = String::new();
124 if let Some(last_frame_data) =
125 this.collected_frame_data_since_second_ago.borrow().last()
126 {
127 use core::fmt::Write;
128 if write!(&mut last_frame_details, "{}", last_frame_data.metrics).is_ok()
129 && !last_frame_details.is_empty()
130 {
131 last_frame_details.insert_str(0, "details from last frame: ");
132 }
133 }
134
135 if this.output_console {
136 debug_log!(
137 "average frames per second: {} {}",
138 this.collected_frame_data_since_second_ago.borrow().len(),
139 last_frame_details
140 );
141 }
142 },
143 );
144
145 Some(collector)
146 }
147
148 fn trim_frame_data_to_second_boundary(self: &Rc<Self>) {
149 let mut frame_times = self.collected_frame_data_since_second_ago.borrow_mut();
150 let now = Instant::now();
151 frame_times.retain(|frame| {
152 now.duration_since(frame.timestamp) <= core::time::Duration::from_secs(1)
153 });
154 }
155
156 /// Call this function every time you've completed the rendering of a frame. The `renderer` parameter
157 /// is used to collect additional data and is used to render an overlay if enabled.
158 pub fn measure_frame_rendered(
159 self: &Rc<Self>,
160 renderer: &mut dyn crate::item_rendering::ItemRenderer,
161 ) {
162 self.collected_frame_data_since_second_ago
163 .borrow_mut()
164 .push(FrameData { timestamp: Instant::now(), metrics: renderer.metrics() });
165 if matches!(self.refresh_mode, RefreshMode::FullSpeed) {
166 crate::animations::CURRENT_ANIMATION_DRIVER
167 .with(|driver| driver.set_has_active_animations());
168 }
169 self.trim_frame_data_to_second_boundary();
170
171 if self.output_overlay {
172 renderer.draw_string(
173 &format!("FPS: {}", self.collected_frame_data_since_second_ago.borrow().len()),
174 crate::Color::from_rgb_u8(0, 128, 128),
175 );
176 }
177 }
178
179 /// Always render the full screen.
180 pub fn refresh_mode(&self) -> RefreshMode {
181 self.refresh_mode
182 }
183}
184