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