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

Child spawning optimizations #17339

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ name = "picking"
path = "benches/bevy_picking/main.rs"
harness = false

[[bench]]
name = "hierarchy"
path = "benches/bevy_hierarchy/main.rs"
harness = false

[[bench]]
name = "reflect"
path = "benches/bevy_reflect/main.rs"
Expand Down
5 changes: 5 additions & 0 deletions benches/benches/bevy_hierarchy/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use criterion::criterion_main;

mod spawn;

criterion_main!(spawn::benches);
41 changes: 41 additions & 0 deletions benches/benches/bevy_hierarchy/spawn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use bevy_ecs::prelude::*;
use bevy_ecs::world::CommandQueue;
use bevy_hierarchy::prelude::*;
use core::hint::black_box;
use criterion::{criterion_group, Criterion};

criterion_group!(benches, spawn_children);

fn spawn_children(c: &mut Criterion) {
let mut group = c.benchmark_group("spawn_children");
group.warm_up_time(core::time::Duration::from_millis(500));
group.measurement_time(core::time::Duration::from_secs(4));

for entity_count in (1..7).map(|i| 10i32.pow(i)) {
group.bench_function(format!("{}_entities", entity_count), |bencher| {
let mut world = World::default();
let mut command_queue = CommandQueue::default();

bencher.iter(|| {
let mut commands = Commands::new(&mut command_queue, &world);
let mut entity = commands.spawn_empty();

entity.with_children(|c| {
for _ in 0..entity_count {
c.spawn_empty();
}
});

entity.with_child(());
entity.with_children(|c| {
for _ in 0..entity_count {
c.spawn_empty();
}
});
command_queue.apply(black_box(&mut world));
});

black_box(world);
});
}
}
54 changes: 37 additions & 17 deletions crates/bevy_hierarchy/src/child_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ fn remove_children(parent: Entity, children: &[Entity], world: &mut World) {
/// ```
pub struct ChildBuilder<'a> {
commands: Commands<'a, 'a>,
/// These children are guaranteed to be spawned entities, and do not need re-parenting checks.
children: SmallVec<[Entity; 8]>,
parent: Entity,
}
Expand Down Expand Up @@ -213,13 +214,15 @@ impl ChildBuild for ChildBuilder<'_> {
Self: 'a;

fn spawn(&mut self, bundle: impl Bundle) -> EntityCommands {
let e = self.commands.spawn(bundle);
// Spawn the entity with the `Parent` component to avoid archetype changes.
let e = self.commands.spawn((bundle, Parent(self.parent)));
self.children.push(e.id());
e
}

fn spawn_empty(&mut self) -> EntityCommands {
let e = self.commands.spawn_empty();
// Spawn the entity with the `Parent` component to avoid archetype changes.
let e = self.commands.spawn(Parent(self.parent));
self.children.push(e.id());
e
}
Expand Down Expand Up @@ -346,20 +349,37 @@ impl BuildChildren for EntityCommands<'_> {
};

spawn_children(&mut builder);

let children = builder.children;
if children.contains(&parent) {
panic!("Entity cannot be a child of itself.");
}

// Send hierarchy events
let events = children
.iter()
.map(|&child| HierarchyEvent::ChildAdded { child, parent })
.collect::<alloc::vec::Vec<_>>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer use alloc::vec::Vec; at the top of the file.

self.commands_mut().queue(move |world: &mut World| {
push_events(world, events);
});

self.queue(move |mut entity: EntityWorldMut| {
entity.add_children(&children);
})
// `ChildBuilder` is responsible for setting the `Parent` component on all the children
// as they are spawned to avoid archetype changes. All that is left for us to do is push
// these children onto the parent entity's `Children` component. Because these are
// freshly spawned entities, we don't need to check for re-parenting, duplicates, or if
// they already exist in the `Parent` component. This avoids a lot of work!
if let Some(mut children_component) = entity.get_mut::<Children>() {
children_component.0.extend(children);
} else {
entity.insert(Children(children));
}
});
self
}

fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
let child = self.commands().spawn(bundle).id();
self.queue(move |mut entity: EntityWorldMut| {
entity.add_child(child);
// Performance: note that `with_child` is faster than `add_child`, because it knows the
// entity is freshly spawned, and does not need to do any re-parenting checks.
entity.with_child(bundle);
})
}

Expand Down Expand Up @@ -499,13 +519,13 @@ impl BuildChildren for EntityWorldMut<'_> {

fn with_child<B: Bundle>(&mut self, bundle: B) -> &mut Self {
let parent = self.id();
let child = self.world_scope(|world| world.spawn((bundle, Parent(parent))).id());
if let Some(mut children_component) = self.get_mut::<Children>() {
children_component.0.retain(|value| child != *value);
children_component.0.push(child);
} else {
self.insert(Children::from_entities(&[child]));
}
self.world_scope(|world| {
let child = world.spawn((bundle, Parent(parent))).id();
// Because we know this entity was just spawned, we don't need to check the child list
// to see if it is already present, it is impossible.
add_child_unchecked(world, parent, child);
push_events(world, [HierarchyEvent::ChildAdded { child, parent }]);
});
self
}

Expand Down
Loading