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 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}