use std::sync::{Mutex, OnceLock};
use std::time::Duration;
struct Marks {
window: Option<Duration>,
gpu: Option<(Duration, frus_gpu::StartupTimes)>,
laid_out: Option<Duration>,
reported: bool,
}
static MARKS: Mutex<Marks> = Mutex::new(Marks {
window: None,
gpu: None,
laid_out: None,
reported: false,
});
static BEGUN: OnceLock<web_time::Instant> = OnceLock::new();
pub(crate) fn begin() {
BEGUN.get_or_init(web_time::Instant::now);
}
pub(crate) fn window_made() {
if let Ok(mut marks) = MARKS.lock() {
marks.window.get_or_insert_with(since_launch);
}
}
pub(crate) fn renderer_made(times: frus_gpu::StartupTimes) {
if let Ok(mut marks) = MARKS.lock() {
marks.gpu.get_or_insert_with(|| (since_launch(), times));
}
}
pub(crate) fn laid_out() {
if let Ok(mut marks) = MARKS.lock() {
marks.laid_out.get_or_insert_with(since_launch);
}
}
pub(crate) fn presented() {
let Ok(mut marks) = MARKS.lock() else {
return;
};
if marks.reported {
return;
}
marks.reported = true;
let now = since_launch();
log::info!("{}", report(now, &marks, frus_text::font_loading()));
}
fn report(at: Duration, marks: &Marks, fonts: (Duration, u32)) -> String {
let ms = |d: Duration| d.as_millis();
let mut parts = Vec::new();
if let Some(window) = marks.window {
parts.push(format!("window at {} ms", ms(window)));
}
if let Some((ready, times)) = marks.gpu {
parts.push(format!(
"GPU ready at {} ms (adapter {} ms, device {} ms, pipelines {} ms, warm-up {} ms)",
ms(ready),
ms(times.adapter),
ms(times.device),
ms(times.pipelines),
ms(times.warm_up)
));
}
if fonts.1 > 0 {
parts.push(format!(
"fonts {} ms ({} load{})",
ms(fonts.0),
fonts.1,
if fonts.1 == 1 { "" } else { "s" }
));
}
if let Some(laid_out) = marks.laid_out {
parts.push(format!("laid out at {} ms", ms(laid_out)));
}
format!(
"first frame at {} ms after launch — {}",
ms(at),
parts.join("; ")
)
}
fn since_launch() -> Duration {
process_age().unwrap_or_else(|| BEGUN.get_or_init(web_time::Instant::now).elapsed())
}
#[cfg(target_arch = "wasm32")]
fn process_age() -> Option<Duration> {
let now = web_sys::window()?.performance()?.now();
Some(Duration::from_secs_f64(now / 1000.0))
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn process_age() -> Option<Duration> {
let stat = std::fs::read_to_string("/proc/self/stat").ok()?;
let uptime = std::fs::read_to_string("/proc/uptime").ok()?;
age_from_proc(&stat, &uptime)
}
#[cfg(any(target_os = "linux", target_os = "android", test))]
fn age_from_proc(stat: &str, uptime: &str) -> Option<Duration> {
let after_command = &stat[stat.rfind(')')? + 1..];
let started_ticks: f64 = after_command.split_whitespace().nth(19)?.parse().ok()?;
let up: f64 = uptime.split_whitespace().next()?.parse().ok()?;
let age = up - started_ticks / 100.0;
(age >= 0.0).then(|| Duration::from_secs_f64(age))
}
#[cfg(windows)]
fn process_age() -> Option<Duration> {
use windows_sys::Win32::Foundation::FILETIME;
use windows_sys::Win32::System::SystemInformation::GetSystemTimeAsFileTime;
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes};
let ticks = |t: FILETIME| (u64::from(t.dwHighDateTime) << 32) | u64::from(t.dwLowDateTime);
let zero = FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
let (mut created, mut exited, mut kernel, mut user, mut now) = (zero, zero, zero, zero, zero);
let found = unsafe {
GetProcessTimes(
GetCurrentProcess(),
&mut created,
&mut exited,
&mut kernel,
&mut user,
)
};
if found == 0 {
return None;
}
unsafe { GetSystemTimeAsFileTime(&mut now) };
let age = ticks(now).checked_sub(ticks(created))?;
Some(Duration::from_nanos(age * 100))
}
#[cfg(not(any(
target_arch = "wasm32",
target_os = "linux",
target_os = "android",
windows
)))]
fn process_age() -> Option<Duration> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_process_age_is_read_from_proc() {
let stat = "4242 (my app (x)) S 1 4242 4242 0 -1 4194560 100 0 0 0 1 2 0 0 20 0 3 0 \
123456 1000 50";
let age = age_from_proc(stat, "1235.06 4000.00\n").unwrap();
assert!((age.as_secs_f64() - 0.5).abs() < 1e-6, "{age:?}");
assert_eq!(age_from_proc("garbage", "1.0"), None);
}
#[test]
fn the_report_names_each_step() {
let marks = Marks {
window: Some(Duration::from_millis(35)),
gpu: Some((
Duration::from_millis(245),
frus_gpu::StartupTimes {
adapter: Duration::from_millis(40),
device: Duration::from_millis(60),
pipelines: Duration::from_millis(20),
warm_up: Duration::from_millis(90),
},
)),
laid_out: Some(Duration::from_millis(290)),
reported: false,
};
assert_eq!(
report(
Duration::from_millis(312),
&marks,
(Duration::from_millis(80), 2)
),
"first frame at 312 ms after launch — window at 35 ms; GPU ready at 245 ms \
(adapter 40 ms, device 60 ms, pipelines 20 ms, warm-up 90 ms); fonts 80 ms \
(2 loads); laid out at 290 ms"
);
}
}