From 82c926f43d9349ec723269114e0b3d51ec8ddd5d Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sat, 18 Nov 2023 21:51:25 -0500 Subject: [PATCH] Add render statistics API --- .../src/rust/custom_elements/viewer.rs | 16 ++ .../src/rust/js/tests/perspective.rs | 1 - rust/perspective-viewer/src/rust/renderer.rs | 6 +- .../src/rust/renderer/render_timer.rs | 172 ++++++++++++------ rust/perspective-viewer/src/ts/viewer.ts | 45 +++++ 5 files changed, 181 insertions(+), 59 deletions(-) diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index 8b3ab72b19..4404846ae3 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -221,6 +221,22 @@ impl PerspectiveViewerElement { }) } + /// Get render statistics. Some fields of the returned stats object are + /// relative to the last time `getRenderStats()` was called, ergo calling + /// this method resets these fields. + #[wasm_bindgen(js_name = "getRenderStats")] + pub fn get_render_stats(&self) -> ApiResult { + Ok(JsValue::from_serde_ext( + &self.renderer.render_timer().get_stats(), + )?) + } + + /// Flush any pending modifications to this ``. Since + /// ``'s API is almost entirely `async`, it may take + /// some milliseconds before any method call such as `restore()` affects + /// the rendered element. If you want to make sure any invoked method which + /// affects the rendered has had its results rendered, call and await + /// `flush()` pub fn flush(&self) -> ApiFuture<()> { clone!(self.renderer, self.session); ApiFuture::new(async move { diff --git a/rust/perspective-viewer/src/rust/js/tests/perspective.rs b/rust/perspective-viewer/src/rust/js/tests/perspective.rs index 0edc760c83..8299a3af4d 100644 --- a/rust/perspective-viewer/src/rust/js/tests/perspective.rs +++ b/rust/perspective-viewer/src/rust/js/tests/perspective.rs @@ -10,7 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_test::*; diff --git a/rust/perspective-viewer/src/rust/renderer.rs b/rust/perspective-viewer/src/rust/renderer.rs index 3c30237eb9..c04a5ec7d6 100644 --- a/rust/perspective-viewer/src/rust/renderer.rs +++ b/rust/perspective-viewer/src/rust/renderer.rs @@ -233,7 +233,7 @@ impl Renderer { let timer = self.render_timer(); draw_mutex .debounce(async { - set_timeout(timer.get_avg()).await?; + set_timeout(timer.get_throttle()).await?; let jsplugin = self.get_active_plugin()?; jsplugin.resize().await?; Ok(()) @@ -263,7 +263,7 @@ impl Renderer { let timer = self.render_timer(); let task = async move { if is_update { - set_timeout(timer.get_avg()).await?; + set_timeout(timer.get_throttle()).await?; } if let Some(view) = session.await?.get_view() { @@ -367,7 +367,7 @@ impl Renderer { self.draw_lock.clone() } - fn render_timer(&self) -> MovingWindowRenderTimer { + pub fn render_timer(&self) -> MovingWindowRenderTimer { self.0.borrow().timer.clone() } diff --git a/rust/perspective-viewer/src/rust/renderer/render_timer.rs b/rust/perspective-viewer/src/rust/renderer/render_timer.rs index 0b504e7f82..8c77022c93 100644 --- a/rust/perspective-viewer/src/rust/renderer/render_timer.rs +++ b/rust/perspective-viewer/src/rust/renderer/render_timer.rs @@ -15,57 +15,36 @@ use std::collections::VecDeque; use std::future::Future; use std::rc::Rc; +use serde::*; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::*; use crate::utils::*; +/// A utility struct to track and calculate framerate metrics. #[derive(Default, Clone)] pub struct MovingWindowRenderTimer(Rc>); enum RenderTimerType { - Moving(Closure, Rc>>>), + Moving(Closure, Rc>), Constant(f64), } -impl Drop for RenderTimerType { - fn drop(&mut self) { - if let Self::Moving(closure, _) = self { - let document = window().unwrap().document().unwrap(); - document - .remove_event_listener_with_callback( - "visibilitychange", - closure.as_ref().unchecked_ref(), - ) - .unwrap(); - } - } -} - -impl Default for RenderTimerType { - fn default() -> Self { - let deque: Rc>>> = Default::default(); - Self::Moving(register_on_visibility_change(deque.clone()), deque) - } +pub struct RenderTimerState { + render_times: VecDeque, + total_render_count: u32, + start_time: f64, } -/// 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. -fn register_on_visibility_change( - deque: Rc>>>, -) -> Closure { - let fun = move |_| { - *deque.borrow_mut() = None; - }; - - let closure = fun.into_closure(); - let document = window().unwrap().document().unwrap(); - document - .add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref()) - .unwrap(); - - closure +/// 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 { @@ -79,15 +58,14 @@ impl MovingWindowRenderTimer { let result = f.await; match &mut *self.0.borrow_mut() { RenderTimerType::Moving(_, timings) => { - let mut timings = timings.borrow_mut(); - if let Some(timings) = &mut *timings { - timings.push_back(perf.now() - start); - if timings.len() > 5 { - timings.pop_front(); - } - } else { - *timings = Some(Default::default()); + let mut stats = timings.borrow_mut(); + 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(_) => (), }; @@ -95,6 +73,20 @@ impl MovingWindowRenderTimer { result } + pub fn get_stats(&self) -> Option { + match &*self.0.borrow_mut() { + RenderTimerType::Constant(_) => None, + RenderTimerType::Moving(_, timings) => { + let perf = window().unwrap().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 => { @@ -106,23 +98,93 @@ impl MovingWindowRenderTimer { } } - pub fn get_avg(&self) -> i32 { + pub fn get_throttle(&self) -> i32 { match &*self.0.borrow() { RenderTimerType::Constant(constant) => *constant as i32, RenderTimerType::Moving(_, timings) => { - if let Some(timings) = &*timings.borrow() { - let len = timings.len(); - if len < 5 { - 0_i32 - } else { - let sum = timings.iter().sum::(); - let avg: f64 = sum / len as f64; - f64::min(5000_f64, avg.floor()) as i32 - } - } else { + 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 { + let document = window().unwrap().document().unwrap(); + 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. + fn register_on_visibility_change(self: &Rc) -> Closure { + let state = self.clone(); + let fun = move |_| { + *state.borrow_mut() = Default::default(); + }; + + let closure = fun.into_closure(); + let document = window().unwrap().document().unwrap(); + document + .add_event_listener_with_callback("visibilitychange", closure.as_ref().unchecked_ref()) + .unwrap(); + + closure + } +} + +impl Default for RenderTimerState { + fn default() -> Self { + let perf = window().unwrap().performance().unwrap(); + let start_time = perf.now(); + Self { + render_times: Default::default(), + total_render_count: Default::default(), + start_time, + } + } +} + +impl From<&RenderTimerState> for RenderTimerStats { + fn from(value: &RenderTimerState) -> Self { + let perf = window().unwrap().performance().unwrap(); + 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(), + } + } +} diff --git a/rust/perspective-viewer/src/ts/viewer.ts b/rust/perspective-viewer/src/ts/viewer.ts index a120cfd9d9..295966a4f2 100644 --- a/rust/perspective-viewer/src/ts/viewer.ts +++ b/rust/perspective-viewer/src/ts/viewer.ts @@ -18,6 +18,37 @@ export type PerspectiveViewerConfig = perspective.ViewConfig & { plugin_config?: any; }; +export type RenderStats = { + /** + * The most recent N render times (default 5) + */ + render_times: Array; + + /** + * Number of plugin renders since the last time `getRenderStats()` was + * called. + */ + total_render_count: number; + + /** + * Time since last `getRenderStats()` call (in milliseconds). + */ + total_time: number; + + /** + * Estimated max framerate (in frames per second) of _just_ plugin draw + * calls (e.g. how many frames could be drawn if there was no engine wait + * time). + */ + virtual_fps: number; + + /** + * Actual framerate (in frames per second) since last `getRenderStats()` + * call. + */ + actual_fps: number; +}; + /** * The Custom Elements implementation for ``, as well at its * API. `PerspectiveViewerElement` should not be constructed directly (like its @@ -373,6 +404,20 @@ export interface IPerspectiveViewerElement { */ getEditPort(): number; + /** + * Get render statistics since the last time `getRenderStats()` was called. + * + * @category Util + * @returns A `RenderStats` statistics struct. + * @example + * ```javascript + * const viewer = document.querySelector("perspective-viewer"); + * const stats = viewer.getRenderStats(); + * console.log(stats.virtual_fps); + * ``` + */ + getRenderStats(): RenderStats; + /** * Determines the render throttling behavior. Can be an integer, for * millisecond window to throttle render event; or, if `undefined`,