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

Minimal Bubbling Observers #13991

Merged
merged 21 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2570,6 +2570,17 @@ description = "Demonstrates observers that react to events (both built-in life-c
category = "ECS (Entity Component System)"
wasm = true

[[example]]
name = "observer_propagation"
path = "examples/ecs/observer_propagation.rs"
doc-scrape-examples = true

[package.metadata.example.observer_propagation]
name = "Observer Propagation"
description = "Demonstrates event propagation with observers"
category = "ECS (Entity Component System)"
wasm = true

[[example]]
name = "3d_rotation"
path = "examples/transforms/3d_rotation.rs"
Expand Down
6 changes: 4 additions & 2 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ rand_chacha = "0.3"
criterion = { version = "0.3", features = ["html_reports"] }
bevy_app = { path = "../crates/bevy_app" }
bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] }
bevy_hierarchy = { path = "../crates/bevy_hierarchy" }
bevy_internal = { path = "../crates/bevy_internal" }
bevy_math = { path = "../crates/bevy_math" }
bevy_reflect = { path = "../crates/bevy_reflect" }
bevy_render = { path = "../crates/bevy_render" }
bevy_tasks = { path = "../crates/bevy_tasks" }
bevy_utils = { path = "../crates/bevy_utils" }
bevy_math = { path = "../crates/bevy_math" }
bevy_render = { path = "../crates/bevy_render" }

[profile.release]
opt-level = 3
Expand Down
2 changes: 2 additions & 0 deletions benches/benches/bevy_ecs/benches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ use criterion::criterion_main;
mod components;
mod events;
mod iteration;
mod observers;
mod scheduling;
mod world;

criterion_main!(
components::components_benches,
events::event_benches,
iteration::iterations_benches,
observers::observer_benches,
scheduling::scheduling_benches,
world::world_benches,
);
6 changes: 6 additions & 0 deletions benches/benches/bevy_ecs/observers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use criterion::criterion_group;

mod propagation;
use propagation::*;

criterion_group!(observer_benches, event_propagation);
151 changes: 151 additions & 0 deletions benches/benches/bevy_ecs/observers/propagation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use bevy_app::{App, First, Startup};
use bevy_ecs::{
component::Component,
entity::Entity,
event::{Event, EventWriter},
observer::Trigger,
query::{Or, With, Without},
system::{Commands, EntityCommands, Query},
};
use bevy_hierarchy::{BuildChildren, Children, Parent};
use bevy_internal::MinimalPlugins;

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::{seq::IteratorRandom, Rng};

const DENSITY: usize = 20; // percent of nodes with listeners
const ENTITY_DEPTH: usize = 64;
const ENTITY_WIDTH: usize = 200;
const N_EVENTS: usize = 500;

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

group.bench_function("baseline", |bencher| {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_systems(Startup, spawn_listener_hierarchy);
app.update();

bencher.iter(|| {
black_box(app.update());
});
});

group.bench_function("single_event_type", |bencher| {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_systems(
Startup,
(
spawn_listener_hierarchy,
add_listeners_to_hierarchy::<DENSITY, 1>,
),
)
.add_systems(First, send_events::<1, N_EVENTS>);
app.update();

bencher.iter(|| {
black_box(app.update());
});
});

group.bench_function("single_event_type_no_listeners", |bencher| {
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_systems(
Startup,
(
spawn_listener_hierarchy,
add_listeners_to_hierarchy::<DENSITY, 1>,
),
)
.add_systems(First, send_events::<9, N_EVENTS>);
app.update();

bencher.iter(|| {
black_box(app.update());
});
});

group.bench_function("four_event_types", |bencher| {
let mut app = App::new();
const FRAC_N_EVENTS_4: usize = N_EVENTS / 4;
const FRAC_DENSITY_4: usize = DENSITY / 4;

app.add_plugins(MinimalPlugins)
.add_systems(
Startup,
(
spawn_listener_hierarchy,
add_listeners_to_hierarchy::<FRAC_DENSITY_4, 1>,
add_listeners_to_hierarchy::<FRAC_DENSITY_4, 2>,
add_listeners_to_hierarchy::<FRAC_DENSITY_4, 3>,
add_listeners_to_hierarchy::<FRAC_DENSITY_4, 4>,
),
)
.add_systems(First, send_events::<1, FRAC_N_EVENTS_4>)
.add_systems(First, send_events::<2, FRAC_N_EVENTS_4>)
.add_systems(First, send_events::<3, FRAC_N_EVENTS_4>)
.add_systems(First, send_events::<4, FRAC_N_EVENTS_4>);
app.update();

bencher.iter(|| {
black_box(app.update());
});
});

group.finish();
}

#[derive(Clone, Component)]
struct TestEvent<const N: usize> {}

impl<const N: usize> Event for TestEvent<N> {
type Traversal = Parent;
const AUTO_PROPAGATE: bool = true;
}

fn send_events<const N: usize, const N_EVENTS: usize>(
mut commands: Commands,
entities: Query<Entity, Without<Children>>,
) {
let target = entities.iter().choose(&mut rand::thread_rng()).unwrap();
(0..N_EVENTS).for_each(|_| {
commands.trigger_targets(TestEvent::<N> {}, target);
});
}

fn spawn_listener_hierarchy(mut commands: Commands) {
for _ in 0..ENTITY_WIDTH {
let mut parent = commands.spawn_empty().id();
for _ in 0..ENTITY_DEPTH {
let child = commands.spawn_empty().id();
commands.entity(parent).add_child(child);
parent = child;
}
}
}

fn empty_listener<const N: usize>(_trigger: Trigger<TestEvent<N>>) {}

fn add_listeners_to_hierarchy<const DENSITY: usize, const N: usize>(
mut commands: Commands,
roots_and_leaves: Query<Entity, Or<(Without<Parent>, Without<Children>)>>,
nodes: Query<Entity, (With<Parent>, With<Children>)>,
) {
for entity in &roots_and_leaves {
commands.entity(entity).observe(empty_listener::<N>);
}
for entity in &nodes {
maybe_insert_listener::<DENSITY, N>(&mut commands.entity(entity));
}
}

fn maybe_insert_listener<const DENSITY: usize, const N: usize>(commands: &mut EntityCommands) {
if rand::thread_rng().gen_bool(DENSITY as f64 / 100.0) {
commands.observe(empty_listener::<N>);
}
}
2 changes: 2 additions & 0 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub fn derive_event(input: TokenStream) -> TokenStream {

TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
type Traversal = #bevy_ecs_path::traversal::TraverseNone;
const AUTO_PROPAGATE: bool = false;
}

impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
Expand Down
16 changes: 14 additions & 2 deletions crates/bevy_ecs/src/event/base.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::component::Component;
use crate::{component::Component, traversal::Traversal};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use std::{
Expand Down Expand Up @@ -34,7 +34,19 @@ use std::{
label = "invalid `Event`",
note = "consider annotating `{Self}` with `#[derive(Event)]`"
)]
pub trait Event: Component {}
pub trait Event: Component {
/// The component that describes which Entity to propagate this event to next, when [propagation] is enabled.
///
/// [propagation]: crate::observer::Trigger::propagate
type Traversal: Traversal;

/// When true, this event will always attempt to propagate when [triggered], without requiring a call
/// to [`Trigger::propagate`].
///
/// [triggered]: crate::system::Commands::trigger_targets
/// [`Trigger::propagate`]: crate::observer::Trigger::propagate
const AUTO_PROPAGATE: bool = false;
}

/// An `EventId` uniquely identifies an event stored in a specific [`World`].
///
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod removal_detection;
pub mod schedule;
pub mod storage;
pub mod system;
pub mod traversal;
pub mod world;

pub use bevy_ptr as ptr;
Expand Down
Loading