Skip to content

Commit

Permalink
Add render statistics API
Browse files Browse the repository at this point in the history
  • Loading branch information
texodus committed Nov 19, 2023
1 parent 498cb28 commit 82c926f
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 59 deletions.
16 changes: 16 additions & 0 deletions rust/perspective-viewer/src/rust/custom_elements/viewer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsValue> {
Ok(JsValue::from_serde_ext(
&self.renderer.render_timer().get_stats(),
)?)
}

/// Flush any pending modifications to this `<perspective-viewer>`. Since
/// `<perspective-viewer>`'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 {
Expand Down
1 change: 0 additions & 1 deletion rust/perspective-viewer/src/rust/js/tests/perspective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down
6 changes: 3 additions & 3 deletions rust/perspective-viewer/src/rust/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
}

Expand Down
172 changes: 117 additions & 55 deletions rust/perspective-viewer/src/rust/renderer/render_timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RefCell<RenderTimerType>>);

enum RenderTimerType {
Moving(Closure<dyn Fn(JsValue)>, Rc<RefCell<Option<VecDeque<f64>>>>),
Moving(Closure<dyn Fn(JsValue)>, Rc<RefCell<RenderTimerState>>),
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<RefCell<Option<VecDeque<f64>>>> = Default::default();
Self::Moving(register_on_visibility_change(deque.clone()), deque)
}
pub struct RenderTimerState {
render_times: VecDeque<f64>,
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<RefCell<Option<VecDeque<f64>>>>,
) -> Closure<dyn Fn(JsValue)> {
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<f64>,
total_render_count: u32,
total_time: f64,
virtual_fps: f64,
actual_fps: f64,
}

impl MovingWindowRenderTimer {
Expand All @@ -79,22 +58,35 @@ 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(_) => (),
};

result
}

pub fn get_stats(&self) -> Option<RenderTimerStats> {
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<f64>) {
match val {
None => {
Expand All @@ -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::<f64>();
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<RefCell<RenderTimerState>> = Default::default();
Self::Moving(state.register_on_visibility_change(), state)
}
}

impl RenderTimerState {
fn virtual_fps(&self) -> f64 {
let sum = self.render_times.iter().sum::<f64>();
let len = self.render_times.len() as f64;
sum / len
}
}

#[extend::ext]
impl RefCell<RenderTimerState> {
/// 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<Self>) -> Closure<dyn Fn(JsValue)> {
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(),
}
}
}
45 changes: 45 additions & 0 deletions rust/perspective-viewer/src/ts/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

/**
* 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 `<perspective-viewer>`, as well at its
* API. `PerspectiveViewerElement` should not be constructed directly (like its
Expand Down Expand Up @@ -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`,
Expand Down

0 comments on commit 82c926f

Please sign in to comment.