Skip to content
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

Observers overhaul: statically-typed multi-event-listening and safer dynamic-event-listening #14674

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e2562d8
initial implementation of multi event observers
ItsDoot Aug 9, 2024
41345f0
fix some clippy lints
ItsDoot Aug 9, 2024
8886f7c
make the old multiple event test work again
ItsDoot Aug 9, 2024
ebc3207
reimplement Trigger::event, Trigger::event_mut, Trigger::event_ptr
ItsDoot Aug 9, 2024
85178f5
recursive event set support and macro generated impls up to n=8
ItsDoot Aug 9, 2024
eec60db
add untyped wrapper that foregoes casting and safety checks, replacin…
ItsDoot Aug 9, 2024
516a3b7
The non-empty Untyped<(A, B)> impl should still only match on A, B, etc
ItsDoot Aug 9, 2024
da07929
Observer::with_event is no longer unsafe, because we always check tha…
ItsDoot Aug 9, 2024
be65bf3
performance reductions means reintroducing some unsafe blocks
ItsDoot Aug 10, 2024
a0774d6
remove Trigger::event_ptr in favor of UntypedEvent event set
ItsDoot Aug 10, 2024
9a41de5
add safety comments
ItsDoot Aug 10, 2024
a2b65e4
remove unused import
ItsDoot Aug 10, 2024
954b1b7
fix docs
ItsDoot Aug 10, 2024
3000906
flip the naming for the with_events so that the unsafe version is longer
ItsDoot Aug 10, 2024
26307ec
add observer propagation tests for multi-events
ItsDoot Aug 10, 2024
7e32e5c
add additional check to propagating between test
ItsDoot Aug 10, 2024
b0519ed
rename untyped tests to be more clear, and change todo comment
ItsDoot Aug 10, 2024
195de90
clarify safety comment and add macro generated impls up to 15
ItsDoot Aug 10, 2024
294fd03
add benchmarks
ItsDoot Aug 10, 2024
6434cd8
log an error when the user incorrectly used with_event_unchecked
ItsDoot Aug 10, 2024
a7cc2c6
Make Observer::with_event call with_event_unchecked internally, to ma…
ItsDoot Aug 10, 2024
a5d6f04
fix up error message
ItsDoot Aug 10, 2024
5c8c516
fix safety comment
ItsDoot Aug 10, 2024
6379077
Both UntypedEvent implementations should match all event types
ItsDoot Aug 10, 2024
bdd6c33
fix lints
ItsDoot Aug 11, 2024
bd139ff
add ability to do mixed statically-known and runtime-known observers …
ItsDoot Aug 11, 2024
2af5fef
remove footgun by forbiding the nesting of DynamicEvent or SemiDynami…
ItsDoot Aug 11, 2024
cad10a3
rearrange tests
ItsDoot Aug 11, 2024
b6fb8dc
update benchmarks
ItsDoot Aug 11, 2024
5f9cbed
add safety comment
ItsDoot Aug 11, 2024
a84c60e
clarify safety comment
ItsDoot Aug 11, 2024
8b82d59
docs first pass
ItsDoot Aug 11, 2024
404e4ec
fix indentation
ItsDoot Aug 11, 2024
0f84b69
Merge branch 'main' into feat/observer-multi-event
ItsDoot Aug 11, 2024
caa5dd8
remove unncessary Register parameters from DynamicEvent and SemiDynam…
ItsDoot Aug 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions benches/benches/bevy_ecs/observers/dynamic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use bevy_ecs::{
event::Event,
observer::{DynamicEvent, EmitDynamicTrigger, EventSet, Observer, Trigger},
world::{Command, World},
};
use criterion::{black_box, Criterion};

pub fn observe_dynamic(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("observe_dynamic");
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(4));

group.bench_function("1", |bencher| {
let mut world = World::new();
let event_id_1 = world.init_component::<TestEvent<1>>();
world.spawn(Observer::new(empty_listener_set::<DynamicEvent>).with_event(event_id_1));
bencher.iter(|| {
for _ in 0..10000 {
unsafe {
EmitDynamicTrigger::new_with_id(event_id_1, TestEvent::<1>, ())
.apply(&mut world)
};
}
});
});
group.bench_function("2", |bencher| {
let mut world = World::new();
let event_id_1 = world.init_component::<TestEvent<1>>();
let event_id_2 = world.init_component::<TestEvent<2>>();
world.spawn(
Observer::new(empty_listener_set::<DynamicEvent>)
.with_event(event_id_1)
.with_event(event_id_2),
);
bencher.iter(|| {
for _ in 0..10000 {
unsafe {
EmitDynamicTrigger::new_with_id(event_id_2, TestEvent::<2>, ())
.apply(&mut world)
};
}
});
});
}

#[derive(Event)]
struct TestEvent<const N: usize>;

fn empty_listener_set<Set: EventSet>(trigger: Trigger<Set>) {
black_box(trigger);
}
15 changes: 14 additions & 1 deletion benches/benches/bevy_ecs/observers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
use criterion::criterion_group;

mod dynamic;
mod multievent;
mod propagation;
mod semidynamic;
mod simple;
use dynamic::*;
use multievent::*;
use propagation::*;
use semidynamic::*;
use simple::*;

criterion_group!(observer_benches, event_propagation, observe_simple);
criterion_group!(
observer_benches,
event_propagation,
observe_simple,
observe_multievent,
observe_dynamic,
observe_semidynamic
);
105 changes: 105 additions & 0 deletions benches/benches/bevy_ecs/observers/multievent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use bevy_ecs::{
component::Component,
event::Event,
observer::{EventSet, Observer, Trigger},
world::World,
};
use criterion::{black_box, measurement::WallTime, BenchmarkGroup, Criterion};

pub fn observe_multievent(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("observe_multievent");
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(4));

group.bench_function("trigger_single", |bencher| {
let mut world = World::new();
world.observe(empty_listener_set::<TestEvent<1>>);
bencher.iter(|| {
for _ in 0..10000 {
world.trigger(TestEvent::<1>);
}
});
});

bench_in_set::<1, (TestEvent<1>,)>(&mut group);
bench_in_set::<2, (TestEvent<1>, TestEvent<2>)>(&mut group);
bench_in_set::<4, (TestEvent<1>, TestEvent<2>, TestEvent<3>, TestEvent<4>)>(&mut group);
bench_in_set::<
8,
(
TestEvent<1>,
TestEvent<2>,
TestEvent<3>,
TestEvent<4>,
TestEvent<5>,
TestEvent<6>,
TestEvent<7>,
TestEvent<8>,
),
>(&mut group);
bench_in_set::<
12,
(
TestEvent<1>,
TestEvent<2>,
TestEvent<3>,
TestEvent<4>,
TestEvent<5>,
TestEvent<6>,
TestEvent<7>,
TestEvent<8>,
TestEvent<9>,
TestEvent<10>,
TestEvent<11>,
TestEvent<12>,
),
>(&mut group);
bench_in_set::<
15,
(
TestEvent<1>,
TestEvent<2>,
TestEvent<3>,
TestEvent<4>,
TestEvent<5>,
TestEvent<6>,
TestEvent<7>,
TestEvent<8>,
TestEvent<9>,
TestEvent<10>,
TestEvent<11>,
TestEvent<12>,
TestEvent<13>,
TestEvent<14>,
TestEvent<15>,
),
>(&mut group);
}

fn bench_in_set<const LAST: usize, Set: EventSet>(group: &mut BenchmarkGroup<WallTime>) {
group.bench_function(format!("trigger_first/{LAST}"), |bencher| {
let mut world = World::new();
world.observe(empty_listener_set::<Set>);
bencher.iter(|| {
for _ in 0..10000 {
world.trigger(TestEvent::<1>);
}
});
});
group.bench_function(format!("trigger_last/{LAST}"), |bencher| {
let mut world = World::new();
world.observe(empty_listener_set::<Set>);
bencher.iter(|| {
for _ in 0..10000 {
world.trigger(TestEvent::<LAST>);
}
});
});
}

#[derive(Event)]
struct TestEvent<const N: usize>;

fn empty_listener_set<Set: EventSet>(trigger: Trigger<Set>) {
black_box(trigger);
}
183 changes: 183 additions & 0 deletions benches/benches/bevy_ecs/observers/semidynamic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use bevy_ecs::{
event::Event,
observer::{EmitDynamicTrigger, EventSet, Observer, SemiDynamicEvent, Trigger},
world::{Command, World},
};
use criterion::{black_box, Criterion};

pub fn observe_semidynamic(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("observe_semidynamic");
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(4));

group.bench_function("static/1s-1d", |bencher| {
let mut world = World::new();
let event_id_1 = world.init_component::<Dynamic<1>>();
world.spawn(
Observer::new(empty_listener_set::<SemiDynamicEvent<Static<1>>>).with_event(event_id_1),
);

bencher.iter(|| {
for _ in 0..10000 {
world.trigger(Static::<1>);
}
});
});
group.bench_function("dynamic/1s-1d", |bencher| {
let mut world = World::new();
let event_id_1 = world.init_component::<Dynamic<1>>();
world.spawn(
Observer::new(empty_listener_set::<SemiDynamicEvent<Static<1>>>).with_event(event_id_1),
);

bencher.iter(|| {
for _ in 0..10000 {
unsafe {
EmitDynamicTrigger::new_with_id(event_id_1, Dynamic::<1>, ()).apply(&mut world)
};
}
});
});

group.bench_function("static/15s-15d", |bencher| {
// Aint she perdy?
let mut world = World::new();
let event_id_1 = world.init_component::<Dynamic<1>>();
let event_id_2 = world.init_component::<Dynamic<2>>();
let event_id_3 = world.init_component::<Dynamic<3>>();
let event_id_4 = world.init_component::<Dynamic<4>>();
let event_id_5 = world.init_component::<Dynamic<5>>();
let event_id_6 = world.init_component::<Dynamic<6>>();
let event_id_7 = world.init_component::<Dynamic<7>>();
let event_id_8 = world.init_component::<Dynamic<8>>();
let event_id_9 = world.init_component::<Dynamic<9>>();
let event_id_10 = world.init_component::<Dynamic<10>>();
let event_id_11 = world.init_component::<Dynamic<11>>();
let event_id_12 = world.init_component::<Dynamic<12>>();
let event_id_13 = world.init_component::<Dynamic<13>>();
let event_id_14 = world.init_component::<Dynamic<14>>();
let event_id_15 = world.init_component::<Dynamic<15>>();
world.spawn(
Observer::new(
empty_listener_set::<
SemiDynamicEvent<(
Static<1>,
Static<2>,
Static<3>,
Static<4>,
Static<5>,
Static<6>,
Static<7>,
Static<8>,
Static<9>,
Static<10>,
Static<11>,
Static<12>,
Static<13>,
Static<14>,
Static<15>,
)>,
>,
)
.with_event(event_id_1)
.with_event(event_id_2)
.with_event(event_id_3)
.with_event(event_id_4)
.with_event(event_id_5)
.with_event(event_id_6)
.with_event(event_id_7)
.with_event(event_id_8)
.with_event(event_id_9)
.with_event(event_id_10)
.with_event(event_id_11)
.with_event(event_id_12)
.with_event(event_id_13)
.with_event(event_id_14)
.with_event(event_id_15),
);

bencher.iter(|| {
for _ in 0..10000 {
world.trigger(Static::<14>);
}
});
});
group.bench_function("dynamic/15s-15d", |bencher| {
// Aint she perdy?
let mut world = World::new();
let event_id_1 = world.init_component::<Dynamic<1>>();
let event_id_2 = world.init_component::<Dynamic<2>>();
let event_id_3 = world.init_component::<Dynamic<3>>();
let event_id_4 = world.init_component::<Dynamic<4>>();
let event_id_5 = world.init_component::<Dynamic<5>>();
let event_id_6 = world.init_component::<Dynamic<6>>();
let event_id_7 = world.init_component::<Dynamic<7>>();
let event_id_8 = world.init_component::<Dynamic<8>>();
let event_id_9 = world.init_component::<Dynamic<9>>();
let event_id_10 = world.init_component::<Dynamic<10>>();
let event_id_11 = world.init_component::<Dynamic<11>>();
let event_id_12 = world.init_component::<Dynamic<12>>();
let event_id_13 = world.init_component::<Dynamic<13>>();
let event_id_14 = world.init_component::<Dynamic<14>>();
let event_id_15 = world.init_component::<Dynamic<15>>();
world.spawn(
Observer::new(
empty_listener_set::<
SemiDynamicEvent<(
Static<1>,
Static<2>,
Static<3>,
Static<4>,
Static<5>,
Static<6>,
Static<7>,
Static<8>,
Static<9>,
Static<10>,
Static<11>,
Static<12>,
Static<13>,
Static<14>,
Static<15>,
)>,
>,
)
.with_event(event_id_1)
.with_event(event_id_2)
.with_event(event_id_3)
.with_event(event_id_4)
.with_event(event_id_5)
.with_event(event_id_6)
.with_event(event_id_7)
.with_event(event_id_8)
.with_event(event_id_9)
.with_event(event_id_10)
.with_event(event_id_11)
.with_event(event_id_12)
.with_event(event_id_13)
.with_event(event_id_14)
.with_event(event_id_15),
);

bencher.iter(|| {
for _ in 0..10000 {
unsafe {
EmitDynamicTrigger::new_with_id(event_id_14, Dynamic::<14>, ())
.apply(&mut world)
};
}
});
});
}

/// Static event type
#[derive(Event)]
struct Static<const N: usize>;

/// Dynamic event type
#[derive(Event)]
struct Dynamic<const N: usize>;

fn empty_listener_set<Set: EventSet>(trigger: Trigger<Set>) {
black_box(trigger);
}
3 changes: 2 additions & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub use bevy_derive::AppLabel;
use bevy_ecs::{
event::{event_update_system, EventCursor},
intern::Interned,
observer::EventSet,
prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel},
system::{IntoObserverSystem, SystemId},
Expand Down Expand Up @@ -990,7 +991,7 @@ impl App {
}

/// Spawns an [`Observer`] entity, which will watch for and respond to the given event.
pub fn observe<E: Event, B: Bundle, M>(
pub fn observe<E: EventSet, B: Bundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
Expand Down
Loading