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::Theme;
29use web_time::Instant;
30
31use crate::{
32 material::{self, Frosted as _},
33 popover,
34};
35
36const TICK: Duration = Duration::from_millis(500);
39
40const LEASE: Duration = Duration::from_secs(1);
42
43pub const WIDTH: f32 = 148.0;
46
47const VALUE_WIDTH: f32 = 64.0;
50
51pub struct Stats {
54 painter: Painter,
55 frames: u32,
57 since: Instant,
58 cpu_since: Option<Duration>,
60 gpu_since: Option<Duration>,
62 reading: Reading,
64}
65
66#[derive(Clone, Copy, Default)]
67struct Reading {
68 fps: f32,
69 cpu: Option<f32>,
71 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#[cfg(unix)]
170fn cpu_time() -> Option<Duration> {
171 let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
172 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}