Add code allowing calculation of EnsoGL stats summaries (#3252)

This change adds utility code for calculating summaries from multiple samples (snapshots) of EnsoGL runtime stats values.

This internal feature is expected to be used by Enso IDE performance profiling tools, which are planned to be added in the near future.

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

A demo scene named `stats` was added, showcasing how to perform calculations using the new tools. Currently, the summary calculations in the scene work only when the EnsoGL stats Monitor Panel is visible; this is planned to be improved in a future task (https://www.pivotaltracker.com/story/show/181093601).

 - Note: the stats aggregation code is intended to be later used in Enso IDE's main rendering loop, so it needs to have very good performance characteristics. 
     - Due to that, `Accumulator` was designed to only use simple addition arithmetic, and be constant-memory once created.
This commit is contained in:
Mateusz Czapliński 2022-02-08 14:58:46 +01:00 committed by GitHub
parent db6d0e2fb3
commit e69e8078c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 308 additions and 5 deletions

12
Cargo.lock generated
View File

@ -1390,6 +1390,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "ensogl-example-stats"
version = "0.1.0"
dependencies = [
"ensogl-core",
"ensogl-hardcoded-theme",
"ensogl-label",
"ensogl-text-msdf-sys",
"wasm-bindgen",
]
[[package]]
name = "ensogl-example-text-area"
version = "0.1.0"
@ -1418,6 +1429,7 @@ dependencies = [
"ensogl-example-slider",
"ensogl-example-sprite-system",
"ensogl-example-sprite-system-benchmark",
"ensogl-example-stats",
"ensogl-example-text-area",
]

View File

@ -58,6 +58,7 @@ members = [
"lib/rust/ensogl/example/slider",
"lib/rust/ensogl/example/sprite-system",
"lib/rust/ensogl/example/sprite-system-benchmark",
"lib/rust/ensogl/example/stats",
"lib/rust/ensogl/example/text-area",
"lib/rust/frp",
"lib/rust/fuzzly",

View File

@ -1,11 +1,24 @@
//! This module defines a structure gathering statistics of the running engine. The statistics are
//! an amazing tool for debugging what is really happening under the hood and understanding the
//! performance characteristics.
//! This module provides utilities for gathering runtime performance statistics of the GUI.
//!
//! The module provides a structure which defines the statistics we are interested in ([`Stats`]),
//! and contains methods for modifying as well as retrieving the current values of the statistics
//! (often also referred to with the shortcut term "stats"). It also provides methods that need
//! to be called to ensure that some of the statistics are properly calculated per each frame, and
//! helper utility types for accumulating and summarizing stats over multiple frames. The intention
//! behind this module is to aid in detecting and debugging possible performance issues in the GUI.
//!
//! Note: some statistics will not be collected (the fields will be present but always zero) when
//! this crate is compiled without the `statistics` feature flag. This is mediated by the
//! [`if_compiled_with_stats!`] macro. At the time of writing this doc, the affected stats are:
//! - `gpu_memory_usage`
//! - `data_upload_size`
use enso_prelude::*;
use enso_types::*;
use js_sys::ArrayBuffer;
use js_sys::WebAssembly::Memory;
use num_traits::cast;
use wasm_bindgen::JsCast;
@ -106,6 +119,62 @@ macro_rules! gen_stats {
);
)* }
// === Accumulator ===
/// Contains aggregated data from multiple [`StatsData`] objects. This is intended to be
/// used as a mutable data structure, which can have new data continuously added. To
/// calculate a summary of the data based on the aggregated samples, its [`summarize()`]
/// method should be called.
#[derive(Debug, Default)]
pub struct Accumulator {
/// How many samples of [`StatsData`] were accumulated.
samples_count: u32,
$($field : ValueAccumulator<$field_type>),*
}
impl Accumulator {
/// Includes the data of the sample into the Accumulator.
pub fn add_sample(&mut self, sample: &StatsData) {
self.samples_count += 1;
if self.samples_count == 1 {
$( self.$field = ValueAccumulator::new(sample.$field); )*
} else {
$( self.$field.add_sample(sample.$field); )*
}
}
/// Calculates a summary of data added into the Accumulator till now. Returns a
/// non-empty result only if [`add_sample`] was called at least once.
pub fn summarize(&self) -> Option<Summary> {
if self.samples_count == 0 {
None
} else {
let n = self.samples_count as f64;
let summary = Summary {
$($field : ValueSummary{
min: self.$field.min,
max: self.$field.max,
avg: self.$field.sum / n,
}),*
};
Some(summary)
}
}
}
// === Summary ===
/// Contains summarized values of stats fields from multiple [`StatsData`] objects.
#[derive(Copy, Clone, Debug)]
pub struct Summary {
$(
#[allow(missing_docs)]
pub $field : ValueSummary<$field_type>
),*
}
}};
}
@ -174,3 +243,109 @@ macro_rules! if_compiled_with_stats {
{}
};
}
// ========================
// === ValueAccumulator ===
// ========================
#[derive(Debug, Default)]
struct ValueAccumulator<T> {
min: T,
max: T,
sum: f64,
}
impl<T: Min + Max + PartialOrd + cast::AsPrimitive<f64> + Copy> ValueAccumulator<T> {
fn new(v: T) -> Self {
Self { min: v, max: v, sum: v.as_() }
}
fn add_sample(&mut self, v: T) {
self.min = min(self.min, v);
self.max = max(self.max, v);
self.sum += v.as_();
}
}
// ====================
// === ValueSummary ===
// ====================
/// Summary for multiple values of type T. Intended to be used for storing a summary of multiple
/// samples of some runtime stat.
#[derive(Copy, Clone, Debug)]
#[allow(missing_docs)]
pub struct ValueSummary<T> {
pub min: T,
pub max: T,
pub avg: f64,
}
// =============
// === Tests ===
// =============
#[cfg(test)]
mod tests {
use super::*;
use assert_approx_eq::assert_approx_eq;
macro_rules! test_with_new_sample {
(
$accumulator:expr;
$(
$field:ident : $type:tt = $sample:literal
=> min: $min:literal avg: $avg:literal max: $max:literal
)*
) => {
$(let $field: $type = $sample;)*
let sample_stats = StatsData { $($field,)* ..default() };
$accumulator.add_sample(&sample_stats);
let tested_summary = $accumulator.summarize().unwrap();
$(
test_with_new_sample!($type, tested_summary.$field.min, $min);
test_with_new_sample!(f64, tested_summary.$field.avg, $avg);
test_with_new_sample!($type, tested_summary.$field.max, $max);
)*
};
// Helper rules for asserting equality on various types
(f64, $val1:expr, $val2:expr) => { assert_approx_eq!($val1, $val2); };
(u32, $val1:expr, $val2:expr) => { assert_eq!($val1, $val2); };
(usize, $val1:expr, $val2:expr) => { assert_eq!($val1, $val2); };
}
#[test]
fn stats_summaries() {
// This tests attempts to verify calculation of proper summaries for stats of each
// primitive type supported by `gen_stats!`.
let mut accumulator: Accumulator = default();
assert!(matches!(accumulator.summarize(), None));
test_with_new_sample!(accumulator;
fps: f64 = 55.0 => min: 55.0 avg: 55.0 max: 55.0
wasm_memory_usage: u32 = 1000 => min: 1000 avg: 1000.0 max: 1000
buffer_count: usize = 3 => min: 3 avg: 3.0 max: 3
);
test_with_new_sample!(accumulator;
fps: f64 = 57.0 => min: 55.0 avg: 56.0 max: 57.0
wasm_memory_usage: u32 = 2000 => min: 1000 avg: 1500.0 max: 2000
buffer_count: usize = 2 => min: 2 avg: 2.5 max: 3
);
test_with_new_sample!(accumulator;
fps: f64 = 56.0 => min: 55.0 avg: 56.0 max: 57.0
wasm_memory_usage: u32 = 3000 => min: 1000 avg: 2000.0 max: 3000
buffer_count: usize = 1 => min: 1 avg: 2.0 max: 3
);
}
}

View File

@ -21,6 +21,7 @@ ensogl-example-shape-system = { path = "shape-system" }
ensogl-example-slider = { path = "slider" }
ensogl-example-sprite-system = { path = "sprite-system" }
ensogl-example-sprite-system-benchmark = { path = "sprite-system-benchmark" }
ensogl-example-stats = { path = "stats" }
ensogl-example-text-area = { path = "text-area" }
#enso-frp = { path = "../../frp" }
#enso-logger = { path = "../../logger"}

View File

@ -33,4 +33,5 @@ pub use ensogl_example_shape_system as shape_system;
pub use ensogl_example_slider as slider;
pub use ensogl_example_sprite_system as sprite_system;
pub use ensogl_example_sprite_system_benchmark as sprite_system_benchmark;
pub use ensogl_example_stats as stats;
pub use ensogl_example_text_area as text_area;

View File

@ -0,0 +1,16 @@
[package]
name = "ensogl-example-stats"
version = "0.1.0"
authors = ["Enso Team <contact@enso.org>"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
ensogl-core = { path = "../../core" }
ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" }
ensogl-label = { path = "../../component/label" }
ensogl-text-msdf-sys = { path = "../../component/text/msdf-sys" }
wasm-bindgen = { version = "=0.2.58", features = [ "nightly" ] }

View File

@ -0,0 +1,97 @@
//! A debug scene which shows how to access and summarize runtime stats.
#![feature(associated_type_defaults)]
#![feature(drain_filter)]
#![feature(entry_insert)]
#![feature(fn_traits)]
#![feature(trait_alias)]
#![feature(type_alias_impl_trait)]
#![feature(unboxed_closures)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
#![warn(missing_docs)]
#![warn(trivial_casts)]
#![warn(trivial_numeric_casts)]
#![warn(unsafe_code)]
#![warn(unused_import_braces)]
#![warn(unused_qualifications)]
#![recursion_limit = "1024"]
use ensogl_core::prelude::*;
use wasm_bindgen::prelude::*;
use ensogl_core::application::Application;
use ensogl_core::debug::stats;
use ensogl_core::display::object::ObjectOps;
use ensogl_core::system::web;
use ensogl_hardcoded_theme as theme;
use ensogl_label::Label;
use ensogl_text_msdf_sys::run_once_initialized;
// ===================
// === Entry Point ===
// ===================
/// An entry point.
#[wasm_bindgen]
#[allow(dead_code)]
pub fn entry_point_stats() {
web::forward_panic_hook_to_console();
web::set_stack_trace_limit();
run_once_initialized(|| {
let app = Application::new(&web::get_html_element_by_id("root").unwrap());
init(&app);
Leak::new(app);
});
}
// ========================
// === Init Application ===
// ========================
fn init(app: &Application) {
theme::builtin::dark::register(&app);
theme::builtin::light::register(&app);
theme::builtin::light::enable(&app);
let label = Label::new(app);
app.display.add_child(&label);
let stats = app.display.scene().stats.clone();
let mut stats_accumulator: stats::Accumulator = default();
let mut old_fps = stats.fps();
let mut frame_counter: usize = 0;
app.display
.on_frame(move |_| {
// TODO [MC]: retrieve stats via on_stats_available hook once the linked task is done:
// https://www.pivotaltracker.com/story/show/181093832
let fps = stats.fps();
// TODO [MC]: remove the `old_fps` check once the linked task is done:
// https://www.pivotaltracker.com/story/show/181093601
if fps != old_fps {
let mut stats_sample: stats::StatsData = default();
stats_sample.fps = fps;
stats_accumulator.add_sample(&stats_sample);
old_fps = fps;
}
let stats_summary = stats_accumulator.summarize();
let fps_summary = stats_summary.map(|s| s.fps);
if frame_counter % 60 == 0 {
let text = iformat!(
"Press CTRL-OPTION-TILDE (TILDE is the key below ESC) to show Monitor panel"
"\n fps = " fps
"\n - min = " fps_summary.map_or(0.0, |s| s.min)
"\n - avg = " fps_summary.map_or(0.0, |s| s.avg)
"\n - max = " fps_summary.map_or(0.0, |s| s.max)
);
label.frp.set_content(text);
}
frame_counter += 1;
})
.forget();
}

View File

@ -198,7 +198,7 @@ macro_rules! gen_min {
)*};
}
gen_min!([f32, f64, i32, i64, usize]);
gen_min!([f32, f64, i32, i64, u32, usize]);
@ -225,7 +225,7 @@ macro_rules! gen_max {
)*};
}
gen_max!([f32, f64, i32, i64, usize]);
gen_max!([f32, f64, i32, i64, u32, usize]);