Allow collection of EnsoGL stats when Monitor panel is not visible (#3260)

This change makes EnsoGL runtime stats be always collected, even when EnsoGL `Monitor` panel is not visible. Those stats are intended to be used in the future by a profiling framework.

**Performance impact:** Continuous collection of stats introduces an overhead of two Web Performance API `now()` calls in each frame of the main rendering loop, plus a small number of simple arithmetic calculations. This is assumed to be a negligible and acceptable overhead.

#### Visuals



A screenshot of the Monitor panel in full `ide` after applying the PR, taken in IDE built with `./run dist`:


<img width="991" alt="Screenshot 2022-02-14 at 16 11 42" src="https://user-images.githubusercontent.com/273837/153891378-8a2fb333-34ce-46ce-99df-7d796817310c.png">

A recording, also in IDE built with `./run dist`; note that FPS is impacted by the act of recording itself:





https://user-images.githubusercontent.com/273837/154104016-49a12e23-1210-4477-9743-ec1611e5b4ed.mov




https://www.pivotaltracker.com/story/show/181093601

# Important Notes
- Responsibility for controlling how `Stats` gathering and calculation is performed at various points in the main rendering loop was removed from `Monitor` - the `Monitor`'s purpose is only to display existing data, it should not influence how the data is collected.
- Two previously existing distinct `Monitor` structs were merged into one, to avoid confusion; after previous refactorings, the remaining `stats::Monitor` did not have much useful code anyway.
- In `stats` package, refactoring was done, to make `StatsData` a "dumb", data-only type, and to move the logic related to stats collection and frame tracking to other helper types.


[ci no changelog needed]
This commit is contained in:
Mateusz Czapliński 2022-02-21 13:38:45 +01:00 committed by GitHub
parent 964645fddf
commit b01217aa69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 303 additions and 291 deletions

View File

@ -749,9 +749,9 @@
"integrity": "sha512-dZMzN0uAjwJXWYYAcnxIwXqRTZw3o14hGe7O6uhwjD1ZQWPVYA5lASgnNskEBra0knVBsOXB4KXg+HnlKewN/A=="
},
"node_modules/@grpc/grpc-js": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.4.tgz",
"integrity": "sha512-+nJTOsqpFAXnfFrMZ7Too4XXZ/J9O+8jYvSoaunupoC7I7b9H4iex1BRsbTdOmiowfPGJrWit7jUPmbENSUSpw==",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.5.tgz",
"integrity": "sha512-FTd27ItHlsSG/7hp62xgI9YnqSwRbHRSVmDVR8DwOoC+6t8JhHRXe2JL0U8N9GLc0jS0HrtEbO/KP5+G0ebjLQ==",
"dependencies": {
"@grpc/proto-loader": "^0.6.4",
"@types/node": ">=12.12.47"
@ -4871,12 +4871,15 @@
}
},
"node_modules/color": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.0.tgz",
"integrity": "sha512-hHTcrbvEnGjC7WBMk6ibQWFVDgEFTVmjrz2Q5HlU6ltwxv0JJN2Z8I7uRbWeQLF04dikxs8zgyZkazRJvSMtyQ==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.1.tgz",
"integrity": "sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
@ -19551,9 +19554,9 @@
"integrity": "sha512-dZMzN0uAjwJXWYYAcnxIwXqRTZw3o14hGe7O6uhwjD1ZQWPVYA5lASgnNskEBra0knVBsOXB4KXg+HnlKewN/A=="
},
"@grpc/grpc-js": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.4.tgz",
"integrity": "sha512-+nJTOsqpFAXnfFrMZ7Too4XXZ/J9O+8jYvSoaunupoC7I7b9H4iex1BRsbTdOmiowfPGJrWit7jUPmbENSUSpw==",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.5.tgz",
"integrity": "sha512-FTd27ItHlsSG/7hp62xgI9YnqSwRbHRSVmDVR8DwOoC+6t8JhHRXe2JL0U8N9GLc0jS0HrtEbO/KP5+G0ebjLQ==",
"requires": {
"@grpc/proto-loader": "^0.6.4",
"@types/node": ">=12.12.47"
@ -22929,9 +22932,9 @@
}
},
"color": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.0.tgz",
"integrity": "sha512-hHTcrbvEnGjC7WBMk6ibQWFVDgEFTVmjrz2Q5HlU6ltwxv0JJN2Z8I7uRbWeQLF04dikxs8zgyZkazRJvSMtyQ==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.1.tgz",
"integrity": "sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw==",
"requires": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"

View File

@ -1,4 +1,4 @@
//! This module implements performance monitoring utils.
//! This module implements the stats monitor view, which can be visible on the screen in debug mode.
use crate::prelude::*;
@ -198,9 +198,59 @@ impl Drop for DomData {
// === Monitor ===
// ===============
/// Implementation of the monitoring panel.
#[derive(Debug)]
/// Visual panel showing performance-related statistics.
#[derive(Debug, Clone, CloneRef)]
pub struct Monitor {
renderer: Rc<RefCell<Renderer>>,
}
impl Default for Monitor {
fn default() -> Self {
let mut renderer = Renderer::new();
renderer.add::<FrameTime>();
renderer.add::<Fps>();
renderer.add::<WasmMemory>();
renderer.add::<GpuMemoryUsage>();
renderer.add::<DrawCallCount>();
renderer.add::<DataUploadCount>();
renderer.add::<DataUploadSize>();
renderer.add::<BufferCount>();
renderer.add::<SymbolCount>();
renderer.add::<ShaderCount>();
renderer.add::<ShaderCompileCount>();
renderer.add::<SpriteSystemCount>();
renderer.add::<SpriteCount>();
Self { renderer: Rc::new(RefCell::new(renderer)) }
}
}
impl Monitor {
/// Constructor.
pub fn new() -> Self {
Self::default()
}
/// Draw the monitor and update its graphs based on the provided stats data.
/// Does nothing if the monitor is not visible (see: [`toggle()`]).
pub fn sample_and_draw(&self, stats: &StatsData) {
self.renderer.borrow_mut().sample_and_draw(stats);
}
/// Toggle the visibility of the monitor.
pub fn toggle(&self) {
self.renderer.borrow_mut().toggle();
}
}
// ================
// === Renderer ===
// ================
/// Code responsible for drawing [`Monitor`]'s data.
#[derive(Debug)]
struct Renderer {
user_config: Config,
config: SamplerConfig,
width: f64,
@ -210,11 +260,8 @@ pub struct Monitor {
first_draw: bool,
}
// === Public API ===
impl Default for Monitor {
fn default() -> Self {
impl Renderer {
fn new() -> Self {
let user_config = Config::default();
let panels = default();
let width = default();
@ -226,35 +273,21 @@ impl Default for Monitor {
out.update_config();
out
}
}
impl Monitor {
/// Cnstructor.
pub fn new() -> Self {
default()
}
/// Modify the Monitor's config and update the view.
pub fn mod_config<F: FnOnce(&mut Config)>(&mut self, f: F) {
f(&mut self.user_config);
self.update_config();
}
/// Add new display element.
pub fn add<S: Sampler + Default + 'static>(&mut self) -> Panel {
fn add<S: Sampler + Default + 'static>(&mut self) {
let panel = Panel::new(self.config.clone(), S::default());
self.panels.push(panel.clone());
self.panels.push(panel);
self.resize();
panel
}
/// Check whether the mointor is visible.
pub fn visible(&self) -> bool {
/// Check whether the monitor is visible.
fn visible(&self) -> bool {
self.dom.is_some()
}
/// Show the monitor and add it's DOM to the scene.
pub fn show(&mut self) {
fn show(&mut self) {
if !self.visible() {
self.first_draw = true;
self.dom = Some(Dom::new());
@ -263,12 +296,12 @@ impl Monitor {
}
/// Hides the monitor and remove it's DOM from the scene.
pub fn hide(&mut self) {
fn hide(&mut self) {
self.dom = None;
}
/// Toggle the visibility of the monitor.
pub fn toggle(&mut self) {
fn toggle(&mut self) {
if self.visible() {
self.hide();
} else {
@ -276,8 +309,17 @@ impl Monitor {
}
}
/// Draw the Monitor and update all of it's values.
pub fn draw(&mut self) {
fn sample_and_draw(&mut self, stats: &StatsData) {
if self.visible() {
for panel in &self.panels {
panel.sample_and_postprocess(stats);
}
self.draw();
}
}
/// Draw the widget and update all of the graphs.
fn draw(&mut self) {
if let Some(dom) = self.dom.clone() {
if self.first_draw {
self.first_draw = false;
@ -288,12 +330,7 @@ impl Monitor {
self.draw_plots(&dom);
}
}
}
// === Private API ===
impl Monitor {
fn update_config(&mut self) {
self.config = self.user_config.to_js_config()
}
@ -743,15 +780,9 @@ macro_rules! stats_sampler {
$precision
}
fn check(&self, stats: &StatsData) -> ValueCheck {
// Before the first frame, all stat values will be 0, which in (only) this case is
// correct.
if stats.frame_counter == 0 {
ValueCheck::Correct
} else {
let value = self.value(&stats);
ValueCheck::from_threshold($warn_threshold, $err_threshold, value)
}
}
fn max_value(&self) -> Option<f64> {
$max_value
}
@ -786,33 +817,91 @@ stats_sampler!(
#[cfg(test)]
mod tests {
use super::*;
use enso_prelude::*;
use crate::debug::stats::Stats;
use crate::debug::stats::StatsWithTimeProvider;
use enso_web::TimeProvider;
use std::ops::AddAssign;
use assert_approx_eq::assert_approx_eq;
#[derive(Default)]
struct TestSampler<S: Sampler> {
stats: Stats,
sampler: S,
t: f64,
// === MockTimeProvider ===
#[derive(Default, Clone)]
struct MockTimeProvider {
t: Rc<RefCell<f64>>,
}
impl TimeProvider for MockTimeProvider {
fn now(&self) -> f64 {
*self.t.borrow()
}
}
impl AddAssign<f64> for MockTimeProvider {
fn add_assign(&mut self, dt: f64) {
*self.t.borrow_mut() += dt;
}
}
// === TestSampler ===
struct TestSampler<S> {
stats: StatsWithTimeProvider<MockTimeProvider>,
sampler: S,
t: MockTimeProvider,
}
impl<S: Sampler + Default> Default for TestSampler<S> {
fn default() -> Self {
let t: MockTimeProvider = default();
let stats = StatsWithTimeProvider::new(t.clone());
let sampler = default();
Self { stats, sampler, t }
}
}
const STAT_VALUE_COMPARISON_PRECISION: f64 = 0.001;
macro_rules! test_and_advance_frame {
($test:expr, $expected_value:expr, $expected_check:path
; next: $frame_time:expr, $post_frame_delay:expr) => {
let prev_frame_stats = $test.stats.begin_frame($test.t);
let prev_frame_stats = $test.stats.begin_frame();
if let Some(prev_frame_stats) = prev_frame_stats {
let tested_value = $test.sampler.value(&prev_frame_stats);
let tested_check = $test.sampler.check(&prev_frame_stats);
assert_approx_eq!(tested_value, $expected_value, 0.001);
assert!(matches!(tested_check, $expected_check));
assert_approx_eq!(tested_value, $expected_value, STAT_VALUE_COMPARISON_PRECISION);
let mismatch_msg = iformat!(
"Stat check was expected to return: " $expected_check;?
", but got: " tested_check;? " instead.");
assert!(matches!(tested_check, $expected_check), "{}", mismatch_msg);
} else {
assert!(false,
"Expected previous frame's stats to be returned by begin_frame(), \
but got none.");
}
$test.t += $frame_time;
$test.stats.end_frame($test.t);
$test.stats.end_frame();
$test.t += $post_frame_delay;
};
($test:expr, None; next: $frame_time:expr, $post_frame_delay:expr) => {
let prev_frame_stats = $test.stats.begin_frame();
let mismatch_msg = iformat!(
"Expected no stats to be returned by begin_frame(), but got: "
prev_frame_stats;? " instead.");
assert!(matches!(prev_frame_stats, None), "{}", mismatch_msg);
$test.t += $frame_time;
$test.stats.end_frame();
$test.t += $post_frame_delay;
};
}
// === Tests ===
#[test]
fn frame_time() {
// Note: 60 FPS means there's 16.6(6) ms budget for 1 frame. The test will be written under
@ -823,10 +912,8 @@ mod tests {
// Frame 1: simulate we managed to complete the work in 10ms, and then we wait 6ms before
// starting next frame.
//
// Note: there is no frame before "Frame 1", so the tested value for the previous frame
// will always be 0.0 and "Correct" at this point (which is a special case - depending on
// tested stat, for later frames 0.0 could result in a threshold warning/error).
test_and_advance_frame!(test, 0.0, ValueCheck::Correct; next: 10.0, 6.0);
// Note: we expect no stats data to be returned before the 1st frame was started.
test_and_advance_frame!(test, None; next: 10.0, 6.0);
// Frame 2: simulate we managed to complete the work in 5ms, and then we wait 11ms before
// starting next frame.
@ -855,11 +942,8 @@ mod tests {
// Frame 1: simulate we managed to complete the work in 10ms, and then we wait 6ms before
// starting next frame.
//
// Note: there is no earlier frame before "Frame 1", so the tested value for the previous
// frame will always be 0.0 and [`Correct`] at this point (which is a special case -
// depending on tested stat, for later frames 0.0 could result in a threshold
// warning/error).
test_and_advance_frame!(test, 0.0, ValueCheck::Correct; next: 10.0, 6.0);
// Note: we expect no stats data to be returned before the 1st frame was started.
test_and_advance_frame!(test, None; next: 10.0, 6.0);
// Frame 2: simulate we managed to complete the work in 5ms, and then we wait 11.67ms before
// starting next frame.

View File

@ -15,6 +15,8 @@
use enso_prelude::*;
use enso_web::Performance;
use enso_web::TimeProvider;
use js_sys::ArrayBuffer;
use js_sys::WebAssembly::Memory;
use wasm_bindgen::JsCast;
@ -25,41 +27,120 @@ use wasm_bindgen::JsCast;
// === Stats ===
// =============
/// Structure containing all the gathered stats.
#[derive(Debug, Clone, CloneRef)]
pub struct Stats {
rc: Rc<RefCell<StatsData>>,
/// Contains all the gathered stats, and provides methods for modifying and retrieving their
/// values. Uses the Web Performance API to access current time for calculating time-dependent
/// stats (e.g. FPS).
pub type Stats = StatsWithTimeProvider<Performance>;
// =============================
// === StatsWithTimeProvider ===
// =============================
/// Contains all the gathered stats, and provides methods for modifying and retrieving their
/// values.
/// Uses [`T`] to access current time for calculating time-dependent stats (e.g. FPS).
#[derive(Debug, CloneRef)]
pub struct StatsWithTimeProvider<T> {
rc: Rc<RefCell<FramedStatsData<T>>>,
}
impl Default for Stats {
fn default() -> Self {
let rc = Rc::new(RefCell::new(default()));
Self { rc }
impl<T> Clone for StatsWithTimeProvider<T> {
fn clone(&self) -> Self {
Self { rc: self.rc.clone() }
}
}
impl Stats {
impl<T: TimeProvider> StatsWithTimeProvider<T> {
/// Constructor.
pub fn new(time_provider: T) -> Self {
let framed_stats_data = FramedStatsData::new(time_provider);
let rc = Rc::new(RefCell::new(framed_stats_data));
Self { rc }
}
/// Starts tracking data for a new animation frame.
/// Also, calculates the `fps` stat and updates `frame_counter`.
/// Also, calculates the [`fps`] stat.
/// Returns a snapshot of statistics data for the previous frame.
/// Note: on first ever frame, there was no "previous frame", so all returned stats are zero
/// (this special case can be recognized by checking `frame_counter == 0`).
pub fn begin_frame(&self, time: f64) -> StatsData {
self.rc.borrow_mut().begin_frame(time)
///
/// Note: the code works under an assumption that [`begin_frame()`] and [`end_frame()`] are
/// called, respectively, at the beginning and end of every frame. The very first time
/// [`begin_frame()`] is called, it returns `None`, because it does not have complete
/// statistics data for the preceding frame.
pub fn begin_frame(&self) -> Option<StatsData> {
self.rc.borrow_mut().begin_frame()
}
/// Ends tracking data for the current animation frame.
/// Also, calculates the `frame_time` and `wasm_memory_usage` stats.
pub fn end_frame(&self, time: f64) {
self.rc.borrow_mut().end_frame(time);
}
/// Resets the per-frame statistics.
pub fn reset_per_frame_statistics(&self) {
self.rc.borrow_mut().reset_per_frame_statistics();
pub fn end_frame(&self) {
self.rc.borrow_mut().end_frame();
}
}
// =======================
// === FramedStatsData ===
// =======================
#[derive(Debug)]
struct FramedStatsData<T> {
time_provider: T,
stats_data: StatsData,
frame_begin_time: Option<f64>,
}
impl<T: TimeProvider> FramedStatsData<T> {
/// Constructor.
fn new(time_provider: T) -> Self {
let stats_data = default();
let frame_begin_time = None;
Self { time_provider, stats_data, frame_begin_time }
}
fn begin_frame(&mut self) -> Option<StatsData> {
let time = self.time_provider.now();
let mut previous_frame_stats = self.stats_data;
self.reset_per_frame_statistics();
let previous_frame_begin_time = self.frame_begin_time.replace(time);
previous_frame_begin_time.map(|begin_time| {
let end_time = time;
previous_frame_stats.fps = 1000.0 / (end_time - begin_time);
previous_frame_stats
})
}
fn end_frame(&mut self) {
if let Some(begin_time) = self.frame_begin_time {
let end_time = self.time_provider.now();
self.stats_data.frame_time = end_time - begin_time;
}
// TODO[MC,IB]: drop the `cfg!` (outlier in our codebase) once wasm_bindgen::memory()
// doesn't panic in non-WASM builds (https://www.pivotaltracker.com/story/show/180978631)
if cfg!(target_arch = "wasm32") {
let memory: Memory = wasm_bindgen::memory().dyn_into().unwrap();
let buffer: ArrayBuffer = memory.buffer().dyn_into().unwrap();
self.stats_data.wasm_memory_usage = buffer.byte_length();
}
}
fn reset_per_frame_statistics(&mut self) {
self.stats_data.draw_call_count = 0;
self.stats_data.shader_compile_count = 0;
self.stats_data.data_upload_count = 0;
self.stats_data.data_upload_size = 0;
}
}
// ========================
// === Macro gen_stats! ===
// ========================
/// Emits the 2nd argument only if the 1st argument is an integer type. A helper macro for
/// gen_stats!, supports only the types currently used with gen_stats!.
macro_rules! emit_if_integer {
@ -68,6 +149,8 @@ macro_rules! emit_if_integer {
(f64, $($block:tt)*) => ();
}
/// Emits the StatsData struct, and extends StatsWithTimeProvider with accessors to StatsData
/// fields.
macro_rules! gen_stats {
($($field:ident : $field_type:ty),* $(,)?) => { paste::item! {
@ -78,23 +161,21 @@ macro_rules! gen_stats {
#[derive(Debug,Default,Clone,Copy)]
#[allow(missing_docs)]
pub struct StatsData {
frame_begin_time: f64,
pub frame_counter: u64,
$(pub $field : $field_type),*
}
// === Stats fields accessors ===
// === StatsWithTimeProvider fields accessors ===
impl Stats { $(
impl<T: TimeProvider> StatsWithTimeProvider<T> { $(
/// Field getter.
pub fn $field(&self) -> $field_type {
self.rc.borrow().$field
self.rc.borrow().stats_data.$field
}
/// Field setter.
pub fn [<set _ $field>](&self, value:$field_type) {
self.rc.borrow_mut().$field = value;
self.rc.borrow_mut().stats_data.$field = value;
}
/// Field modifier.
@ -137,44 +218,6 @@ gen_stats! {
shader_compile_count : usize,
}
// === StatsData methods ===
impl StatsData {
fn begin_frame(&mut self, time: f64) -> StatsData {
// See [Stats::begin_frame()] docs for explanation of this check.
let previous_frame_snapshot = if self.frame_counter == 0 {
default()
} else {
let end_time = time;
self.fps = 1000.0 / (end_time - self.frame_begin_time);
*self
};
self.frame_counter += 1;
self.frame_begin_time = time;
previous_frame_snapshot
}
fn end_frame(&mut self, time: f64) {
self.frame_time = time - self.frame_begin_time;
// TODO[MC,IB]: drop the `cfg!` (outlier in our codebase) once wasm_bindgen::memory()
// doesn't panic in non-WASM builds (https://www.pivotaltracker.com/story/show/180978631)
if cfg!(target_arch = "wasm32") {
let memory: Memory = wasm_bindgen::memory().dyn_into().unwrap();
let buffer: ArrayBuffer = memory.buffer().dyn_into().unwrap();
self.wasm_memory_usage = buffer.byte_length();
}
}
fn reset_per_frame_statistics(&mut self) {
self.draw_call_count = 0;
self.shader_compile_count = 0;
self.data_upload_count = 0;
self.data_upload_size = 0;
}
}
/// Keeps the body if the `statistics` compilation flag was enabled.
#[macro_export]
macro_rules! if_compiled_with_stats {

View File

@ -1,8 +1,6 @@
//! This module implements `World`, the main object responsible for handling what you see on the
//! screen.
pub mod stats;
pub use crate::display::symbol::types::*;
use crate::prelude::*;
@ -11,6 +9,7 @@ use crate::animation;
use crate::control::callback;
use crate::data::dirty;
use crate::data::dirty::traits::*;
use crate::debug;
use crate::debug::stats::Stats;
use crate::debug::stats::StatsData;
use crate::display;
@ -63,7 +62,8 @@ pub struct World {
main_loop: animation::DynamicLoop,
uniforms: Uniforms,
stats: Stats,
stats_monitor: stats::Monitor,
stats_monitor: debug::monitor::Monitor,
stats_draw_handle: callback::Handle,
main_loop_frame: callback::Handle,
on_before_frame: callback::SharedRegistryMut1<animation::TimeInfo>,
on_after_frame: callback::SharedRegistryMut1<animation::TimeInfo>,
@ -75,24 +75,27 @@ impl World {
#[allow(clippy::new_ret_no_self)]
pub fn new(dom: &web_sys::HtmlElement) -> World {
let logger = Logger::new("world");
let stats = default();
let stats = debug::stats::Stats::new(web::performance());
let scene_dirty = dirty::SharedBool::new(Logger::new_sub(&logger, "scene_dirty"), ());
let on_change = enclose!((scene_dirty) move || scene_dirty.set());
let scene = Scene::new(dom, &logger, &stats, on_change);
let uniforms = Uniforms::new(&scene.variables);
let main_loop = animation::DynamicLoop::new();
let stats_monitor = stats::Monitor::new(&stats);
let stats_monitor = debug::monitor::Monitor::new();
let on_before_frame = <callback::SharedRegistryMut1<animation::TimeInfo>>::new();
let on_after_frame = <callback::SharedRegistryMut1<animation::TimeInfo>>::new();
let on_stats_available = <callback::SharedRegistryMut1<StatsData>>::new();
let stats_draw_handle = on_stats_available.add(f!([stats_monitor] (stats: &StatsData) {
stats_monitor.sample_and_draw(stats);
}));
let main_loop_frame = main_loop.on_frame(
f!([stats_monitor,on_before_frame,on_after_frame,on_stats_available,uniforms,scene_dirty,scene]
f!([stats,on_before_frame,on_after_frame,on_stats_available,uniforms,scene_dirty,scene]
(t:animation::TimeInfo) {
// Note [Main Loop Performance]
stats_monitor.begin();
let previous_frame_stats = stats.begin_frame();
on_before_frame.run_all(&t);
if let Some(stats) = stats_monitor.previous_frame_stats() {
if let Some(stats) = previous_frame_stats {
on_stats_available.run_all(&stats);
}
@ -102,7 +105,7 @@ impl World {
scene.renderer.run();
on_after_frame.run_all(&t);
stats_monitor.end();
stats.end_frame();
}),
);
@ -114,6 +117,7 @@ impl World {
uniforms,
stats,
stats_monitor,
stats_draw_handle,
main_loop_frame,
on_before_frame,
on_after_frame,

View File

@ -1,140 +0,0 @@
//! This module implements the stats monitor view, which can be visible on the screen in debug mode.
use crate::prelude::*;
use crate::debug;
use crate::debug::stats::Stats;
use crate::debug::stats::StatsData;
use crate::system::web;
// ===============
// === Monitor ===
// ===============
shared! { Monitor
/// Visual panel showing performance-related methods.
#[derive(Debug)]
pub struct MonitorData {
stats : Stats,
previous_frame_stats : Option<StatsData>,
current_frame_measurement : FrameMeasurementState,
performance : web::Performance,
monitor : debug::Monitor,
panels : Vec<debug::monitor::Panel>
}
impl {
/// Constructor.
pub fn new(stats:&Stats) -> Self {
let stats = stats.clone_ref();
let previous_frame_stats = None;
let current_frame_measurement = FrameMeasurementState::Skipped;
let performance = web::performance();
let mut monitor = debug::Monitor::new();
let panels = vec![
monitor.add::<debug::monitor::FrameTime>(),
monitor.add::<debug::monitor::Fps>(),
monitor.add::<debug::monitor::WasmMemory>(),
monitor.add::<debug::monitor::GpuMemoryUsage>(),
monitor.add::<debug::monitor::DrawCallCount>(),
monitor.add::<debug::monitor::DataUploadCount>(),
monitor.add::<debug::monitor::DataUploadSize>(),
monitor.add::<debug::monitor::BufferCount>(),
monitor.add::<debug::monitor::SymbolCount>(),
monitor.add::<debug::monitor::ShaderCount>(),
monitor.add::<debug::monitor::ShaderCompileCount>(),
monitor.add::<debug::monitor::SpriteSystemCount>(),
monitor.add::<debug::monitor::SpriteCount>(),
];
Self {
stats,
previous_frame_stats,
current_frame_measurement,
performance,
monitor,
panels,
}
}
/// Start measuring data.
pub fn begin(&mut self) {
if self.visible() {
let time = self.performance.now();
if self.current_frame_measurement == FrameMeasurementState::Ended {
let previous_frame_stats = self.stats.begin_frame(time);
for panel in &self.panels {
panel.sample_and_postprocess(&previous_frame_stats);
}
self.monitor.draw();
self.previous_frame_stats = Some(previous_frame_stats);
} else {
let _ = self.stats.begin_frame(time);
self.previous_frame_stats = None;
}
self.current_frame_measurement = FrameMeasurementState::InProgress;
} else {
self.current_frame_measurement = FrameMeasurementState::Skipped;
self.previous_frame_stats = None;
}
// This should be done even when hidden in order for the stats not to overflow limits.
self.stats.reset_per_frame_statistics();
}
/// Finish measuring data.
pub fn end(&mut self) {
if self.visible() && self.current_frame_measurement == FrameMeasurementState::InProgress {
let time = self.performance.now();
self.stats.end_frame(time);
self.current_frame_measurement = FrameMeasurementState::Ended;
} else {
self.current_frame_measurement = FrameMeasurementState::Skipped;
}
}
/// Returns a snapshot of statistics data for the previous rendering frame, if available.
/// A previous rendering frame is the one before the most recent call to [`begin()`].
///
/// Note: a `None` result is returned if [`Monitor`] was not [`visible()`] during [`begin()`]
/// or [`end()`] of the previous frame, or during [`begin()`] of the current frame. That's
/// because (as a performance optimization) we're skipping stats tracking when not
/// [`visible()`].
/// For example, if [`Monitor`] is not [`visible()`] during a call to [`end()`], we skip a
/// [`Stats::end_frame()`] call, which makes it impossible for [`Stats`] to correctly calculate
/// the value of [`StatsData::frame_time`].
pub fn previous_frame_stats(&self) -> Option<StatsData> {
self.previous_frame_stats
}
/// Checks if the monitor is visible.
pub fn visible(&self) -> bool {
self.monitor.visible()
}
/// Show the monitor.
pub fn show(&mut self) {
self.monitor.show()
}
/// Hide the monitor.
pub fn hide(&mut self) {
self.monitor.hide()
}
/// Toggle the visibility of the monitor.
pub fn toggle(&mut self) {
self.monitor.toggle()
}
}}
// === FrameMeasurementState ===
#[derive(Debug, PartialEq)]
enum FrameMeasurementState {
Skipped,
InProgress,
Ended,
}

View File

@ -133,6 +133,24 @@ pub fn ignore_context_menu(target: &EventTarget) -> Option<IgnoreContextMenuHand
// ====================
// === TimeProvider ===
// ====================
/// Trait for an entity that can retrieve current time.
pub trait TimeProvider {
/// Returns current time, measured in milliseconds.
fn now(&self) -> f64;
}
impl TimeProvider for Performance {
fn now(&self) -> f64 {
self.now()
}
}
// ===================
// === DOM Helpers ===
// ===================