Skip to content

Commit ae9775c

Browse files
james7132jdmalice-i-cecile
authored
Optimize Event Updates (#12936)
# Objective Improve performance scalability when adding new event types to a Bevy app. Currently, just using Bevy in the default configuration, all apps spend upwards of 100+us in the `First` schedule, every app tick, evaluating if it should update events or not, even if events are not being used for that particular frame, and this scales with the number of Events registered in the app. ## Solution As `Events::update` is guaranteed `O(1)` by just checking if a resource's value, swapping two Vecs, and then clearing one of them, the actual cost of running `event_update_system` is *very* cheap. The overhead of doing system dependency injection, task scheduling ,and the multithreaded executor outweighs the cost of running the system by a large margin. Create an `EventRegistry` resource that keeps a number of function pointers that update each event. Replace the per-event type `event_update_system` with a singular exclusive system uses the `EventRegistry` to update all events instead. Update `SubApp::add_event` to use `EventRegistry` instead. ## Performance This speeds reduces the cost of the `First` schedule in both many_foxes and many_cubes by over 80%. Note this is with system spans on. The majority of this is now context-switching costs from launching `time_system`, which should be mostly eliminated with #12869. ![image](https://github.com/bevyengine/bevy/assets/3137680/037624be-21a2-4dc2-a42f-9d0bfa3e9b4a) The actual `event_update_system` is usually *very* short, using only a few microseconds on average. ![image](https://github.com/bevyengine/bevy/assets/3137680/01ff1689-3595-49b6-8f09-5c44bcf903e8) --- ## Changelog TODO ## Migration Guide TODO --------- Co-authored-by: Josh Matthews <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent 57719fc commit ae9775c

File tree

9 files changed

+122
-68
lines changed

9 files changed

+122
-68
lines changed

crates/bevy_app/src/app.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::{
2-
Main, MainSchedulePlugin, PlaceholderPlugin, Plugin, Plugins, PluginsState, SubApp, SubApps,
2+
First, Main, MainSchedulePlugin, PlaceholderPlugin, Plugin, Plugins, PluginsState, SubApp,
3+
SubApps,
34
};
45
pub use bevy_derive::AppLabel;
56
use bevy_ecs::{
7+
event::event_update_system,
68
intern::Interned,
79
prelude::*,
810
schedule::{ScheduleBuildSettings, ScheduleLabel},
@@ -89,7 +91,12 @@ impl Default for App {
8991
#[cfg(feature = "bevy_reflect")]
9092
app.init_resource::<AppTypeRegistry>();
9193
app.add_plugins(MainSchedulePlugin);
92-
94+
app.add_systems(
95+
First,
96+
event_update_system
97+
.in_set(bevy_ecs::event::EventUpdates)
98+
.run_if(bevy_ecs::event::event_update_condition),
99+
);
93100
app.add_event::<AppExit>();
94101

95102
app
@@ -369,8 +376,6 @@ impl App {
369376
/// #
370377
/// app.add_event::<MyEvent>();
371378
/// ```
372-
///
373-
/// [`event_update_system`]: bevy_ecs::event::event_update_system
374379
pub fn add_event<T>(&mut self) -> &mut Self
375380
where
376381
T: Event,

crates/bevy_app/src/sub_app.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use crate::{App, First, InternedAppLabel, Plugin, Plugins, PluginsState, StateTransition};
1+
use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, StateTransition};
22
use bevy_ecs::{
3+
event::EventRegistry,
34
prelude::*,
45
schedule::{
56
common_conditions::run_once as run_once_condition, run_enter_schedule,
@@ -362,12 +363,7 @@ impl SubApp {
362363
T: Event,
363364
{
364365
if !self.world.contains_resource::<Events<T>>() {
365-
self.init_resource::<Events<T>>().add_systems(
366-
First,
367-
bevy_ecs::event::event_update_system::<T>
368-
.in_set(bevy_ecs::event::EventUpdates)
369-
.run_if(bevy_ecs::event::event_update_condition::<T>),
370-
);
366+
EventRegistry::register_event::<T>(self.world_mut());
371367
}
372368

373369
self

crates/bevy_ecs/examples/events.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
//! In this example a system sends a custom event with a 50/50 chance during any frame.
22
//! If an event was send, it will be printed by the console in a receiving system.
33
4-
use bevy_ecs::prelude::*;
4+
use bevy_ecs::{event::EventRegistry, prelude::*};
55

66
fn main() {
77
// Create a new empty world and add the event as a resource
88
let mut world = World::new();
9-
world.insert_resource(Events::<MyEvent>::default());
9+
// The event registry is stored as a resource, and allows us to quickly update all events at once.
10+
// This call adds both the registry resource and the events resource into the world.
11+
EventRegistry::register_event::<MyEvent>(&mut world);
1012

1113
// Create a schedule to store our systems
1214
let mut schedule = Schedule::default();
@@ -17,7 +19,7 @@ fn main() {
1719
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
1820
pub struct FlushEvents;
1921

20-
schedule.add_systems(bevy_ecs::event::event_update_system::<MyEvent>.in_set(FlushEvents));
22+
schedule.add_systems(bevy_ecs::event::event_update_system.in_set(FlushEvents));
2123

2224
// Add systems sending and receiving events after the events are flushed.
2325
schedule.add_systems((

crates/bevy_ecs/src/change_detection.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,12 @@ impl<'w> MutUntyped<'w> {
833833
}
834834
}
835835

836+
/// Returns `true` if this value was changed or mutably dereferenced
837+
/// either since a specific change tick.
838+
pub fn has_changed_since(&self, tick: Tick) -> bool {
839+
self.ticks.changed.is_newer_than(tick, self.ticks.this_run)
840+
}
841+
836842
/// Returns a pointer to the value without taking ownership of this smart pointer, marking it as changed.
837843
///
838844
/// In order to avoid marking the value as changed, you need to call [`bypass_change_detection`](DetectChangesMut::bypass_change_detection).

crates/bevy_ecs/src/component.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@ impl Components {
765765

766766
/// A value that tracks when a system ran relative to other systems.
767767
/// This is used to power change detection.
768-
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
768+
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
769769
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
770770
pub struct Tick {
771771
tick: u32,

crates/bevy_ecs/src/event.rs

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
//! Event handling types.
22
33
use crate as bevy_ecs;
4-
use crate::system::{Local, Res, ResMut, Resource, SystemParam};
4+
use crate::change_detection::MutUntyped;
5+
use crate::{
6+
change_detection::{DetectChangesMut, Mut},
7+
component::{ComponentId, Tick},
8+
system::{Local, Res, ResMut, Resource, SystemParam},
9+
world::World,
10+
};
511
pub use bevy_ecs_macros::Event;
612
use bevy_ecs_macros::SystemSet;
713
use bevy_utils::detailed_trace;
@@ -253,7 +259,13 @@ impl<E: Event> Events<E> {
253259
///
254260
/// If you need access to the events that were removed, consider using [`Events::update_drain`].
255261
pub fn update(&mut self) {
256-
let _ = self.update_drain();
262+
std::mem::swap(&mut self.events_a, &mut self.events_b);
263+
self.events_b.clear();
264+
self.events_b.start_event_count = self.event_count;
265+
debug_assert_eq!(
266+
self.events_a.start_event_count + self.events_a.len(),
267+
self.events_b.start_event_count
268+
);
257269
}
258270

259271
/// Swaps the event buffers and drains the oldest event buffer, returning an iterator
@@ -798,48 +810,90 @@ impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> {
798810
}
799811
}
800812

801-
#[doc(hidden)]
813+
struct RegisteredEvent {
814+
component_id: ComponentId,
815+
// Required to flush the secondary buffer and drop events even if left unchanged.
816+
previously_updated: bool,
817+
// SAFETY: The component ID and the function must be used to fetch the Events<T> resource
818+
// of the same type initialized in `register_event`, or improper type casts will occur.
819+
update: unsafe fn(MutUntyped),
820+
}
821+
822+
/// A registry of all of the [`Events`] in the [`World`], used by [`event_update_system`]
823+
/// to update all of the events.
802824
#[derive(Resource, Default)]
803-
pub struct EventUpdateSignal(bool);
825+
pub struct EventRegistry {
826+
needs_update: bool,
827+
event_updates: Vec<RegisteredEvent>,
828+
}
829+
830+
impl EventRegistry {
831+
/// Registers an event type to be updated.
832+
pub fn register_event<T: Event>(world: &mut World) {
833+
// By initializing the resource here, we can be sure that it is present,
834+
// and receive the correct, up-to-date `ComponentId` even if it was previously removed.
835+
let component_id = world.init_resource::<Events<T>>();
836+
let mut registry = world.get_resource_or_insert_with(Self::default);
837+
registry.event_updates.push(RegisteredEvent {
838+
component_id,
839+
previously_updated: false,
840+
update: |ptr| {
841+
// SAFETY: The resource was initialized with the type Events<T>.
842+
unsafe { ptr.with_type::<Events<T>>() }
843+
.bypass_change_detection()
844+
.update();
845+
},
846+
});
847+
}
848+
849+
/// Updates all of the registered events in the World.
850+
pub fn run_updates(&mut self, world: &mut World, last_change_tick: Tick) {
851+
for registered_event in &mut self.event_updates {
852+
// Bypass the type ID -> Component ID lookup with the cached component ID.
853+
if let Some(events) = world.get_resource_mut_by_id(registered_event.component_id) {
854+
let has_changed = events.has_changed_since(last_change_tick);
855+
if registered_event.previously_updated || has_changed {
856+
// SAFETY: The update function pointer is called with the resource
857+
// fetched from the same component ID.
858+
unsafe { (registered_event.update)(events) };
859+
// Always set to true if the events have changed, otherwise disable running on the second invocation
860+
// to wait for more changes.
861+
registered_event.previously_updated =
862+
has_changed || !registered_event.previously_updated;
863+
}
864+
}
865+
}
866+
}
867+
}
804868

805869
#[doc(hidden)]
806870
#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)]
807871
pub struct EventUpdates;
808872

809873
/// Signals the [`event_update_system`] to run after `FixedUpdate` systems.
810-
pub fn signal_event_update_system(signal: Option<ResMut<EventUpdateSignal>>) {
811-
if let Some(mut s) = signal {
812-
s.0 = true;
874+
pub fn signal_event_update_system(signal: Option<ResMut<EventRegistry>>) {
875+
if let Some(mut registry) = signal {
876+
registry.needs_update = true;
813877
}
814878
}
815879

816-
/// Resets the `EventUpdateSignal`
817-
pub fn reset_event_update_signal_system(signal: Option<ResMut<EventUpdateSignal>>) {
818-
if let Some(mut s) = signal {
819-
s.0 = false;
820-
}
821-
}
822-
823-
/// A system that calls [`Events::update`].
824-
pub fn event_update_system<T: Event>(
825-
update_signal: Option<Res<EventUpdateSignal>>,
826-
mut events: ResMut<Events<T>>,
827-
) {
828-
if let Some(signal) = update_signal {
829-
// If we haven't got a signal to update the events, but we *could* get such a signal
830-
// return early and update the events later.
831-
if !signal.0 {
832-
return;
833-
}
880+
/// A system that calls [`Events::update`] on all registered [`Events`] in the world.
881+
pub fn event_update_system(world: &mut World, mut last_change_tick: Local<Tick>) {
882+
if world.contains_resource::<EventRegistry>() {
883+
world.resource_scope(|world, mut registry: Mut<EventRegistry>| {
884+
registry.run_updates(world, *last_change_tick);
885+
// Disable the system until signal_event_update_system runs again.
886+
registry.needs_update = false;
887+
});
834888
}
835-
836-
events.update();
889+
*last_change_tick = world.change_tick();
837890
}
838891

839-
/// A run condition that checks if the event's [`event_update_system`]
840-
/// needs to run or not.
841-
pub fn event_update_condition<T: Event>(events: Res<Events<T>>) -> bool {
842-
!events.events_a.is_empty() || !events.events_b.is_empty()
892+
/// A run condition for [`event_update_system`].
893+
pub fn event_update_condition(signal: Option<Res<EventRegistry>>) -> bool {
894+
// If we haven't got a signal to update the events, but we *could* get such a signal
895+
// return early and update the events later.
896+
signal.map_or(false, |signal| signal.needs_update)
843897
}
844898

845899
/// [`Iterator`] over sent [`EventIds`](`EventId`) from a batch.

crates/bevy_ecs/src/schedule/condition.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ pub mod common_conditions {
837837
/// # let mut world = World::new();
838838
/// # world.init_resource::<Counter>();
839839
/// # world.init_resource::<Events<MyEvent>>();
840-
/// # app.add_systems(bevy_ecs::event::event_update_system::<MyEvent>.before(my_system));
840+
/// # app.add_systems(bevy_ecs::event::event_update_system.before(my_system));
841841
///
842842
/// app.add_systems(
843843
/// my_system.run_if(on_event::<MyEvent>()),

crates/bevy_time/src/lib.rs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub mod prelude {
3030
}
3131

3232
use bevy_app::{prelude::*, RunFixedMainLoop};
33-
use bevy_ecs::event::{signal_event_update_system, EventUpdateSignal, EventUpdates};
33+
use bevy_ecs::event::signal_event_update_system;
3434
use bevy_ecs::prelude::*;
3535
use bevy_utils::{tracing::warn, Duration, Instant};
3636
pub use crossbeam_channel::TrySendError;
@@ -57,19 +57,11 @@ impl Plugin for TimePlugin {
5757
.register_type::<Time<Virtual>>()
5858
.register_type::<Time<Fixed>>()
5959
.register_type::<Timer>()
60-
.add_systems(
61-
First,
62-
(time_system, virtual_time_system.after(time_system)).in_set(TimeSystem),
63-
)
60+
.add_systems(First, time_system.in_set(TimeSystem))
6461
.add_systems(RunFixedMainLoop, run_fixed_main_schedule);
6562

6663
// ensure the events are not dropped until `FixedMain` systems can observe them
67-
app.init_resource::<EventUpdateSignal>()
68-
.add_systems(
69-
First,
70-
bevy_ecs::event::reset_event_update_signal_system.after(EventUpdates),
71-
)
72-
.add_systems(FixedPostUpdate, signal_event_update_system);
64+
app.add_systems(FixedPostUpdate, signal_event_update_system);
7365
}
7466
}
7567

@@ -111,7 +103,9 @@ pub fn create_time_channels() -> (TimeSender, TimeReceiver) {
111103
/// The system used to update the [`Time`] used by app logic. If there is a render world the time is
112104
/// sent from there to this system through channels. Otherwise the time is updated in this system.
113105
fn time_system(
114-
mut time: ResMut<Time<Real>>,
106+
mut real_time: ResMut<Time<Real>>,
107+
mut virtual_time: ResMut<Time<Virtual>>,
108+
mut time: ResMut<Time>,
115109
update_strategy: Res<TimeUpdateStrategy>,
116110
time_recv: Option<Res<TimeReceiver>>,
117111
mut has_received_time: Local<bool>,
@@ -132,10 +126,12 @@ fn time_system(
132126
};
133127

134128
match update_strategy.as_ref() {
135-
TimeUpdateStrategy::Automatic => time.update_with_instant(new_time),
136-
TimeUpdateStrategy::ManualInstant(instant) => time.update_with_instant(*instant),
137-
TimeUpdateStrategy::ManualDuration(duration) => time.update_with_duration(*duration),
129+
TimeUpdateStrategy::Automatic => real_time.update_with_instant(new_time),
130+
TimeUpdateStrategy::ManualInstant(instant) => real_time.update_with_instant(*instant),
131+
TimeUpdateStrategy::ManualDuration(duration) => real_time.update_with_duration(*duration),
138132
}
133+
134+
update_virtual_time(&mut time, &mut virtual_time, &real_time);
139135
}
140136

141137
#[cfg(test)]

crates/bevy_time/src/virt.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use bevy_ecs::system::{Res, ResMut};
21
use bevy_reflect::Reflect;
32
use bevy_utils::{tracing::debug, Duration};
43

@@ -268,11 +267,7 @@ impl Default for Virtual {
268267
/// Advances [`Time<Virtual>`] and [`Time`] based on the elapsed [`Time<Real>`].
269268
///
270269
/// The virtual time will be advanced up to the provided [`Time::max_delta`].
271-
pub fn virtual_time_system(
272-
mut current: ResMut<Time>,
273-
mut virt: ResMut<Time<Virtual>>,
274-
real: Res<Time<Real>>,
275-
) {
270+
pub fn update_virtual_time(current: &mut Time, virt: &mut Time<Virtual>, real: &Time<Real>) {
276271
let raw_delta = real.delta();
277272
virt.advance_with_raw_delta(raw_delta);
278273
*current = virt.as_generic();

0 commit comments

Comments
 (0)