gpui_fps/lib.rs
1//! A realtime performance HUD for GPUI applications: frames per second, a
2//! rolling frame time chart, and this process' GPU, CPU and memory usage.
3//!
4//! Frame data comes from GPUI's own frame trace
5//! ([`gpui::FrameTimingCollector`]). The interval counts frames *presented*,
6//! stamped with their own present time, so it agrees with the platform's
7//! overlay (Metal's HUD counts the same drawables); the frame cost is what the
8//! framework actually spent in `Window::draw`, rather than an approximation
9//! measured from the outside. The headline rate is derived from that cost —
10//! the HUD never drives the frame loop, so nothing it reports is something it
11//! caused.
12//!
13//! Render it wherever it should appear, guarded by your own flag:
14//!
15//! ```no_run
16//! # use gpui::*;
17//! # use gpui_fps::fps_monitor;
18//! # struct Example { show_fps: bool }
19//! # impl Render for Example {
20//! fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
21//! div()
22//! .relative()
23//! .size_full()
24//! .child("your app")
25//! .when(self.show_fps, |this| this.child(fps_monitor(window, cx)))
26//! }
27//! # }
28//! ```
29//!
30//! The returned overlay can change its corner and its frame budget. A custom palette or an
31//! embedded rather than overlaid HUD is built by composing [`FpsMonitor`] and
32//! [`FpsOverlay`] directly.
33//!
34//! This crate depends only on `gpui`, so it can be used from any GPUI
35//! application.
36
37#[cfg(not(target_family = "wasm"))]
38mod gpu;
39#[cfg(not(target_family = "wasm"))]
40mod memory;
41mod monitor;
42mod overlay;
43mod refresh;
44mod sampler;
45mod style;
46
47pub use monitor::FpsMonitor;
48pub use overlay::FpsOverlay;
49
50use std::{collections::HashMap, sync::Mutex};
51
52use gpui::{App, AppContext as _, Entity, Global, Window, WindowId};
53
54/// The performance HUD, pinned to the top right of its parent.
55///
56/// The parent element must be `relative()`, since the HUD positions itself
57/// absolutely. Call this at most once per window — a second call renders the
58/// same monitor twice.
59///
60/// Whether the HUD is on screen is the caller's to decide; render this only
61/// when it should be visible:
62///
63/// ```no_run
64/// # use gpui::*;
65/// # use gpui_fps::fps_monitor;
66/// # struct Example { show_fps: bool }
67/// # impl Render for Example {
68/// fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
69/// div()
70/// .relative()
71/// .size_full()
72/// .child("your app")
73/// .when(self.show_fps, |this| this.child(fps_monitor(window, cx)))
74/// }
75/// # }
76/// ```
77///
78/// The monitor behind it is created on first use and reused afterwards, one per
79/// window, so this can be called straight from `render` every frame.
80pub fn fps_monitor(window: &mut Window, cx: &mut App) -> FpsOverlay {
81 let window_id = window.window_handle().window_id();
82 let existing = cx
83 .try_global::<Monitors>()
84 .and_then(|state| state.0.get(&window_id).cloned());
85 let monitor = match existing {
86 Some(monitor) => monitor,
87 None => {
88 let monitor = cx.new(|cx| FpsMonitor::new(window, cx));
89 cx.default_global::<Monitors>()
90 .0
91 .insert(window_id, monitor.clone());
92 monitor
93 }
94 };
95
96 FpsOverlay::new(&monitor)
97}
98
99/// The monitor [`fps_monitor`] reuses for each window.
100///
101/// Entries outlive their window; the leak is one small entity per window that
102/// ever showed the HUD, which is not worth tracking window closes for.
103#[derive(Default)]
104struct Monitors(HashMap<WindowId, Entity<FpsMonitor>>);
105
106impl Global for Monitors {}
107
108struct TraceState {
109 /// Number of live [`FrameTraceGuard`]s.
110 refs: usize,
111 /// Whether frame tracing was already on when the first guard was taken,
112 /// meaning the host application owns the switch and we must leave it alone.
113 owned_by_host: bool,
114}
115
116static TRACE_STATE: Mutex<TraceState> = Mutex::new(TraceState {
117 refs: 0,
118 owned_by_host: false,
119});
120
121/// Keeps GPUI's frame trace enabled for as long as it is alive.
122///
123/// [`gpui::profiler::set_trace_enabled`] is a process-wide switch, and turning it
124/// off clears the recorded buffer. A monitor therefore must not disable it
125/// while another monitor — or the host application's own profiling — still
126/// depends on it, so guards are reference counted and the switch is only
127/// restored by the last one. If tracing was already on before the first guard,
128/// it is never turned off.
129pub(crate) struct FrameTraceGuard {
130 _private: (),
131}
132
133impl FrameTraceGuard {
134 /// Enables frame tracing if it isn't already on.
135 pub(crate) fn acquire() -> Self {
136 if let Ok(mut state) = TRACE_STATE.lock() {
137 if state.refs == 0 {
138 // Returns false when the value was already `true`, which means
139 // somebody else turned tracing on and owns restoring it.
140 state.owned_by_host = !gpui::profiler::set_trace_enabled(true);
141 }
142 state.refs += 1;
143 }
144 Self { _private: () }
145 }
146}
147
148impl Drop for FrameTraceGuard {
149 fn drop(&mut self) {
150 if let Ok(mut state) = TRACE_STATE.lock() {
151 state.refs = state.refs.saturating_sub(1);
152 if state.refs == 0 && !state.owned_by_host {
153 gpui::profiler::set_trace_enabled(false);
154 }
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn dropping_an_inner_guard_keeps_tracing_on_for_the_outer_guard() {
165 let outer = FrameTraceGuard::acquire();
166 let inner = FrameTraceGuard::acquire();
167 assert!(gpui::profiler::trace_enabled());
168
169 drop(inner);
170 assert!(
171 gpui::profiler::trace_enabled(),
172 "the outer guard still needs the trace"
173 );
174
175 drop(outer);
176 }
177}