Allow systems using Diagnostics to run in parallel#8677
Allow systems using Diagnostics to run in parallel#8677alice-i-cecile merged 8 commits intobevyengine:mainfrom
Conversation
To use Diagnostics a system was required to ask for a ResMut of the Diagnostics struct. This meant that any system writing a Diagnostic couldn't run in parallel (only a single mut instance). Instead, we can use channels to "send" the new metric without requiring mutability and then have a separate system that receives and integrates them at the end.
|
Would it be possible using the |
That seems like a nicer option if it works, I can test it out. I'm not sure about is how that will work with the |
|
@nicopap I'm not that familiar with Rather than have a system require A dummy implementation would be something like this (in actuality I would assume the structs would be merged with existing ones). #[derive(SystemParam)]
struct DiagnosticsParam<'s, F>
where
F: FnOnce() -> f64,
{
queue: Deferred<'s, DiagnosticsBuffer<F>>,
}
impl<'s, F> DiagnosticsParam<'s, F>
where
F: FnOnce() -> f64,
{
pub fn add_measurement(&mut self, id: DiagnosticId, value: F) {
self.queue.0.insert(id, value)
}
}
#[derive(Default)]
struct DiagnosticsBuffer<F: FnOnce() -> f64>(StableHashMap<DiagnosticId, F>);
impl<F> SystemBuffer for DiagnosticsBuffer<F>
where
F: FnOnce() -> f64,
{
fn apply(
&mut self,
system_meta: &bevy_ecs::system::SystemMeta,
world: &mut bevy_ecs::world::World,
) {
let mut diagnostics = world.resource_mut::<Diagnostics>();
for (id, value) in self.0.into_iter() {
diagnostics.add_measurement(id, value)
}
}
}This doesn't like the function pointer that is part of the |
|
You can get the edit: To be clear, you need to drop the |
|
Ok, I updated the PR to use a Deferred SystemParam and I verified it does run in parallel. The user experience has been hurt a little as now there are two structs they need to care about. The Once we are happy with the code the documentation will also need a brush up. @nicopap anything else I'm missing? |
|
Final update for now. To try and fix the "usability" I did a little renaming and added a new trait helper method. Now the API looks more like it used to for the write case, and only differs for the read. fn main() {
App::new()
...
.register_diagnostic(Diagnostic::new(SYSTEM_ITERATION_COUNT, "system_iteration_count", 10))
.add_systems(Update, my_system)
.run();
}
fn my_system(mut diagnostics: Diagnostics) {
// Add a measurement of 10.0 for our diagnostic each time this system runs.
diagnostics.add_measurement(SYSTEM_ITERATION_COUNT, || 10.0);
}To get this I renamed The Note that I added the |
nicopap
left a comment
There was a problem hiding this comment.
Really nice work, just a small question on Diagnostic::add_measurement
| 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, value: impl Into<DiagnosticMeasurement>) { |
There was a problem hiding this comment.
I'd prefer if this took a DiagnosticMeasurement over an Into<DiagnosticMeasurement>. Or even be made private. It's breaking, but using this method directly is footgunny, so better hint through the type that you aren't supposed to add the measurement through this API.
There was a problem hiding this comment.
I removed the Into<> and just use the DiagnosticMeasurement directly. I didn't make it private just because it seems like some people might be using the Diagnostic struct without Diagnostics and I erred on the side of reducing API changes. I'm happy to change it to private if preferred.
There was a problem hiding this comment.
I trust your judgment! I think It's fine to leave it public, especially with the new doc infos you added.
nicopap
left a comment
There was a problem hiding this comment.
Great work! I'd like to see this merged.
|
Well documented, and a nice use of |
Objective
I was trying to add some
Diagnosticsto have a better break down of performance but I noticed that the current implementation uses aResMutwhich 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
Diagnosticsworks by changing it to aSystemParam. This allows us to hold aDeferredbuffer 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
DiagnosticstoDiagnosticsStore. 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
Diagnosticsstruct which implementsSystemParamand contains a deferredSystemBuffer. This can be used very similar to the oldDiagnosticsfor writing new measurements.For reading the diagnostics, the user needs to change from
DiagnosticstoDiagnosticsStorebut otherwise the function calls are the same.Finally, we add a new method to the
Appfor 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
Diagnosticsto implementSystemParamwhich allows diagnostic systems to run in parallel.Migration Guide
Diagnostic's using the newapp.register_diagnostic(Diagnostic::new(DIAGNOSTIC_ID, "diagnostic_name", 10));mut diagnostics: ResMut<Diagnostics>tomut diagnostics: Diagnosticsto allow the systems to run in parallel.diagnostics: Res<Diagnostics>todiagnostics: Res<DiagnosticsStore>.