1use 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
33const TICK: Duration = Duration::from_millis(500);
36
37const LEASE: Duration = Duration::from_secs(1);
39
40pub const WIDTH: f32 = 148.0;
43
44const VALUE_WIDTH: f32 = 64.0;
47
48pub struct Stats {
51 painter: Painter,
52 frames: u32,
54 since: Instant,
55 cpu_since: Option<Duration>,
57 gpu_since: Option<Duration>,
59 reading: Reading,
61}
62
63#[derive(Clone, Copy, Default)]
64struct Reading {
65 fps: f32,
66 cpu: Option<f32>,
68 gpu: Option<f32>,
71 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 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 |mem| format!("{:.0} MB", mem as f32 / 1e6),
155 ),
156 ));
157
158 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#[cfg(unix)]
193fn cpu_time() -> Option<Duration> {
194 let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
195 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#[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 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#[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 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#[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 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}