Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend Communication Profiling #3382

Merged

Conversation

MichaelMauderer
Copy link
Contributor

@MichaelMauderer MichaelMauderer commented Apr 6, 2022

Pull Request Description

Expands the profiling tooling

  • adds Enso specific metadata functionality
  • adds profiling to backend communication
  • enables visualizations of async task that are not active
  • enables profiling of FRP outputs (important to track user interactions that trigger computations that take a lot of time)
  • expands the sample demo scene rendering profiling data to easily allow rendering of custom profiling data

This shows the updated sample data. The grey bars show an async task that is not currently active. The vertical bars show a "Mark" that has been set in the timeline.

Peek.2022-04-06.15-01.mp4

[ci no changelog needed]

Checklist

Please include the following checklist in your PR:

  • The documentation has been updated if necessary.
  • All code conforms to the Scala, Java, and Rust style guides.
  • All code has been tested:
    • Unit tests have been written where possible.
    • If GUI codebase was changed: Enso GUI was tested when built using BOTH ./run dist and ./run watch.

@MichaelMauderer MichaelMauderer self-assigned this Apr 6, 2022
@MichaelMauderer MichaelMauderer force-pushed the wip/michaelmauderer/backend_communication_profiling branch from 51f143f to c7e49d8 Compare April 6, 2022 12:24
@MichaelMauderer MichaelMauderer marked this pull request as ready for review April 6, 2022 13:03
@kazcw kazcw mentioned this pull request Apr 7, 2022
4 tasks
@wdanilo
Copy link
Member

wdanilo commented Apr 8, 2022

@kazcw could you review it pls? I would just do super fast check after your review.

1 similar comment
@wdanilo
Copy link
Member

wdanilo commented Apr 8, 2022

@kazcw could you review it pls? I would just do super fast check after your review.

component
}

/// Instantiate a `Block` shape for the given block data from the profiler.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment is from the other function.

component
}

const MIN_INTERVALL_TIME: f64 = 0.0;
Copy link
Contributor

@kazcw kazcw Apr 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"INTERVAL". Also, units?


let blocks_marks_aligned = marks.into_iter().map(|mut mark| {
mark.position -= origin_x as f64;
mark.position += X_SCALE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+?

}

/// Return a reference to the blocks that make up the flame graph.
pub fn blocks(&self) -> &[Block] {
&self.blocks
}
/// Return a reference to the blocks that make up the flame graph.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied comment.

@@ -0,0 +1,190 @@
//! A single block component that is used to build up a flame graph.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied.


/// Log an RPC Event to the profiling framework.
pub fn log_rpc_event(event_name: &'static str) {
let event_logger = enso_profiler::MetadataLogger::new("RpcEvent");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each MetadataLogger registers itself separately with the logging framework. For performance we should create one per type, not one per event. It can be done with lazy_static.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. lazy_static seems not to be enough as MetadataLogger is not Sync, but thread_local does the trick.

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
pub enum Metadata {
/// An RPC event that was received from the backend.
RpcEvent(String),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should an ensogl module know anything about RPC? profiler supports arbitrarily-many dependency-injected metadata types so that we can separate concerns--a metadata type defined here should know only about EnsoGL metadata, and we can have one somewhere else that's appropriate for RPC messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify this, the idea of this API design is that we can use a big enum that depends on everything when we're analyzing the data (because data consumers will need access to data type definitions from all over the app), but we shouldn't try to use such a cross-cutting enum when logging metadata from all over the codebase (it mixes concerns, and the dependencies all go circular). That's why MetadataLogger doesn't rely on an enum of all metadata types; instead it supports defining different variants of the enum at different places in the code. They come together in the serialized log, and then we can use a big all-knowing enum only for deserializing.

We will need a place to define the all-inclusive Metadata enum--a crate for Enso-specific profile-interpreting code, that is very high level; it will have dependencies on all the app's runtime crates that define metadata types. We could call it something like enso-profiler-data-tools (it defines metadata and tools for building on enso-profiler-data). When I implement the tool that integrates data from the backend I'll merge that into the same crate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. But that means we need to rely on some convention that is distributed across the codebase for naming. We have the reading and writing in quite different places, but the names used need to stay in sync. But I agree that any other solution will end up with dependency issues eventually, so I don's see another nice solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could gather the names all in one place that defines them as consts for use when registering each MetadataLogger--serialization and deserialization would still be decoupled, but at least it would be easy to compare the metadata enum with the list of names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That place could just be the independent crate, which would be imported in many places. But that crate would only need some 3rd party libs and the profiling crate. So, no circular dependencies.

I think I like that better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well no, wait. If we want to log custom data, we would either need to define that data in that crate, or we would end up with cyclic dependencies. But if we define the data in that crate, we would probably end up duplicating the data structures we would like to log and need to implement conversion to those.

I'm thinking especially of the cases like the EnsoGL stats, where we already have a data struct like

pub struct StatsData {
    pub frame_time:           f64,
    pub fps:                  f64,
    pub wasm_memory_usage:    u32,
    pub gpu_memory_usage:     u32,
    pub draw_call_count:      usize,
    pub buffer_count:         usize,
    pub data_upload_count:    usize,
    pub data_upload_size:     u32,
    pub sprite_system_count:  usize,
    pub sprite_count:         usize,
    pub symbol_count:         usize,
    pub mesh_count:           usize,
    pub shader_count:         usize,
    pub shader_compile_count: usize,
}

Which is generated by a macro, and can be easily serialized. Creating a duplicate and defining a conversion seems unnecessary boilerplate.

Copy link
Contributor Author

@MichaelMauderer MichaelMauderer Apr 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we also need those structs for deserialisaiton anyway. So, there really is no good place to put this if we don't use our own structs. I guess we do need to duplicate those or we get into dependency issues eventually. Even putting this into Enso core would break as soon as we want to log some struct from some other crate that depends on Enso core.

Or am I missing something here?

Copy link
Contributor

@kazcw kazcw Apr 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't put the data definitions in a common crate; data consumers will need to depend on various application crates so that they can access types like StatsData. But I'm thinking it would be useful to define all the loggable types' names (for use with MetadataLogger::new) in a common crate: Just a bunch of declarations like const STATS_DATA: str = "StatsData"; (we could encourage this pattern with a newtype used here and as the parameter type of MetadataLogger::new). This would be enough so that we could see the list of names of serializable types, and compare it to the types a Metadata enum can deserialize.

The type definitions used for serialization and deserialization are only linked informally (nothing statically ensures they are compatible), but with serialization this is kind of true anyway; even if we used one type definition for both sides in a particular build, we may be deserializing a file that was created by a build of the app with different definitions. It's outside the realm of static guarantees, but that's what RecoverableError is for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I see a big benefit to that list. The usages will be all over the place anyway.
I restructured it now, but did not include this list of names as a separate crate. Let me know if you see this as super useful, and I’ll include it.

@@ -1,4 +1,6 @@
//! Demo scene showing a sample flame graph.
//! Demo scene showing a sample flame graph. Can be used to display a log file, if you have one.
//! To do so, set the `PROFILER_LOG_DATA` to contain the profiling log via `include_str`, and it
Copy link
Contributor

@kazcw kazcw Apr 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why set a profile at compile time? We currently render a file obtained at runtime from dist/content/proflie.json

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loading the data at runtime is a bit more useful, just a bot more work. But I changed to do load the data instead of hard-coding the content..

lib/rust/json-rpc/Cargo.toml Outdated Show resolved Hide resolved
lib/rust/profiler/flame-graph/src/lib.rs Outdated Show resolved Hide resolved
const DURATION_FLOOR_MS: f64 = 3.0;
if end < start + DURATION_FLOOR_MS {
end = start + DURATION_FLOOR_MS;
for window in measurement.intervals.windows(2) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't create any Active block for the last interval of each measurement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted! Fixed.

Copy link
Member

@wdanilo wdanilo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kazcw you did an amazing review, thanks for it!

version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are normally not keeping this comment in Cargo tomls I think.

use std::fmt::Display;
use std::fmt::Formatter;


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sections missing

Comment on lines +3593 to +3599
frp.private.output.default_x_gap_between_nodes <+ default_x_gap;
frp.private.output.default_y_gap_between_nodes <+ default_y_gap;
frp.private.output.min_x_spacing_for_new_nodes <+ min_x_spacing;
}
frp.source.default_x_gap_between_nodes.emit(default_x_gap.value());
frp.source.default_y_gap_between_nodes.emit(default_y_gap.value());
frp.source.min_x_spacing_for_new_nodes.emit(min_x_spacing.value());
frp.private.output.default_x_gap_between_nodes.emit(default_x_gap.value());
frp.private.output.default_y_gap_between_nodes.emit(default_y_gap.value());
frp.private.output.min_x_spacing_for_new_nodes.emit(min_x_spacing.value());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow, you moved graph editor to the new fro definition, that's huge! ❤️

Comment on lines 56 to 58


(shape + hover_area).into()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove at least one empty line here. We can definitely use empty lines to group related lines together, but 2 empty lines is too much IMO. If you dont agree, lets talk about it! :)

'RequestMode',
'Response',
'Window',
]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nl

const DURATION_FLOOR_MS: f64 = 3.0;
if end < start + DURATION_FLOOR_MS {
end = start + DURATION_FLOOR_MS;
for window in measurement.intervals.windows(2) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this 2 looks strange and I dont understand where it comes from. If you'd refactor it to a var it would be so much better.

@kazcw kazcw mentioned this pull request Apr 15, 2022
4 tasks
Copy link
Contributor

@kazcw kazcw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one more thing missing in visit_measurement: Each measurement should have an inactive interval from its creation time (Measurement.created) to the start of its first active interval.

@MichaelMauderer MichaelMauderer added the CI: Ready to merge This PR is eligible for automatic merge label Apr 19, 2022
@MichaelMauderer MichaelMauderer merged commit 24e0f33 into develop Apr 19, 2022
@MichaelMauderer MichaelMauderer deleted the wip/michaelmauderer/backend_communication_profiling branch April 19, 2022 11:30
mergify bot pushed a commit that referenced this pull request Apr 21, 2022
Add logging of EnsoGL performance stats to the profiling framework. Also extends the visualization in the debug scene to show an overview of the performance stats. We now render a timeline of blocks that indicate by their colour the rough FPS range we are in:

https://user-images.githubusercontent.com/1428930/162433094-57fbb61a-b502-43bb-8815-b7fc992d3862.mp4

# Important Notes
[ci no changelog needed]

Needs to be merged after #3382 as it requires some changes about metadata logging from there. That is why this PR is currently still in draft mode and based on that branch.
mwu-tow pushed a commit that referenced this pull request Apr 21, 2022
See: [#181837344](https://www.pivotaltracker.com/story/show/181837344).

I've separated this PR from some deeper changes I'm making to the profile format, because the changeset was getting too complex. The new APIs and tools in this PR are fully-implemented, except the profile format is too simplistic--it doesn't currently support headers that are needed to determine the relative timings of events from different processes.

- Adds basic support for profile files containing data collected by multiple processes.
- Implements `api_events_to_profile`, a tool for converting backend message logs (#3392) to the `profiler` format so they can be merged with frontend profiles (currently they can be merged with `cat`, but the next PR will introduce a merge tool).
- Introduces `message_beanpoles`, a simple tool that diagrams timing relationships between frontend and backend messages.

### Important Notes
- All TODOs introduced here will be addressed in the next PR that defines the new format.
- Introduced a new crate, `enso_profiler_enso_data`, to be used by profile consumers that need to refer to Enso application datatypes to interpret metadata.
- Introduced a `ProfileBuilder` abstraction for writing the JSON profile format; partially decouples the runtime event log structures from the format definition.
- Introducing the conversion performed for `ProfilerBuilder` uncovered that the `.._with_same_start!` low-level `profiler` APIs don't currently work; they return `Started<_>` profilers, but that is inconsistent with the stricter data model that I introduced when I implemented `profiler_data`; they need to return profilers in a created, unstarted state. Low-level async profilers have not been a priority, but once #3382 merges we'll have a way to render their data, which will be really useful because async profilers capture *why* we're doing things. I'll bring up scheduling this in the next performance meeting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI: Ready to merge This PR is eligible for automatic merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants