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:
Michael Mauderer 2022-03-03 23:23:27 +01:00 committed by GitHub
parent fb68f18739
commit d3cc2c1025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 757 additions and 50 deletions

43
Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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" }

View 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" }

View 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>;

View 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
}
}

View File

@ -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()
}
}

View File

@ -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);

View File

@ -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" }

View 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" ] }

View 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()
}
}

View File

@ -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;

View File

@ -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"

View File

@ -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() {

View 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"

View 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"));
}
}

View File

@ -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.