Skip to main content

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}