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