Skip to main content

ui/
stats.rs

1//! [`Stats`] — the meter: how many frames this window is drawing, and what the
2//! process and the GPU cost, while you watch it.
3//!
4//! A window at rest reads `0`. The count comes from this view's own renders,
5//! which is the same number: gpui re-renders every uncached view once per
6//! window draw. The one render it does not count is the one its own tick
7//! provoked, and [`Painter::woken`] is what says which that was — the clock
8//! knows, so nothing here has to infer it from a stopwatch. Those two draws a
9//! second are the meter's own cost, and the CPU figure includes them.
10//!
11//! Placement is the caller's, as it is for [`crate::control_bar`]:
12//!
13//! ```ignore
14//! let meter = cx.new(Stats::new);
15//! div().relative().size_full()
16//!     .child(page)
17//!     .child(div().absolute().top(px(16.0)).right(px(16.0)).child(meter.clone()))
18//! ```
19//!
20//! [`crate::floating::panel`] is what makes one draggable.
21
22use std::time::Duration;
23
24use gpui::{
25    Context, IntoElement, ParentElement as _, Render, SharedString, Styled as _, Window, div, px,
26};
27use motion::Painter;
28use theme::Theme;
29use web_time::Instant;
30
31use crate::{
32    material::{self, Frosted as _},
33    popover,
34};
35
36/// How often the meter refreshes once nothing else is drawing, and so the span
37/// each reading is measured over.
38const TICK: Duration = Duration::from_millis(500);
39
40/// How long the claim on the clock outlives the render that took it.
41const LEASE: Duration = Duration::from_secs(1);
42
43/// The box's width. Public because a host placing the meter by its trailing
44/// edge has to know how wide it is.
45pub const WIDTH: f32 = 148.0;
46
47/// Width of the value column, so a digit arriving or leaving never reflows the
48/// row it is in.
49const VALUE_WIDTH: f32 = 64.0;
50
51/// The frame and CPU meter. One per window — two mounted meters each count the
52/// other's frames, and neither reads zero again.
53pub struct Stats {
54    painter: Painter,
55    /// Draws this bucket that the meter did not ask for.
56    frames: u32,
57    since: Instant,
58    /// Process CPU time at [`Self::since`].
59    cpu_since: Option<Duration>,
60    /// GPU time spent on this window's frames at [`Self::since`].
61    gpu_since: Option<Duration>,
62    /// Held between recomputes, so the digits stand still long enough to read.
63    reading: Reading,
64}
65
66#[derive(Clone, Copy, Default)]
67struct Reading {
68    fps: f32,
69    /// Percent of one core, the figure Activity Monitor prints.
70    cpu: Option<f32>,
71    /// Percent of wall time the GPU was busy on this window's frames. `None`
72    /// off Metal, where nothing measures it.
73    gpu: Option<f32>,
74}
75
76impl Stats {
77    pub fn new(cx: &mut Context<Self>) -> Self {
78        Self {
79            painter: Painter::of(cx),
80            frames: 0,
81            since: Instant::now(),
82            cpu_since: cpu_time(),
83            gpu_since: None,
84            reading: Reading::default(),
85        }
86    }
87}
88
89impl Render for Stats {
90    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
91        if !self.painter.woken(cx) {
92            self.frames += 1;
93        }
94
95        let now = Instant::now();
96        let elapsed = now.duration_since(self.since);
97        if elapsed >= TICK {
98            let share = |then: Duration, now: Duration| {
99                now.saturating_sub(then).as_secs_f32() / elapsed.as_secs_f32() * 100.0
100            };
101            let (cpu, gpu) = (cpu_time(), window.gpu_time());
102            self.reading = Reading {
103                fps: self.frames as f32 / elapsed.as_secs_f32(),
104                cpu: self.cpu_since.zip(cpu).map(|(then, cpu)| share(then, cpu)),
105                gpu: self.gpu_since.zip(gpu).map(|(then, gpu)| share(then, gpu)),
106            };
107            self.frames = 0;
108            self.since = now;
109            self.cpu_since = cpu;
110            self.gpu_since = gpu;
111        }
112
113        self.painter.lease(1.0 / TICK.as_secs_f32(), LEASE, cx);
114
115        let theme = Theme::of(cx).clone();
116        let reading = self.reading;
117        let card = popover::popover_card(&theme)
118            .w(px(WIDTH))
119            .p(px(10.0))
120            .flex()
121            .flex_col()
122            .gap(px(2.0))
123            .child(row(&theme, "FPS", format!("{:.0}", reading.fps)))
124            .child(row(
125                &theme,
126                "CPU",
127                reading
128                    .cpu
129                    .map_or_else(|| "—".to_string(), |cpu| format!("{cpu:.1}%")),
130            ))
131            .child(row(
132                &theme,
133                "GPU",
134                reading
135                    .gpu
136                    .map_or_else(|| "—".to_string(), |gpu| format!("{gpu:.1}%")),
137            ));
138
139        card.material(material::PANEL_BLUR)
140    }
141}
142
143fn row(theme: &Theme, label: &'static str, value: String) -> impl IntoElement {
144    div()
145        .flex()
146        .flex_row()
147        .items_center()
148        .justify_between()
149        .gap(px(10.0))
150        .child(
151            div()
152                .text_size(px(11.0))
153                .text_color(theme.text_faint)
154                .child(label),
155        )
156        .child(
157            div()
158                .w(px(VALUE_WIDTH))
159                .text_right()
160                .font_family(theme.font_mono.clone())
161                .text_size(px(13.0))
162                .text_color(theme.text)
163                .child(SharedString::from(value)),
164        )
165}
166
167/// Process CPU time — user plus system, every thread. `None` where the platform
168/// has no such call.
169#[cfg(unix)]
170fn cpu_time() -> Option<Duration> {
171    let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
172    // SAFETY: getrusage fills the struct it is handed, and only on success.
173    let usage = unsafe {
174        if libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) != 0 {
175            return None;
176        }
177        usage.assume_init()
178    };
179    let spent = |time: libc::timeval| {
180        Duration::from_secs(time.tv_sec as u64) + Duration::from_micros(time.tv_usec as u64)
181    };
182    Some(spent(usage.ru_utime) + spent(usage.ru_stime))
183}
184
185#[cfg(not(unix))]
186fn cpu_time() -> Option<Duration> {
187    None
188}