
# Objective I was trying to add some `Diagnostics` to have a better break down of performance but I noticed that the current implementation uses a `ResMut` which forces the functions to all run sequentially whereas before they could run in parallel. This created too great a performance penalty to be usable. ## Solution This PR reworks how the diagnostics work with a couple of breaking changes. The idea is to change how `Diagnostics` works by changing it to a `SystemParam`. This allows us to hold a `Deferred` buffer of measurements that can be applied later, avoiding the need for multiple mutable references to the hashmap. This means we can run systems that write diagnostic measurements in parallel. Firstly, we rename the old `Diagnostics` to `DiagnosticsStore`. This clears up the original name for the new interface while allowing us to preserve more closely the original API. Then we create a new `Diagnostics` struct which implements `SystemParam` and contains a deferred `SystemBuffer`. This can be used very similar to the old `Diagnostics` for writing new measurements. ```rust fn system(diagnostics: ResMut<Diagnostics>) { diagnostics.new_measurement(ID, || 10.0)} // changes to fn system(mut diagnostics: Diagnostics) { diagnostics.new_measurement(ID, || 10.0)} ``` For reading the diagnostics, the user needs to change from `Diagnostics` to `DiagnosticsStore` but otherwise the function calls are the same. Finally, we add a new method to the `App` for registering diagnostics. This replaces the old method of creating a startup system and adding it manually. Testing it, this PR does indeed allow Diagnostic systems to be run in parallel. ## Changelog - Change `Diagnostics` to implement `SystemParam` which allows diagnostic systems to run in parallel. ## Migration Guide - Register `Diagnostic`'s using the new `app.register_diagnostic(Diagnostic::new(DIAGNOSTIC_ID, "diagnostic_name", 10));` - In systems for writing new measurements, change `mut diagnostics: ResMut<Diagnostics>` to `mut diagnostics: Diagnostics` to allow the systems to run in parallel. - In systems for reading measurements, change `diagnostics: Res<Diagnostics>` to `diagnostics: Res<DiagnosticsStore>`.
133 lines
4.3 KiB
Rust
133 lines
4.3 KiB
Rust
use super::{Diagnostic, DiagnosticId, DiagnosticsStore};
|
|
use bevy_app::prelude::*;
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_log::{debug, info};
|
|
use bevy_time::{Time, Timer, TimerMode};
|
|
use bevy_utils::Duration;
|
|
|
|
/// An App Plugin that logs diagnostics to the console
|
|
pub struct LogDiagnosticsPlugin {
|
|
pub debug: bool,
|
|
pub wait_duration: Duration,
|
|
pub filter: Option<Vec<DiagnosticId>>,
|
|
}
|
|
|
|
/// State used by the [`LogDiagnosticsPlugin`]
|
|
#[derive(Resource)]
|
|
struct LogDiagnosticsState {
|
|
timer: Timer,
|
|
filter: Option<Vec<DiagnosticId>>,
|
|
}
|
|
|
|
impl Default for LogDiagnosticsPlugin {
|
|
fn default() -> Self {
|
|
LogDiagnosticsPlugin {
|
|
debug: false,
|
|
wait_duration: Duration::from_secs(1),
|
|
filter: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Plugin for LogDiagnosticsPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.insert_resource(LogDiagnosticsState {
|
|
timer: Timer::new(self.wait_duration, TimerMode::Repeating),
|
|
filter: self.filter.clone(),
|
|
});
|
|
|
|
if self.debug {
|
|
app.add_systems(PostUpdate, Self::log_diagnostics_debug_system);
|
|
} else {
|
|
app.add_systems(PostUpdate, Self::log_diagnostics_system);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LogDiagnosticsPlugin {
|
|
pub fn filtered(filter: Vec<DiagnosticId>) -> Self {
|
|
LogDiagnosticsPlugin {
|
|
filter: Some(filter),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn log_diagnostic(diagnostic: &Diagnostic) {
|
|
if let Some(value) = diagnostic.smoothed() {
|
|
if diagnostic.get_max_history_length() > 1 {
|
|
if let Some(average) = diagnostic.average() {
|
|
info!(
|
|
target: "bevy diagnostic",
|
|
// Suffix is only used for 's' or 'ms' currently,
|
|
// so we reserve two columns for it; however,
|
|
// Do not reserve columns for the suffix in the average
|
|
// The ) hugging the value is more aesthetically pleasing
|
|
"{name:<name_width$}: {value:>11.6}{suffix:2} (avg {average:>.6}{suffix:})",
|
|
name = diagnostic.name,
|
|
suffix = diagnostic.suffix,
|
|
name_width = crate::MAX_DIAGNOSTIC_NAME_WIDTH,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
info!(
|
|
target: "bevy diagnostic",
|
|
"{name:<name_width$}: {value:>.6}{suffix:}",
|
|
name = diagnostic.name,
|
|
suffix = diagnostic.suffix,
|
|
name_width = crate::MAX_DIAGNOSTIC_NAME_WIDTH,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn log_diagnostics_system(
|
|
mut state: ResMut<LogDiagnosticsState>,
|
|
time: Res<Time>,
|
|
diagnostics: Res<DiagnosticsStore>,
|
|
) {
|
|
if state.timer.tick(time.raw_delta()).finished() {
|
|
if let Some(ref filter) = state.filter {
|
|
for diagnostic in filter.iter().flat_map(|id| {
|
|
diagnostics
|
|
.get(*id)
|
|
.filter(|diagnostic| diagnostic.is_enabled)
|
|
}) {
|
|
Self::log_diagnostic(diagnostic);
|
|
}
|
|
} else {
|
|
for diagnostic in diagnostics
|
|
.iter()
|
|
.filter(|diagnostic| diagnostic.is_enabled)
|
|
{
|
|
Self::log_diagnostic(diagnostic);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn log_diagnostics_debug_system(
|
|
mut state: ResMut<LogDiagnosticsState>,
|
|
time: Res<Time>,
|
|
diagnostics: Res<DiagnosticsStore>,
|
|
) {
|
|
if state.timer.tick(time.raw_delta()).finished() {
|
|
if let Some(ref filter) = state.filter {
|
|
for diagnostic in filter.iter().flat_map(|id| {
|
|
diagnostics
|
|
.get(*id)
|
|
.filter(|diagnostic| diagnostic.is_enabled)
|
|
}) {
|
|
debug!("{:#?}\n", diagnostic);
|
|
}
|
|
} else {
|
|
for diagnostic in diagnostics
|
|
.iter()
|
|
.filter(|diagnostic| diagnostic.is_enabled)
|
|
{
|
|
debug!("{:#?}\n", diagnostic);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|