-
Notifications
You must be signed in to change notification settings - Fork 322
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
Backend Communication Profiling #3382
Conversation
51f143f
to
c7e49d8
Compare
@kazcw could you review it pls? I would just do super fast check after your review. |
1 similar comment
@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. |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copied.
lib/rust/ensogl/core/src/profiler.rs
Outdated
|
||
/// 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"); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
lib/rust/ensogl/core/src/profiler.rs
Outdated
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)] | ||
pub enum Metadata { | ||
/// An RPC event that was received from the backend. | ||
RpcEvent(String), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 const
s 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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..
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well spotted! Fixed.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sections missing
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()); |
There was a problem hiding this comment.
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! ❤️
|
||
|
||
(shape + hover_area).into() |
There was a problem hiding this comment.
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', | ||
] |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
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.
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.
Pull Request Description
Expands the profiling tooling
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:
./run dist
and./run watch
.