mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 13:15:52 +03:00
Flame Graph for Profiling Data (#3297)
Add an API to create a flame graph from profiling data. Also adds a demo scene showcasing the functionality that generates some profiling data by measuring dummy function calls and rendering a flame graph for the dummy data (see video for the result). Not that the functionality is not yet exposed user-facing in the GUI itself, but only as API and demo scene, therefore [ci no changelog needed] https://user-images.githubusercontent.com/1428930/155118977-ecac0628-777c-48bd-9aa7-30ee6aef1976.mp4 # Important Notes * Change from the initial design: labels are shown on the flame graph instead of as a tooltip. This is because tooltips are currently only implemented in the graph editor and would require some additional refactoring (probably taking the better part of a day). * re-instated the behaviour that logs are shown in the JS console if development mode is active.
This commit is contained in:
parent
fb68f18739
commit
d3cc2c1025
43
Cargo.lock
generated
43
Cargo.lock
generated
@ -1110,6 +1110,7 @@ name = "enso-profiler"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"enso-profiler-macros",
|
||||
"enso-web",
|
||||
"futures 0.3.21",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1126,6 +1127,15 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enso-profiler-flame-graph"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"enso-profiler",
|
||||
"enso-profiler-data",
|
||||
"futures 0.3.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enso-profiler-macros"
|
||||
version = "0.1.0"
|
||||
@ -1241,6 +1251,7 @@ dependencies = [
|
||||
"ensogl-drop-down-menu",
|
||||
"ensogl-drop-manager",
|
||||
"ensogl-file-browser",
|
||||
"ensogl-flame-graph",
|
||||
"ensogl-label",
|
||||
"ensogl-list-view",
|
||||
"ensogl-scroll-area",
|
||||
@ -1399,6 +1410,23 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ensogl-example-profiling-run-graph"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"enso-frp",
|
||||
"enso-profiler",
|
||||
"enso-profiler-flame-graph",
|
||||
"enso-web",
|
||||
"ensogl-core",
|
||||
"ensogl-flame-graph",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-text",
|
||||
"ensogl-text-msdf-sys",
|
||||
"futures 0.3.21",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ensogl-example-scroll-area"
|
||||
version = "0.1.0"
|
||||
@ -1469,6 +1497,7 @@ dependencies = [
|
||||
"ensogl-example-glyph-system",
|
||||
"ensogl-example-list-view",
|
||||
"ensogl-example-mouse-events",
|
||||
"ensogl-example-profiling-run-graph",
|
||||
"ensogl-example-scroll-area",
|
||||
"ensogl-example-shape-system",
|
||||
"ensogl-example-slider",
|
||||
@ -1485,6 +1514,20 @@ dependencies = [
|
||||
"ensogl-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ensogl-flame-graph"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"enso-frp",
|
||||
"enso-profiler",
|
||||
"enso-profiler-flame-graph",
|
||||
"ensogl",
|
||||
"ensogl-core",
|
||||
"ensogl-gui-component",
|
||||
"ensogl-hardcoded-theme",
|
||||
"ensogl-text",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ensogl-gui-component"
|
||||
version = "0.1.0"
|
||||
|
@ -934,7 +934,10 @@ async function runEntryPoint(config: Config) {
|
||||
//initCrashHandling()
|
||||
style_root()
|
||||
printScamWarning()
|
||||
hideLogs()
|
||||
/// Only hide logs in production, but show them when running a development version.
|
||||
if (!Versions.isDevVersion()) {
|
||||
hideLogs()
|
||||
}
|
||||
disableContextMenu()
|
||||
|
||||
let entryTarget = ok(config.entry) ? config.entry : main_entry_point
|
||||
|
@ -173,7 +173,7 @@ commands.build.rust = async function (argv) {
|
||||
console.log('Minimizing the WASM binary.')
|
||||
await gzip(paths.wasm.main, paths.wasm.mainGz)
|
||||
|
||||
const limitMb = 4.6
|
||||
const limitMb = 4.62
|
||||
await checkWasmSize(paths.wasm.mainGz, limitMb)
|
||||
}
|
||||
// Copy WASM files from temporary directory to Webpack's `dist` directory.
|
||||
|
@ -9,6 +9,7 @@ ensogl-button = { path = "button" }
|
||||
ensogl-drop-down-menu = { path = "drop-down-menu" }
|
||||
ensogl-drop-manager = { path = "drop-manager" }
|
||||
ensogl-file-browser = { path = "file-browser" }
|
||||
ensogl-flame-graph = { path = "flame-graph" }
|
||||
ensogl-label = { path = "label" }
|
||||
ensogl-list-view = { path = "list-view" }
|
||||
ensogl-scroll-area = { path = "scroll-area" }
|
||||
|
15
lib/rust/ensogl/component/flame-graph/Cargo.toml
Normal file
15
lib/rust/ensogl/component/flame-graph/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "ensogl-flame-graph"
|
||||
version = "0.1.0"
|
||||
authors = ["Enso Team <contact@enso.org>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
enso-frp = { path = "../../../frp" }
|
||||
ensogl-core = { path = "../../core" }
|
||||
enso-profiler-flame-graph = { path = "../../../profiler/flame-graph" }
|
||||
enso-profiler = { path = "../../../profiler" }
|
||||
ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" }
|
||||
ensogl-text = { path = "../text" }
|
||||
ensogl = { version = "0.1.0", path = "../../../ensogl" }
|
||||
ensogl-gui-component = { version = "0.1.0", path = "../../component/gui" }
|
173
lib/rust/ensogl/component/flame-graph/src/block.rs
Normal file
173
lib/rust/ensogl/component/flame-graph/src/block.rs
Normal file
@ -0,0 +1,173 @@
|
||||
//! A single block component that is used to build up a flame graph.
|
||||
|
||||
use ensogl_core::prelude::*;
|
||||
|
||||
use ensogl::frp;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::data::color;
|
||||
use ensogl_core::display;
|
||||
use ensogl_core::display::scene::Layer;
|
||||
use ensogl_core::display::shape::StyleWatchFrp;
|
||||
use ensogl_core::display::shape::*;
|
||||
use ensogl_core::Animation;
|
||||
use ensogl_gui_component::component;
|
||||
use ensogl_gui_component::component::Component;
|
||||
use ensogl_text as text;
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === Layout Constants ===
|
||||
// =======================
|
||||
|
||||
const TEXT_OFFSET_X: f32 = 4.0;
|
||||
const TEXT_OFFSET_Y: f32 = -2.0;
|
||||
|
||||
|
||||
|
||||
// =========================
|
||||
// === Shape Definition ===
|
||||
// =========================
|
||||
|
||||
mod background {
|
||||
use super::*;
|
||||
ensogl_core::define_shape_system! {
|
||||
(style:Style) {
|
||||
let width : Var<Pixels> = "input_size.x".into();
|
||||
let height : Var<Pixels> = "input_size.y".into();
|
||||
let zoom = Var::<f32>::from("1.0/zoom()");
|
||||
let base_color = style.get_color("flame_graph_color");
|
||||
|
||||
let shape = Rect((&width,&height));
|
||||
|
||||
let border_width: Var<Pixels> = (zoom * 2.0).into();
|
||||
|
||||
let right = Rect((&border_width,&height));
|
||||
let right = right.translate_x(&width/2.0);
|
||||
|
||||
let left = Rect((&border_width,&height));
|
||||
let left = left.translate_x(-&width/2.0);
|
||||
|
||||
let shape = shape - left;
|
||||
let shape = shape - right;
|
||||
let shape = shape.fill(base_color);
|
||||
|
||||
(shape).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
ensogl_core::define_endpoints! {
|
||||
Input {
|
||||
set_content(String),
|
||||
set_position(Vector2),
|
||||
set_size(Vector2)
|
||||
}
|
||||
Output {}
|
||||
}
|
||||
|
||||
impl component::Frp<Model> for Frp {
|
||||
fn init(&self, _app: &Application, model: &Model, _style: &StyleWatchFrp) {
|
||||
let network = &self.network;
|
||||
|
||||
let label_opacity = Animation::<f32>::new(network);
|
||||
|
||||
frp::extend! { network
|
||||
eval self.set_content((t) model.set_content(t));
|
||||
eval self.set_size((size) model.set_size(*size));
|
||||
|
||||
label_opacity.target <+ model.background.events.mouse_over.constant(1.0);
|
||||
label_opacity.target <+ model.background.events.mouse_out.constant(0.0);
|
||||
eval label_opacity.value ((t) model.set_label_opacity(*t));
|
||||
}
|
||||
|
||||
label_opacity.target.emit(0.0);
|
||||
model.set_label_opacity(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
pub struct Model {
|
||||
app: Application,
|
||||
background: background::View,
|
||||
label: text::Area,
|
||||
display_object: display::object::Instance,
|
||||
}
|
||||
|
||||
impl component::Model for Model {
|
||||
fn label() -> &'static str {
|
||||
"FlameGraphBlock"
|
||||
}
|
||||
|
||||
fn new(app: &Application, logger: &Logger) -> Self {
|
||||
let scene = app.display.scene();
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
let label = app.new_view::<text::Area>();
|
||||
let background = background::View::new(&logger);
|
||||
|
||||
display_object.add_child(&background);
|
||||
display_object.add_child(&label);
|
||||
|
||||
let app = app.clone_ref();
|
||||
let model = Model { app, background, label, display_object };
|
||||
model.set_layers(&scene.layers.tooltip, &scene.layers.tooltip_text);
|
||||
model
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Set scene layers for background and text respectively.
|
||||
pub fn set_layers(&self, background_layer: &Layer, text_layer: &Layer) {
|
||||
// FIXME[MM/WD]: Depth sorting of labels to in front of everything else in the scene.
|
||||
// Temporary solution. The depth management needs to allow defining relative position
|
||||
// of the text and background and let the whole component to be set to am
|
||||
// an arbitrary layer.
|
||||
background_layer.add_exclusive(&self.background);
|
||||
self.label.add_to_scene_layer(text_layer);
|
||||
}
|
||||
|
||||
fn set_size(&self, size: Vector2) {
|
||||
self.background.size.set(size);
|
||||
let text_size = self.label.height.value();
|
||||
let text_origin = Vector2(TEXT_OFFSET_X - size.x / 2.0, TEXT_OFFSET_Y + text_size / 2.0);
|
||||
self.label.set_position_xy(text_origin);
|
||||
}
|
||||
|
||||
fn set_content(&self, t: &str) {
|
||||
self.label.set_content(t)
|
||||
}
|
||||
|
||||
fn set_label_opacity(&self, opacity: f32) {
|
||||
let color = self.label.default_color.value();
|
||||
let color: color::Rgba = color.opaque.with_alpha(opacity);
|
||||
self.label.set_color_all.emit(color);
|
||||
self.label.set_default_color.emit(color);
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for Model {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Component ===
|
||||
// =================
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub type Block = Component<Model, Frp>;
|
93
lib/rust/ensogl/component/flame-graph/src/lib.rs
Normal file
93
lib/rust/ensogl/component/flame-graph/src/lib.rs
Normal file
@ -0,0 +1,93 @@
|
||||
//! An EnsoGL implementation of a basic flame graph.
|
||||
|
||||
#![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)]
|
||||
|
||||
use ensogl_core::prelude::*;
|
||||
|
||||
use enso_profiler_flame_graph as profiler_flame_graph;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::display;
|
||||
|
||||
mod block;
|
||||
|
||||
|
||||
pub use block::Block;
|
||||
|
||||
|
||||
|
||||
// ========================
|
||||
// === Layout Constants ===
|
||||
// ========================
|
||||
|
||||
const ROW_HEIGHT: f64 = 20.0;
|
||||
const ROW_PADDING: f64 = 5.0;
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === Flame Graph ===
|
||||
// ===================
|
||||
|
||||
|
||||
/// EnsoGL FlameGraph component. Consists of stacked blocks that indicate hierarchical time
|
||||
/// intervals. The blocks can be hovered to reveal a label associated with the block.
|
||||
#[derive(Debug)]
|
||||
pub struct FlameGraph {
|
||||
display_object: display::object::Instance,
|
||||
blocks: Vec<Block>,
|
||||
}
|
||||
|
||||
/// Instantiate a `Block` shape for the given block data from the profiler.
|
||||
fn shape_from_block(block: profiler_flame_graph::Block, app: &Application) -> Block {
|
||||
let component = app.new_view::<Block>();
|
||||
|
||||
let size = Vector2::new(block.width() as f32, ROW_HEIGHT as f32);
|
||||
let x = block.start + block.width() / 2.0;
|
||||
let y = block.row as f64 * (ROW_HEIGHT + ROW_PADDING);
|
||||
let pos = Vector2::new(x as f32, y as f32);
|
||||
|
||||
component.set_content.emit(block.label);
|
||||
component.set_size.emit(size);
|
||||
component.set_position_xy(pos);
|
||||
|
||||
component
|
||||
}
|
||||
|
||||
impl FlameGraph {
|
||||
/// Create a `FlameGraph` EnsoGL component from the given flame graph data from the profiler.
|
||||
pub fn from_data(data: profiler_flame_graph::FlameGraph, app: &Application) -> Self {
|
||||
let logger = Logger::new("FlameGraph");
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
|
||||
let min_time =
|
||||
data.blocks.iter().map(|block| block.start.floor() as u32).min().unwrap_or_default();
|
||||
|
||||
let blocks_zero_aligned = data.blocks.into_iter().map(|mut block| {
|
||||
block.start -= min_time as f64;
|
||||
block.end -= min_time as f64;
|
||||
block
|
||||
});
|
||||
|
||||
let blocks = blocks_zero_aligned.map(|block| shape_from_block(block, app)).collect_vec();
|
||||
blocks.iter().for_each(|item| display_object.add_child(item));
|
||||
Self { display_object, blocks }
|
||||
}
|
||||
|
||||
/// Return a reference to the blocks that make up the flame graph.
|
||||
pub fn blocks(&self) -> &[Block] {
|
||||
&self.blocks
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for FlameGraph {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ use crate::prelude::*;
|
||||
use enso_frp as frp;
|
||||
use ensogl_core::application;
|
||||
use ensogl_core::application::command::CommandApi;
|
||||
use ensogl_core::application::command::FrpNetworkProvider;
|
||||
use ensogl_core::application::shortcut;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::display;
|
||||
use ensogl_core::display::shape::*;
|
||||
@ -25,8 +27,12 @@ use ensogl_core::display::shape::*;
|
||||
/// returns `Self`. The model will be created with this constructor when constructing the
|
||||
/// `Component`.
|
||||
pub trait Model {
|
||||
/// Identifier of the Model. Used for initializing the component logger and
|
||||
/// to provide the label for the `command::View` implementation.
|
||||
fn label() -> &'static str;
|
||||
|
||||
/// Constructor.
|
||||
fn new(app: &Application) -> Self;
|
||||
fn new(app: &Application, logger: &Logger) -> Self;
|
||||
}
|
||||
|
||||
|
||||
@ -41,6 +47,12 @@ pub trait Model {
|
||||
pub trait Frp<Model>: Default + CommandApi {
|
||||
/// Frp initializer.
|
||||
fn init(&self, app: &Application, model: &Model, style: &StyleWatchFrp);
|
||||
|
||||
/// Set of default shortcuts to be used in the `CommandApi`. See
|
||||
/// `lib/rust/ensogl/core/src/application/command.rs` for more details.
|
||||
fn default_shortcuts() -> Vec<shortcut::Shortcut> {
|
||||
default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -57,6 +69,7 @@ pub struct Component<Model, Frp> {
|
||||
/// Public FRP api of the Component.
|
||||
pub frp: Rc<Frp>,
|
||||
model: Rc<Model>,
|
||||
logger: Logger,
|
||||
/// Reference to the application the Component belongs to. Generally required for implementing
|
||||
/// `application::View` and initialising the `Model` and `Frp` and thus provided by the
|
||||
/// `Component`.
|
||||
@ -67,12 +80,13 @@ impl<M: Model, F: Frp<M>> Component<M, F> {
|
||||
/// Constructor.
|
||||
pub fn new(app: &Application) -> Self {
|
||||
let app = app.clone_ref();
|
||||
let model = Rc::new(M::new(&app));
|
||||
let logger = Logger::new(M::label());
|
||||
let model = Rc::new(M::new(&app, &logger));
|
||||
let frp = F::default();
|
||||
let style = StyleWatchFrp::new(&app.display.scene().style_sheet);
|
||||
frp.init(&app, &model, &style);
|
||||
let frp = Rc::new(frp);
|
||||
Self { frp, model, app }
|
||||
Self { frp, model, app, logger }
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,10 +103,26 @@ impl<M, F: Frp<M>> Deref for Component<M, F> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, F: application::command::FrpNetworkProvider> application::command::FrpNetworkProvider
|
||||
for Component<M, F>
|
||||
{
|
||||
impl<M, F: FrpNetworkProvider> FrpNetworkProvider for Component<M, F> {
|
||||
fn network(&self) -> &frp::Network {
|
||||
self.frp.network()
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Model, F: Frp<M> + FrpNetworkProvider> application::View for Component<M, F> {
|
||||
fn label() -> &'static str {
|
||||
M::label()
|
||||
}
|
||||
|
||||
fn new(app: &Application) -> Self {
|
||||
Component::new(app)
|
||||
}
|
||||
|
||||
fn app(&self) -> &Application {
|
||||
&self.app
|
||||
}
|
||||
|
||||
fn default_shortcuts() -> Vec<shortcut::Shortcut> {
|
||||
F::default_shortcuts()
|
||||
}
|
||||
}
|
||||
|
@ -265,6 +265,7 @@ ensogl_core::define_endpoints! {
|
||||
content (Text),
|
||||
hovered (bool),
|
||||
selection_color (color::Rgb),
|
||||
default_color (color::Rgba),
|
||||
}
|
||||
}
|
||||
|
||||
@ -468,6 +469,8 @@ impl Area {
|
||||
// === Colors ===
|
||||
|
||||
eval input.set_default_color ((t) m.buffer.frp.set_default_color(*t));
|
||||
self.frp.source.default_color <+ self.frp.set_default_color;
|
||||
|
||||
eval input.set_default_text_size ((t) {
|
||||
m.buffer.frp.set_default_text_size(*t);
|
||||
m.redraw(true);
|
||||
|
@ -16,6 +16,7 @@ ensogl-example-easing-animator = { path = "easing-animator" }
|
||||
ensogl-example-glyph-system = { path = "glyph-system" }
|
||||
ensogl-example-list-view = { path = "list-view" }
|
||||
ensogl-example-mouse-events = { path = "mouse-events" }
|
||||
ensogl-example-profiling-run-graph = { path = "profiling-run-graph" }
|
||||
ensogl-example-scroll-area = { path = "scroll-area" }
|
||||
ensogl-example-shape-system = { path = "shape-system" }
|
||||
ensogl-example-slider = { path = "slider" }
|
||||
|
23
lib/rust/ensogl/example/profiling-run-graph/Cargo.toml
Normal file
23
lib/rust/ensogl/example/profiling-run-graph/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "ensogl-example-profiling-run-graph"
|
||||
version = "0.1.0"
|
||||
authors = ["Enso Team <contact@enso.org>"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
enso-frp = { path = "../../../frp" }
|
||||
enso-profiler = { path = "../../../profiler" }
|
||||
enso-profiler-flame-graph = { path = "../../../profiler/flame-graph" }
|
||||
enso-web = { path = "../../../web" }
|
||||
ensogl-core = { path = "../../core" }
|
||||
ensogl-flame-graph = { path = "../../component/flame-graph" }
|
||||
ensogl-hardcoded-theme = { path = "../../app/theme/hardcoded" }
|
||||
ensogl-text = { path = "../../component/text" }
|
||||
ensogl-text-msdf-sys = { path = "../../component/text/msdf-sys" }
|
||||
futures = "0.3"
|
||||
wasm-bindgen = { version = "0.2.58", features = [ "nightly" ] }
|
||||
|
174
lib/rust/ensogl/example/profiling-run-graph/src/lib.rs
Normal file
174
lib/rust/ensogl/example/profiling-run-graph/src/lib.rs
Normal file
@ -0,0 +1,174 @@
|
||||
//! Demo scene showing a sample flame graph.
|
||||
|
||||
#![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)]
|
||||
|
||||
use ensogl_core::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use enso_profiler as profiler;
|
||||
use enso_profiler::profile;
|
||||
use enso_profiler_flame_graph as profiler_flame_graph;
|
||||
use ensogl_core::application::Application;
|
||||
use ensogl_core::data::color;
|
||||
use ensogl_core::display::navigation::navigator::Navigator;
|
||||
use ensogl_core::display::object::ObjectOps;
|
||||
use ensogl_core::display::style::theme;
|
||||
use ensogl_core::display::Scene;
|
||||
use ensogl_core::system::web;
|
||||
use ensogl_flame_graph as flame_graph;
|
||||
|
||||
|
||||
|
||||
// ===================
|
||||
// === Entry Point ===
|
||||
// ===================
|
||||
|
||||
/// The example entry point.
|
||||
#[wasm_bindgen]
|
||||
#[allow(dead_code)]
|
||||
pub fn entry_point_profiling_run_graph() {
|
||||
web::forward_panic_hook_to_console();
|
||||
web::set_stack_trace_limit();
|
||||
|
||||
let app = &Application::new(&web::get_html_element_by_id("root").unwrap());
|
||||
let world = &app.display;
|
||||
let scene = world.scene();
|
||||
let camera = scene.camera().clone_ref();
|
||||
let navigator = Navigator::new(scene, &camera);
|
||||
|
||||
init_theme(scene);
|
||||
|
||||
// Generate Test data
|
||||
futures::executor::block_on(start_project());
|
||||
|
||||
let measurements = profiler_flame_graph::FlameGraph::take_from_log();
|
||||
let flame_graph = flame_graph::FlameGraph::from_data(measurements, app);
|
||||
|
||||
world.add_child(&flame_graph);
|
||||
scene.add_child(&flame_graph);
|
||||
scene.layers.main.add_exclusive(&flame_graph);
|
||||
|
||||
world.keep_alive_forever();
|
||||
let scene = world.scene().clone_ref();
|
||||
|
||||
world
|
||||
.on_frame(move |_time| {
|
||||
let _keep_alive = &navigator;
|
||||
let _keep_alive = &scene;
|
||||
let _keep_alive = &flame_graph;
|
||||
})
|
||||
.forget();
|
||||
}
|
||||
|
||||
fn init_theme(scene: &Scene) {
|
||||
let theme_manager = theme::Manager::from(&scene.style_sheet);
|
||||
|
||||
let theme = theme::Theme::new();
|
||||
theme.set("flame_graph_color", color::Rgb::new(1.0, 45.0 / 255.0, 0.0));
|
||||
|
||||
theme_manager.register("theme", theme);
|
||||
|
||||
theme_manager.set_enabled(&["theme".to_string()]);
|
||||
|
||||
let style_watch = ensogl_core::display::shape::StyleWatch::new(&scene.style_sheet);
|
||||
style_watch.get("flame_graph_color");
|
||||
}
|
||||
|
||||
|
||||
// ==========================
|
||||
// === Dummy Computations ===
|
||||
// ==========================
|
||||
|
||||
/// A dummy computation that is intended to take some time based on input (where a higher number
|
||||
///takes longer).
|
||||
fn work(n: u32) {
|
||||
let mut m = n;
|
||||
for x in 0..n {
|
||||
for y in 0..n {
|
||||
for z in 0..n {
|
||||
m = m.wrapping_add(x * y * z)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create a side effect to avoid optimising away the computation.
|
||||
println!("{}", m % 7)
|
||||
}
|
||||
|
||||
#[profile(Objective)]
|
||||
async fn start_project() {
|
||||
wake_dragon().await;
|
||||
feed_troll();
|
||||
ride_rainbow();
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn ride_rainbow() {
|
||||
work(333)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn feed_troll() {
|
||||
gather_herbs_and_spices();
|
||||
cook_troll_food();
|
||||
run_away();
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn run_away() {
|
||||
work(100)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn cook_troll_food() {
|
||||
work(100)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn gather_herbs_and_spices() {
|
||||
walk_to_woods();
|
||||
search_stuff();
|
||||
find_stuff();
|
||||
gather_stuff();
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn gather_stuff() {
|
||||
work(100)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn find_stuff() {
|
||||
work(100)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn search_stuff() {
|
||||
work(100)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn walk_to_woods() {
|
||||
work(100)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
async fn wake_dragon() {
|
||||
gather_gold().await;
|
||||
bake_gold_cake().await;
|
||||
start_tea_party().await;
|
||||
}
|
||||
#[profile(Objective)]
|
||||
async fn start_tea_party() {
|
||||
work(100)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
async fn bake_gold_cake() {
|
||||
work(100)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
fn pick_coin() {
|
||||
work(75)
|
||||
}
|
||||
#[profile(Objective)]
|
||||
async fn gather_gold() {
|
||||
for _ in 0..5 {
|
||||
pick_coin()
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ pub use ensogl_example_easing_animator as easing_animator;
|
||||
pub use ensogl_example_glyph_system as glyph_system;
|
||||
pub use ensogl_example_list_view as list_view;
|
||||
pub use ensogl_example_mouse_events as mouse_events;
|
||||
pub use ensogl_example_profiling_run_graph as profiling_run_graph;
|
||||
pub use ensogl_example_scroll_area as scroll_area;
|
||||
pub use ensogl_example_shape_system as shape_system;
|
||||
pub use ensogl_example_slider as slider;
|
||||
|
@ -9,6 +9,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0.59", features = ["raw_value"] }
|
||||
wasm-bindgen = { version = "0.2.58", features = ["nightly"] }
|
||||
enso-profiler-macros = { path = "macros" }
|
||||
enso-web = { path = "../web" }
|
||||
|
||||
[dev-dependencies]
|
||||
futures = "0.3"
|
||||
|
@ -306,6 +306,17 @@ pub struct Metadata<M> {
|
||||
}
|
||||
|
||||
|
||||
// === OpaqueMetadata ===
|
||||
|
||||
/// Black-box metadata object, for ignoring metadata contents.
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum OpaqueMetadata {
|
||||
/// Anything.
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
// === Mark ===
|
||||
@ -314,7 +325,9 @@ pub struct Metadata<M> {
|
||||
/// A timestamp that can be used for distinguishing event order.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
pub struct Mark {
|
||||
/// Sequence number of the mark. Used to resolve timestamp collisions.
|
||||
seq: Seq,
|
||||
/// Time of the mark.
|
||||
time: profiler::internal::Timestamp,
|
||||
}
|
||||
|
||||
@ -322,6 +335,11 @@ impl Mark {
|
||||
fn time_origin() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Time of the mark in milliseconds.
|
||||
pub fn into_ms(self) -> f64 {
|
||||
self.time.into_ms()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -376,6 +394,17 @@ pub struct Label {
|
||||
}
|
||||
|
||||
|
||||
impl fmt::Display for Label {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(pos) = self.pos.as_ref() {
|
||||
write!(f, "{} ({}:{})", self.name, pos.file, pos.line)
|
||||
} else {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === CodePos ===
|
||||
|
||||
/// Identifies a position within a specific file.
|
||||
@ -395,17 +424,12 @@ pub struct CodePos {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate as profiler_data;
|
||||
use crate::OpaqueMetadata;
|
||||
use enso_profiler as profiler;
|
||||
use profiler::profile;
|
||||
|
||||
/// Black-box metadata object, for ignoring metadata contents.
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) enum OpaqueMetadata {
|
||||
/// Anything.
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_sync() {
|
||||
|
12
lib/rust/profiler/flame-graph/Cargo.toml
Normal file
12
lib/rust/profiler/flame-graph/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "enso-profiler-flame-graph"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Enso Team <contact@enso.org>"]
|
||||
|
||||
[dependencies]
|
||||
enso-profiler = { path = "../" }
|
||||
enso-profiler-data = { path = "../data" }
|
||||
|
||||
[dev-dependencies]
|
||||
futures = "0.3"
|
138
lib/rust/profiler/flame-graph/src/lib.rs
Normal file
138
lib/rust/profiler/flame-graph/src/lib.rs
Normal file
@ -0,0 +1,138 @@
|
||||
//! This module contains functionality that allows the profiling framework to
|
||||
//! generate the data required to render a flame graph. This means creating data for each block
|
||||
//! that is supposed to be rendered, with start time, end time and labels.
|
||||
|
||||
|
||||
use enso_profiler as profiler;
|
||||
use enso_profiler_data as data;
|
||||
use enso_profiler_data::Interval;
|
||||
use enso_profiler_data::Lifetime;
|
||||
use enso_profiler_data::Measurement;
|
||||
|
||||
|
||||
// ==================
|
||||
// === Block Data ===
|
||||
// ==================
|
||||
|
||||
/// A `Block` contains the data required to render a single block of a frame graph.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Block {
|
||||
/// Start x coordinate of the block.
|
||||
pub start: f64,
|
||||
/// End x coordinate of the block.
|
||||
pub end: f64,
|
||||
/// Row that the block should be placed in.
|
||||
pub row: u32,
|
||||
/// The label to be displayed with the block.
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl Block {
|
||||
/// Width of the block.
|
||||
pub fn width(&self) -> f64 {
|
||||
self.end - self.start
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========================
|
||||
// === Flame Graph Data ===
|
||||
// ========================
|
||||
|
||||
/// A `FlameGraph`, contains the information required to render a flame graph, i.e., the data for
|
||||
/// all blocks that make up the flame graph.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FlameGraph {
|
||||
/// Collection of all blocks making up the flame graph.
|
||||
pub blocks: Vec<Block>,
|
||||
}
|
||||
|
||||
fn blocks_from_measurement<Metadata>(measurement: &Measurement<Metadata>, row: u32) -> Vec<Block> {
|
||||
match &measurement.lifetime {
|
||||
Lifetime::Async(lifetime) => lifetime
|
||||
.active
|
||||
.iter()
|
||||
.map(|interval| block_from_interval(&measurement.label, row, interval))
|
||||
.collect(),
|
||||
Lifetime::NonAsync { active } => vec![block_from_interval(&measurement.label, row, active)],
|
||||
}
|
||||
}
|
||||
|
||||
fn block_from_interval(label: &data::Label, row: u32, interval: &Interval) -> Block {
|
||||
let start = interval.start.into_ms();
|
||||
let end = interval.end.map(|mark| mark.into_ms()).unwrap_or(f64::MAX);
|
||||
let label = label.to_string();
|
||||
let row = row;
|
||||
Block { start, end, label, row }
|
||||
}
|
||||
|
||||
impl<Metadata> From<data::Measurement<Metadata>> for FlameGraph {
|
||||
fn from(root: data::Measurement<Metadata>) -> Self {
|
||||
let mut blocks = Vec::default();
|
||||
// We skip the root node, which is the app lifetime, and store the measurements with
|
||||
// their depth. Newly added children of visited nodes, will be added one row above their
|
||||
// parents.
|
||||
let mut to_parse: Vec<_> = root.children.iter().map(|m| (m, 0)).collect();
|
||||
|
||||
loop {
|
||||
let measurement = to_parse.pop();
|
||||
match measurement {
|
||||
Some((measurement, row)) => {
|
||||
let target_row = if measurement.lifetime.is_async() { 0 } else { row };
|
||||
let measurement_blocks = blocks_from_measurement(measurement, target_row);
|
||||
blocks.extend(measurement_blocks);
|
||||
to_parse.extend(measurement.children.iter().map(|m| (m, target_row + 1)));
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Self { blocks }
|
||||
}
|
||||
}
|
||||
|
||||
impl FlameGraph {
|
||||
/// Gather and remove all logged measurements and return them as a `FlameGraph`.
|
||||
pub fn take_from_log() -> Self {
|
||||
let root: Result<data::Measurement<data::OpaqueMetadata>, _> =
|
||||
profiler::internal::take_log().parse();
|
||||
if let Ok(root) = root {
|
||||
root.into()
|
||||
} else {
|
||||
eprintln!("Failed to deserialize profiling event log.");
|
||||
FlameGraph::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
#[cfg(test)]
|
||||
mod compile_tests {
|
||||
use super::*;
|
||||
use profiler::profile;
|
||||
|
||||
#[profile(Objective)]
|
||||
pub fn profiled_a() {
|
||||
profiled_b()
|
||||
}
|
||||
#[profile(Objective)]
|
||||
pub fn profiled_b() {}
|
||||
|
||||
#[test]
|
||||
fn check_flame_graph_creation() {
|
||||
profiled_a();
|
||||
|
||||
let flame_graph = FlameGraph::take_from_log();
|
||||
assert_eq!(flame_graph.blocks.len(), 2);
|
||||
|
||||
assert_eq!(flame_graph.blocks[1].row, 1);
|
||||
assert!(flame_graph.blocks[1].label.contains("profiled_b"));
|
||||
assert_eq!(flame_graph.blocks[0].row, 0);
|
||||
assert!(flame_graph.blocks[0].label.contains("profiled_a"));
|
||||
}
|
||||
}
|
@ -285,7 +285,7 @@ const TS_OFFSET: u64 = 1;
|
||||
impl Timestamp {
|
||||
/// Return the current time, relative to the time origin.
|
||||
pub fn now() -> Self {
|
||||
Self::from_ms(js::performance::now())
|
||||
Self::from_ms(now())
|
||||
}
|
||||
|
||||
/// Return the timestamp corresponding to an offset from the time origin, in ms.
|
||||
@ -317,45 +317,17 @@ impl Default for Timestamp {
|
||||
// === FFI ===
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Web APIs.
|
||||
mod js {
|
||||
/// [The `Performance` API](https://developer.mozilla.org/en-US/docs/Web/API/Performance)
|
||||
pub mod performance {
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
/// The
|
||||
/// [performance.now](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now)
|
||||
/// method returns a double-precision float, measured in milliseconds.
|
||||
///
|
||||
/// The returned value represents the time elapsed since the time origin, which is when
|
||||
/// the page began to load.
|
||||
#[wasm_bindgen(js_namespace = performance)]
|
||||
pub fn now() -> f64;
|
||||
}
|
||||
}
|
||||
fn now() -> f64 {
|
||||
use enso_web as web;
|
||||
web::performance().now()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Web APIs.
|
||||
mod js {
|
||||
/// [The `Performance` API](https://developer.mozilla.org/en-US/docs/Web/API/Performance)
|
||||
pub mod performance {
|
||||
/// The
|
||||
/// [performance.now](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now)
|
||||
/// method returns a double-precision float, measured in milliseconds.
|
||||
///
|
||||
/// The returned value represents the time elapsed since the time origin, which is when
|
||||
/// the page began to load.
|
||||
// This mock implementation returns a dummy value.
|
||||
pub fn now() -> f64 {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
fn now() -> f64 {
|
||||
0.0
|
||||
}
|
||||
|
||||
|
||||
|
||||
// === Timestamped ===
|
||||
|
||||
/// Wrapper adding a timestamp to an object.
|
||||
|
Loading…
Reference in New Issue
Block a user