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