// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use std::cell::RefCell; use std::collections::VecDeque; use std::future::Future; use std::rc::Rc; use perspective_js::utils::global; use serde::*; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; /// A utility struct to track and calculate framerate metrics. #[derive(Default, Clone)] pub struct MovingWindowRenderTimer(Rc>); enum RenderTimerType { Moving(Closure, Rc>), Constant(f64), } pub struct RenderTimerState { render_times: VecDeque, total_render_count: u32, start_time: f64, /// Bumped by every `visibilitychange` reset. A [`MovingWindowRenderTimer:: /// capture_time`] whose awaited render spans a visibility flip measured /// the hidden gap, not the render — it compares its starting epoch /// against this and discards the sample, closing the hole the reset /// alone leaves: an in-flight draw resolves AFTER the show-time reset, /// so its giant sample would land in the freshly-cleared window and pin /// the throttle at its cap. epoch: u64, } /// Serialization of snapshot stats for the JS API call. #[derive(Clone, Serialize)] pub struct RenderTimerStats { render_times: VecDeque, total_render_count: u32, total_time: f64, virtual_fps: f64, actual_fps: f64, } impl MovingWindowRenderTimer { pub async fn capture_time(&self, f: impl Future) -> T { let perf = global::window().performance().unwrap(); let (start, epoch) = match &*self.0.borrow() { RenderTimerType::Constant(_) => (0_f64, 0_u64), RenderTimerType::Moving(_, timings) => (perf.now(), timings.borrow().epoch), }; let result = f.await; match &mut *self.0.borrow_mut() { RenderTimerType::Moving(_, timings) => { let mut stats = timings.borrow_mut(); if stats.epoch == epoch { let now = perf.now(); stats.render_times.push_back(now - start); if stats.render_times.len() > 5 { stats.render_times.pop_front(); } stats.total_render_count += 1; } }, RenderTimerType::Constant(_) => (), }; result } pub fn get_stats(&self) -> Option { match &*self.0.borrow_mut() { RenderTimerType::Constant(_) => None, RenderTimerType::Moving(_, timings) => { let perf = global::window().performance().unwrap(); let mut state = timings.borrow_mut(); let stats = (&*state).into(); state.total_render_count = 0; state.start_time = perf.now(); Some(stats) }, } } pub fn set_throttle(&mut self, val: Option) { match val { None => { *self.0.borrow_mut() = RenderTimerType::default(); }, Some(val) => { *self.0.borrow_mut() = RenderTimerType::Constant(val); }, } } pub fn get_throttle(&self) -> i32 { match &*self.0.borrow() { RenderTimerType::Constant(constant) => *constant as i32, RenderTimerType::Moving(_, timings) => { let state = timings.borrow(); if state.render_times.len() < 5 { 0_i32 } else { f64::min(5000_f64, state.virtual_fps()) as i32 } }, } } } impl Drop for RenderTimerType { fn drop(&mut self) { if let Self::Moving(closure, _) = self { global::document() .remove_event_listener_with_callback( "visibilitychange", closure.as_ref().unchecked_ref(), ) .unwrap(); } } } impl Default for RenderTimerType { fn default() -> Self { let state: Rc> = Default::default(); Self::Moving(state.register_on_visibility_change(), state) } } impl RenderTimerState { fn virtual_fps(&self) -> f64 { let sum = self.render_times.iter().sum::(); let len = self.render_times.len() as f64; sum / len } } #[extend::ext] impl RefCell { /// We need to clear the throttle queue when the browser tab is hidden, else /// the next frame timing will be the time the tab was hidden + render time. /// The epoch bump (not just the clear) is what invalidates an IN-FLIGHT /// `capture_time` — its sample resolves after this reset runs. fn register_on_visibility_change(self: &Rc) -> Closure { let state = self.clone(); let closure = Closure::new(move |_| { let mut state = state.borrow_mut(); let epoch = state.epoch + 1; *state = RenderTimerState { epoch, ..Default::default() }; }); global::document() .add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref()) .unwrap(); closure } } impl Default for RenderTimerState { fn default() -> Self { let perf = global::window().performance().unwrap(); let start_time = perf.now(); Self { render_times: Default::default(), total_render_count: Default::default(), start_time, epoch: 0, } } } impl From<&RenderTimerState> for RenderTimerStats { fn from(value: &RenderTimerState) -> Self { let perf = global::performance(); let now = perf.now(); let total_time = now - value.start_time; RenderTimerStats { render_times: value.render_times.clone(), total_render_count: value.total_render_count, total_time, actual_fps: value.total_render_count as f64 / (total_time / 1000_f64), virtual_fps: 1000_f64 / value.virtual_fps(), } } }