use std::time::Duration;
use gpui::{
Context, IntoElement, ParentElement as _, Render, SharedString, Styled as _, Window, div, px,
};
use motion::Painter;
use theme::{Glass, SurfaceStyle, TextStyle, Theme, Typeset};
use web_time::Instant;
use crate::{popover, surface::Surfaced as _};
const TICK: Duration = Duration::from_millis(500);
const LEASE: Duration = Duration::from_secs(1);
pub const WIDTH: f32 = 148.0;
const VALUE_WIDTH: f32 = 64.0;
pub struct Stats {
painter: Painter,
frames: u32,
since: Instant,
cpu_since: Option<Duration>,
gpu_since: Option<Duration>,
reading: Reading,
}
#[derive(Clone, Copy, Default)]
struct Reading {
fps: f32,
cpu: Option<f32>,
gpu: Option<f32>,
mem: Option<u64>,
}
impl Stats {
pub fn new(cx: &mut Context<Self>) -> Self {
Self {
painter: Painter::of(cx),
frames: 0,
since: Instant::now(),
cpu_since: cpu_time(),
gpu_since: None,
reading: Reading::default(),
}
}
}
impl Render for Stats {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !self.painter.woken(cx) {
self.frames += 1;
}
let now = Instant::now();
let elapsed = now.duration_since(self.since);
if elapsed >= TICK {
let share = |then: Duration, now: Duration| {
now.saturating_sub(then).as_secs_f32() / elapsed.as_secs_f32() * 100.0
};
let (cpu, gpu) = (cpu_time(), window.gpu_time());
self.reading = Reading {
fps: self.frames as f32 / elapsed.as_secs_f32(),
cpu: self.cpu_since.zip(cpu).map(|(then, cpu)| share(then, cpu)),
gpu: self.gpu_since.zip(gpu).map(|(then, gpu)| share(then, gpu)),
mem: memory(),
};
self.frames = 0;
self.since = now;
self.cpu_since = cpu;
self.gpu_since = gpu;
}
self.painter.lease(1.0 / TICK.as_secs_f32(), LEASE, cx);
let theme = Theme {
popover_surface: SurfaceStyle::Glass(Glass::Regular),
..Theme::of(cx).clone()
};
let reading = self.reading;
let card = popover::popover_card(&theme)
.w(px(WIDTH))
.p(px(10.0))
.flex()
.flex_col()
.gap(px(2.0))
.child(row(&theme, "FPS", format!("{:.0}", reading.fps)))
.child(row(
&theme,
"CPU",
reading
.cpu
.map_or_else(|| "—".to_string(), |cpu| format!("{cpu:.1}%")),
))
.child(row(
&theme,
"GPU",
reading
.gpu
.map_or_else(|| "—".to_string(), |gpu| format!("{gpu:.1}%")),
))
.child(row(
&theme,
"MEM",
reading.mem.map_or_else(
|| "—".to_string(),
|mem| format!("{:.0} MB", mem as f32 / 1e6),
),
));
card.surface(&theme, theme.popover_surface)
}
}
fn row(theme: &Theme, label: &'static str, value: String) -> impl IntoElement {
div()
.flex()
.flex_row()
.items_center()
.justify_between()
.gap(px(10.0))
.child(
div()
.text_style(TextStyle::Subheadline)
.text_color(theme.text_faint)
.child(label),
)
.child(
div()
.w(px(VALUE_WIDTH))
.text_right()
.font_family(theme.font_mono.clone())
.text_style(TextStyle::Body)
.text_color(theme.text)
.child(SharedString::from(value)),
)
}
#[cfg(unix)]
fn cpu_time() -> Option<Duration> {
let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
let usage = unsafe {
if libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) != 0 {
return None;
}
usage.assume_init()
};
let spent = |time: libc::timeval| {
Duration::from_secs(time.tv_sec as u64) + Duration::from_micros(time.tv_usec as u64)
};
Some(spent(usage.ru_utime) + spent(usage.ru_stime))
}
#[cfg(not(unix))]
fn cpu_time() -> Option<Duration> {
None
}
#[cfg(target_os = "macos")]
#[allow(deprecated)]
fn memory() -> Option<u64> {
let mut info = std::mem::MaybeUninit::<libc::mach_task_basic_info>::uninit();
let mut count = libc::MACH_TASK_BASIC_INFO_COUNT;
let info = unsafe {
if libc::task_info(
libc::mach_task_self(),
libc::MACH_TASK_BASIC_INFO,
info.as_mut_ptr().cast(),
&mut count,
) != libc::KERN_SUCCESS
{
return None;
}
info.assume_init()
};
Some(info.resident_size)
}
#[cfg(not(target_os = "macos"))]
fn memory() -> Option<u64> {
None
}