Skip to main content

ui/
stats.rs

1//! [`Stats`] — the meter: how many frames this window is drawing, and what the
2//! process, the GPU and memory 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::{Glass, SurfaceStyle, TextStyle, Theme, Typeset};
29use web_time::Instant;
30
31use crate::{popover, surface::Surfaced as _};
32
33/// How often the meter refreshes once nothing else is drawing, and so the span
34/// each reading is measured over.
35const TICK: Duration = Duration::from_millis(500);
36
37/// How long the claim on the clock outlives the render that took it.
38const LEASE: Duration = Duration::from_secs(1);
39
40/// The box's width. Public because a host placing the meter by its trailing
41/// edge has to know how wide it is.
42pub const WIDTH: f32 = 148.0;
43
44/// Width of the value column, so a digit arriving or leaving never reflows the
45/// row it is in.
46const VALUE_WIDTH: f32 = 64.0;
47
48/// The frame and CPU meter. One per window — two mounted meters each count the
49/// other's frames, and neither reads zero again.
50pub struct Stats {
51    painter: Painter,
52    /// Draws this bucket that the meter did not ask for.
53    frames: u32,
54    since: Instant,
55    /// Process CPU time at [`Self::since`].
56    cpu_since: Option<Duration>,
57    /// GPU time spent on this window's frames at [`Self::since`].
58    gpu_since: Option<Duration>,
59    /// Held between recomputes, so the digits stand still long enough to read.
60    reading: Reading,
61}
62
63#[derive(Clone, Copy, Default)]
64struct Reading {
65    fps: f32,
66    /// Percent of one core, the figure Activity Monitor prints.
67    cpu: Option<f32>,
68    /// Percent of wall time the GPU was busy on this window's frames. `None`
69    /// off Metal, where nothing measures it.
70    gpu: Option<f32>,
71    /// This process's resident bytes — a level rather than a rate, so it is
72    /// sampled at the tick instead of differenced across one. `None` where the
73    /// platform hands out no process figure, a browser tab included.
74    mem: Option<u64>,
75}
76
77impl Stats {
78    pub fn new(cx: &mut Context<Self>) -> Self {
79        Self {
80            painter: Painter::of(cx),
81            frames: 0,
82            since: Instant::now(),
83            cpu_since: cpu_time(),
84            gpu_since: None,
85            reading: Reading::default(),
86        }
87    }
88}
89
90impl Render for Stats {
91    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
92        if !self.painter.woken(cx) {
93            self.frames += 1;
94        }
95
96        let now = Instant::now();
97        let elapsed = now.duration_since(self.since);
98        if elapsed >= TICK {
99            let share = |then: Duration, now: Duration| {
100                now.saturating_sub(then).as_secs_f32() / elapsed.as_secs_f32() * 100.0
101            };
102            let (cpu, gpu) = (cpu_time(), window.gpu_time());
103            self.reading = Reading {
104                fps: self.frames as f32 / elapsed.as_secs_f32(),
105                cpu: self.cpu_since.zip(cpu).map(|(then, cpu)| share(then, cpu)),
106                gpu: self.gpu_since.zip(gpu).map(|(then, gpu)| share(then, gpu)),
107                mem: memory(),
108            };
109            self.frames = 0;
110            self.since = now;
111            self.cpu_since = cpu;
112            self.gpu_since = gpu;
113        }
114
115        self.painter.lease(1.0 / TICK.as_secs_f32(), LEASE, cx);
116
117        // Regular whatever the app mounts its menus on, and the card is handed
118        // the same choice — a card that thinks it is frost paints a fill, and a
119        // fill over a lens buries it.
120        let theme = Theme {
121            popover_surface: SurfaceStyle::Glass(Glass::Regular),
122            ..Theme::of(cx).clone()
123        };
124        let reading = self.reading;
125        let card = popover::popover_card(&theme)
126            .w(px(WIDTH))
127            .p(px(10.0))
128            .flex()
129            .flex_col()
130            .gap(px(2.0))
131            .child(row(&theme, "FPS", format!("{:.0}", reading.fps)))
132            .child(row(
133                &theme,
134                "CPU",
135                reading
136                    .cpu
137                    .map_or_else(|| "—".to_string(), |cpu| format!("{cpu:.1}%")),
138            ))
139            .child(row(
140                &theme,
141                "GPU",
142                reading
143                    .gpu
144                    .map_or_else(|| "—".to_string(), |gpu| format!("{gpu:.1}%")),
145            ))
146            .child(row(
147                &theme,
148                "MEM",
149                reading.mem.map_or_else(
150                    || "—".to_string(),
151                    // Whole megabytes: a fourth digit and a decimal together
152                    // outgrow the value column, and this number moves in
153                    // megabytes anyway.
154                    |mem| format!("{:.0} MB", mem as f32 / 1e6),
155                ),
156            ));
157
158        // Regular, not clear: a meter floats over the thing it is measuring,
159        // and the everyday material is the one that both refracts and carries
160        // dense content. Its blur and its dimming come with the look — Apple
161        // exposes no way to ask for either, and neither does this.
162        card.surface(&theme, theme.popover_surface)
163    }
164}
165
166fn row(theme: &Theme, label: &'static str, value: String) -> impl IntoElement {
167    div()
168        .flex()
169        .flex_row()
170        .items_center()
171        .justify_between()
172        .gap(px(10.0))
173        .child(
174            div()
175                .text_style(TextStyle::Subheadline)
176                .text_color(theme.text_faint)
177                .child(label),
178        )
179        .child(
180            div()
181                .w(px(VALUE_WIDTH))
182                .text_right()
183                .font_family(theme.font_mono.clone())
184                .text_style(TextStyle::Body)
185                .text_color(theme.text)
186                .child(SharedString::from(value)),
187        )
188}
189
190/// Process CPU time — user plus system, every thread. `None` where the platform
191/// has no such call.
192#[cfg(unix)]
193fn cpu_time() -> Option<Duration> {
194    let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
195    // SAFETY: getrusage fills the struct it is handed, and only on success.
196    let usage = unsafe {
197        if libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) != 0 {
198            return None;
199        }
200        usage.assume_init()
201    };
202    let spent = |time: libc::timeval| {
203        Duration::from_secs(time.tv_sec as u64) + Duration::from_micros(time.tv_usec as u64)
204    };
205    Some(spent(usage.ru_utime) + spent(usage.ru_stime))
206}
207
208/// The same two figures, from the kernel's own counters. `FILETIME` counts
209/// 100-nanosecond ticks, and the two the process has not used are zero rather
210/// than absent.
211#[cfg(target_os = "windows")]
212fn cpu_time() -> Option<Duration> {
213    use windows_sys::Win32::{Foundation::FILETIME, System::Threading};
214
215    let zero = || FILETIME {
216        dwLowDateTime: 0,
217        dwHighDateTime: 0,
218    };
219    let (mut created, mut exited, mut kernel, mut user) = (zero(), zero(), zero(), zero());
220    // SAFETY: the four are written only on success, and the pseudo handle
221    // `GetCurrentProcess` returns needs no close.
222    let read = unsafe {
223        Threading::GetProcessTimes(
224            Threading::GetCurrentProcess(),
225            &mut created,
226            &mut exited,
227            &mut kernel,
228            &mut user,
229        )
230    };
231    if read == 0 {
232        return None;
233    }
234    let ticks = |time: FILETIME| ((time.dwHighDateTime as u64) << 32) | time.dwLowDateTime as u64;
235    Some(Duration::from_nanos((ticks(kernel) + ticks(user)) * 100))
236}
237
238#[cfg(not(any(unix, target_os = "windows")))]
239fn cpu_time() -> Option<Duration> {
240    None
241}
242
243/// Resident bytes — the figure Activity Monitor prints in its Memory column.
244///
245/// libc deprecated its Mach bindings in favour of the `mach2` crate; the symbol
246/// is a stable part of the platform ABI, and a whole dependency on a published
247/// crate is a lot to carry for one port name.
248#[cfg(target_os = "macos")]
249#[allow(deprecated)]
250fn memory() -> Option<u64> {
251    let mut info = std::mem::MaybeUninit::<libc::mach_task_basic_info>::uninit();
252    let mut count = libc::MACH_TASK_BASIC_INFO_COUNT;
253    // SAFETY: task_info fills the struct it is handed, and only on success.
254    let info = unsafe {
255        if libc::task_info(
256            libc::mach_task_self(),
257            libc::MACH_TASK_BASIC_INFO,
258            info.as_mut_ptr().cast(),
259            &mut count,
260        ) != libc::KERN_SUCCESS
261        {
262            return None;
263        }
264        info.assume_init()
265    };
266    Some(info.resident_size)
267}
268
269/// The working set — what Task Manager prints in its Memory column, and the
270/// closest Windows has to a resident size.
271#[cfg(target_os = "windows")]
272fn memory() -> Option<u64> {
273    use windows_sys::Win32::System::{ProcessStatus, Threading};
274
275    let mut counters = unsafe { std::mem::zeroed::<ProcessStatus::PROCESS_MEMORY_COUNTERS>() };
276    counters.cb = size_of::<ProcessStatus::PROCESS_MEMORY_COUNTERS>() as u32;
277    // SAFETY: the struct is filled only on success, and says its own size.
278    let read = unsafe {
279        ProcessStatus::GetProcessMemoryInfo(
280            Threading::GetCurrentProcess(),
281            &mut counters,
282            counters.cb,
283        )
284    };
285    (read != 0).then_some(counters.WorkingSetSize as u64)
286}
287
288#[cfg(not(any(target_os = "macos", target_os = "windows")))]
289fn memory() -> Option<u64> {
290    None
291}