From 3507b21dce8278dc64ffd15f533a04e195efad86 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Mon, 5 Jun 2023 21:51:22 +0100 Subject: [PATCH] Allow systems using Diagnostics to run in parallel (#8677) # 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.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` to `mut diagnostics: Diagnostics` to allow the systems to run in parallel. - In systems for reading measurements, change `diagnostics: Res` to `diagnostics: Res`. --- .../asset_count_diagnostics_plugin.rs | 8 +- crates/bevy_diagnostic/src/diagnostic.rs | 102 ++++++++++++++---- .../src/entity_count_diagnostics_plugin.rs | 12 +-- .../src/frame_time_diagnostics_plugin.rs | 21 ++-- crates/bevy_diagnostic/src/lib.rs | 2 +- .../src/log_diagnostics_plugin.rs | 6 +- .../system_information_diagnostics_plugin.rs | 6 +- examples/diagnostics/custom_diagnostic.rs | 20 ++-- examples/stress_tests/bevymark.rs | 4 +- examples/stress_tests/many_gizmos.rs | 4 +- examples/ui/text.rs | 7 +- examples/ui/text_debug.rs | 4 +- 12 files changed, 125 insertions(+), 71 deletions(-) diff --git a/crates/bevy_asset/src/diagnostic/asset_count_diagnostics_plugin.rs b/crates/bevy_asset/src/diagnostic/asset_count_diagnostics_plugin.rs index a5ce2546db..dafd215673 100644 --- a/crates/bevy_asset/src/diagnostic/asset_count_diagnostics_plugin.rs +++ b/crates/bevy_asset/src/diagnostic/asset_count_diagnostics_plugin.rs @@ -1,6 +1,8 @@ use crate::{Asset, Assets}; use bevy_app::prelude::*; -use bevy_diagnostic::{Diagnostic, DiagnosticId, Diagnostics, MAX_DIAGNOSTIC_NAME_WIDTH}; +use bevy_diagnostic::{ + Diagnostic, DiagnosticId, Diagnostics, DiagnosticsStore, MAX_DIAGNOSTIC_NAME_WIDTH, +}; use bevy_ecs::prelude::*; /// Adds an asset count diagnostic to an [`App`] for assets of type `T`. @@ -32,7 +34,7 @@ impl AssetCountDiagnosticsPlugin { } /// Registers the asset count diagnostic for the current application. - pub fn setup_system(mut diagnostics: ResMut) { + pub fn setup_system(mut diagnostics: ResMut) { let asset_type_name = std::any::type_name::(); let max_length = MAX_DIAGNOSTIC_NAME_WIDTH - "asset_count ".len(); diagnostics.add(Diagnostic::new( @@ -52,7 +54,7 @@ impl AssetCountDiagnosticsPlugin { } /// Updates the asset count of `T` assets. - pub fn diagnostic_system(mut diagnostics: ResMut, assets: Res>) { + pub fn diagnostic_system(mut diagnostics: Diagnostics, assets: Res>) { diagnostics.add_measurement(Self::diagnostic_id(), || assets.len() as f64); } } diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index aa2c868258..b7f7192dc1 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -1,4 +1,5 @@ -use bevy_ecs::system::Resource; +use bevy_app::App; +use bevy_ecs::system::{Deferred, Res, Resource, SystemBuffer, SystemParam}; use bevy_log::warn; use bevy_utils::{Duration, Instant, StableHashMap, Uuid}; use std::{borrow::Cow, collections::VecDeque}; @@ -44,16 +45,14 @@ pub struct Diagnostic { } impl Diagnostic { - /// Add a new value as a [`DiagnosticMeasurement`]. Its timestamp will be [`Instant::now`]. - pub fn add_measurement(&mut self, value: f64) { - let time = Instant::now(); - + /// Add a new value as a [`DiagnosticMeasurement`]. + pub fn add_measurement(&mut self, measurement: DiagnosticMeasurement) { if let Some(previous) = self.measurement() { - let delta = (time - previous.time).as_secs_f64(); + let delta = (measurement.time - previous.time).as_secs_f64(); let alpha = (delta / self.ema_smoothing_factor).clamp(0.0, 1.0); - self.ema += alpha * (value - self.ema); + self.ema += alpha * (measurement.value - self.ema); } else { - self.ema = value; + self.ema = measurement.value; } if self.max_history_length > 1 { @@ -63,14 +62,13 @@ impl Diagnostic { } } - self.sum += value; + self.sum += measurement.value; } else { self.history.clear(); - self.sum = value; + self.sum = measurement.value; } - self.history - .push_back(DiagnosticMeasurement { time, value }); + self.history.push_back(measurement); } /// Create a new diagnostic with the given ID, name and maximum history. @@ -199,14 +197,16 @@ impl Diagnostic { /// A collection of [Diagnostic]s #[derive(Debug, Default, Resource)] -pub struct Diagnostics { +pub struct DiagnosticsStore { // This uses a [`StableHashMap`] to ensure that the iteration order is deterministic between // runs when all diagnostics are inserted in the same order. diagnostics: StableHashMap, } -impl Diagnostics { +impl DiagnosticsStore { /// Add a new [`Diagnostic`]. + /// + /// If possible, prefer calling [`App::register_diagnostic`]. pub fn add(&mut self, diagnostic: Diagnostic) { self.diagnostics.insert(diagnostic.id, diagnostic); } @@ -227,6 +227,20 @@ impl Diagnostics { .and_then(|diagnostic| diagnostic.measurement()) } + /// Return an iterator over all [`Diagnostic`]. + pub fn iter(&self) -> impl Iterator { + self.diagnostics.values() + } +} + +/// Record new [`DiagnosticMeasurement`]'s. +#[derive(SystemParam)] +pub struct Diagnostics<'w, 's> { + store: Res<'w, DiagnosticsStore>, + queue: Deferred<'s, DiagnosticsBuffer>, +} + +impl<'w, 's> Diagnostics<'w, 's> { /// Add a measurement to an enabled [`Diagnostic`]. The measurement is passed as a function so that /// it will be evaluated only if the [`Diagnostic`] is enabled. This can be useful if the value is /// costly to calculate. @@ -234,17 +248,63 @@ impl Diagnostics { where F: FnOnce() -> f64, { - if let Some(diagnostic) = self - .diagnostics - .get_mut(&id) + if self + .store + .get(id) .filter(|diagnostic| diagnostic.is_enabled) + .is_some() { - diagnostic.add_measurement(value()); + let measurement = DiagnosticMeasurement { + time: Instant::now(), + value: value(), + }; + self.queue.0.insert(id, measurement); } } +} - /// Return an iterator over all [`Diagnostic`]. - pub fn iter(&self) -> impl Iterator { - self.diagnostics.values() +#[derive(Default)] +struct DiagnosticsBuffer(StableHashMap); + +impl SystemBuffer for DiagnosticsBuffer { + fn apply( + &mut self, + _system_meta: &bevy_ecs::system::SystemMeta, + world: &mut bevy_ecs::world::World, + ) { + let mut diagnostics = world.resource_mut::(); + for (id, measurement) in self.0.drain() { + if let Some(diagnostic) = diagnostics.get_mut(id) { + diagnostic.add_measurement(measurement); + } + } + } +} + +/// Extend [`App`] with new `register_diagnostic` function. +pub trait RegisterDiagnostic { + fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self; +} + +impl RegisterDiagnostic for App { + /// Register a new [`Diagnostic`] with an [`App`]. + /// + /// ```rust + /// use bevy_app::App; + /// use bevy_diagnostic::{Diagnostic, DiagnosticsPlugin, DiagnosticId, RegisterDiagnostic}; + /// + /// const UNIQUE_DIAG_ID: DiagnosticId = DiagnosticId::from_u128(42); + /// + /// App::new() + /// .add_plugin(DiagnosticsPlugin) + /// // Must only be called after the `DiagnosticsPlugin` has been added. + /// .register_diagnostic(Diagnostic::new(UNIQUE_DIAG_ID, "example", 10)) + /// .run(); + /// ``` + fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self { + let mut diagnostics = self.world.resource_mut::(); + diagnostics.add(diagnostic); + + self } } diff --git a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs index 5c4415c09b..8999163aa9 100644 --- a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs @@ -1,7 +1,7 @@ use bevy_app::prelude::*; -use bevy_ecs::{entity::Entities, prelude::*}; +use bevy_ecs::entity::Entities; -use crate::{Diagnostic, DiagnosticId, Diagnostics}; +use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic}; /// Adds "entity count" diagnostic to an App #[derive(Default)] @@ -9,7 +9,7 @@ pub struct EntityCountDiagnosticsPlugin; impl Plugin for EntityCountDiagnosticsPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, Self::setup_system) + app.register_diagnostic(Diagnostic::new(Self::ENTITY_COUNT, "entity_count", 20)) .add_systems(Update, Self::diagnostic_system); } } @@ -18,11 +18,7 @@ impl EntityCountDiagnosticsPlugin { pub const ENTITY_COUNT: DiagnosticId = DiagnosticId::from_u128(187513512115068938494459732780662867798); - pub fn setup_system(mut diagnostics: ResMut) { - diagnostics.add(Diagnostic::new(Self::ENTITY_COUNT, "entity_count", 20)); - } - - pub fn diagnostic_system(mut diagnostics: ResMut, entities: &Entities) { + pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) { diagnostics.add_measurement(Self::ENTITY_COUNT, || entities.len() as f64); } } diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index 4dbc3c4cfa..a17a6a19d4 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -1,4 +1,4 @@ -use crate::{Diagnostic, DiagnosticId, Diagnostics}; +use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic}; use bevy_app::prelude::*; use bevy_core::FrameCount; use bevy_ecs::prelude::*; @@ -10,8 +10,14 @@ pub struct FrameTimeDiagnosticsPlugin; impl Plugin for FrameTimeDiagnosticsPlugin { fn build(&self, app: &mut bevy_app::App) { - app.add_systems(Startup, Self::setup_system) - .add_systems(Update, Self::diagnostic_system); + app.register_diagnostic( + Diagnostic::new(Self::FRAME_TIME, "frame_time", 20).with_suffix("ms"), + ) + .register_diagnostic(Diagnostic::new(Self::FPS, "fps", 20)) + .register_diagnostic( + Diagnostic::new(Self::FRAME_COUNT, "frame_count", 1).with_smoothing_factor(0.0), + ) + .add_systems(Update, Self::diagnostic_system); } } @@ -22,15 +28,8 @@ impl FrameTimeDiagnosticsPlugin { pub const FRAME_TIME: DiagnosticId = DiagnosticId::from_u128(73441630925388532774622109383099159699); - pub fn setup_system(mut diagnostics: ResMut) { - diagnostics.add(Diagnostic::new(Self::FRAME_TIME, "frame_time", 20).with_suffix("ms")); - diagnostics.add(Diagnostic::new(Self::FPS, "fps", 20)); - diagnostics - .add(Diagnostic::new(Self::FRAME_COUNT, "frame_count", 1).with_smoothing_factor(0.0)); - } - pub fn diagnostic_system( - mut diagnostics: ResMut, + mut diagnostics: Diagnostics, time: Res