Skip to content

Remove invalid entity locations#19433

Merged
alice-i-cecile merged 15 commits intobevyengine:mainfrom
ElliottjPierce:remove-INVALID-entity-locations
May 31, 2025
Merged

Remove invalid entity locations#19433
alice-i-cecile merged 15 commits intobevyengine:mainfrom
ElliottjPierce:remove-INVALID-entity-locations

Conversation

@ElliottjPierce
Copy link
Copy Markdown
Contributor

@ElliottjPierce ElliottjPierce commented May 29, 2025

Objective

This is the first step of #19430 and is a follow up for #19132.

Now that ArchetypeRow has a niche, we can use Option instead of needing INVALID everywhere.

This was especially concerning since INVALID really was valid!

Using options here made the code clearer and more data-driven.

Solution

Replace all uses of INVALID entity locations (and archetype/table rows) with None.

Testing

CI

@ElliottjPierce ElliottjPierce added A-ECS Entities, components, systems, and events X-Uncontroversial This work is generally agreed upon D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward C-Code-Quality A section of code that is hard to understand or change labels May 29, 2025
if meta.generation != EntityGeneration::FIRST
|| meta.location.archetype_id != ArchetypeId::INVALID
{
if meta.location.is_some() {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this is right, but honestly, not even sure why we need the if.

Copy link
Copy Markdown
Contributor

@urben1680 urben1680 May 29, 2025

Choose a reason for hiding this comment

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

I think this is not right, the tick may also be indirectly returned by entity_get_spawned_or_despawned even if meta.location is None in some cases.

If the the tick is considered valid in such a case, then it needs to be checked upon in this method here too.

It exposes that we somehow lost the ability to reliably detect when the tick is indeed unused and only a default value. But honestly I would not bother given how cold this method here is. Just check all ticks in here and with that be sure all edge cases are covered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree. I'll remove the if.

/// before handing control to unknown code.
#[inline]
pub(crate) unsafe fn set(&mut self, index: u32, location: EntityLocation) {
pub(crate) unsafe fn set(&mut self, index: u32, location: Option<EntityLocation>) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems that this method is always called with Some location. Do you expect calls with None in the future or another PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added it for completeness but yes.

We used to have alloc_at which let you respawn an entity and reuse its index. There have been requests to support that again. That's one of the next steps in my tracking issue. Basically, we'll set the location to None instead of fully despawning.

pub(crate) fn assert_not_despawned(&self) {
if self.location.archetype_id == ArchetypeId::INVALID {
self.panic_despawned();
pub(crate) fn assert_not_despawned(&self) -> EntityLocation {
Copy link
Copy Markdown
Contributor

@urben1680 urben1680 May 29, 2025

Choose a reason for hiding this comment

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

Since this and EntityWorldMut::location do the same now, this one can probably be removed and usages switch to the location method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a good idea.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Turns out it's used for other things too, so I've kept it but reverted to how it was.

if meta.generation != EntityGeneration::FIRST
|| meta.location.archetype_id != ArchetypeId::INVALID
{
if meta.location.is_some() {
Copy link
Copy Markdown
Contributor

@urben1680 urben1680 May 29, 2025

Choose a reason for hiding this comment

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

I think this is not right, the tick may also be indirectly returned by entity_get_spawned_or_despawned even if meta.location is None in some cases.

If the the tick is considered valid in such a case, then it needs to be checked upon in this method here too.

It exposes that we somehow lost the ability to reliably detect when the tick is indeed unused and only a default value. But honestly I would not bother given how cold this method here is. Just check all ticks in here and with that be sure all edge cases are covered.

Comment on lines -1319 to -1327
impl EntityLocation {
/// location for **pending entity** and **invalid entity**
pub(crate) const INVALID: EntityLocation = EntityLocation {
archetype_id: ArchetypeId::INVALID,
archetype_row: ArchetypeRow::INVALID,
table_id: TableId::INVALID,
table_row: TableRow::INVALID,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

feels_good_man.jpg

Copy link
Copy Markdown
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

Looks good! Just left some style suggestions.

Some(meta.location)
(meta.generation == entity.generation)
.then_some(meta.location)
.flatten()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

then_some().flatten() is a little odd. I think that might be Option::filter()?

Hmm, or could this whole thing be replaced with filter().and_then() like

self.meta.get(entity.index() as usize)
    .filter(|meta| meta.generation == entity.generation)
    .and_then(|meta| meta.location)

?

(Sorry, I can't get GitHub to let me write that as a suggestion.)

///
/// Must not be called while reserved entities are awaiting `flush()`.
pub fn free(&mut self, entity: Entity) -> Option<EntityLocation> {
pub fn free(&mut self, entity: Entity) -> Option<Option<EntityLocation>> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Option<Option<T>> can be hard to reason about. Does anyone care about Some(None) vs None, or could we flatten this and return a plain Option<EntityLocation>? It looks like the only place this is called changed from .expect() to .expect().expect(), so it doesn't seem to care.

Otherwise, it might be worth returning a Result with some new enum that gives names to the two kinds of None.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 30, 2025
ElliottjPierce and others added 2 commits May 30, 2025 12:04
Co-Authored-By: Chris Russell <[email protected]>
Co-Authored-By: Chris Russell <[email protected]>
@ElliottjPierce
Copy link
Copy Markdown
Contributor Author

As far as Option<Option<EntityLocation>> goes, I agree that, for now, flattening is the right way to go, especially given the inconsistency between get and free.

But as a heads up, I think IMO we should expose this fully. Maybe a struct MaybeEntityLocation(Option<EntityLocation>);? This will be useful for constructing/destructing entities in the future, but I'll save that for another pr.

@ElliottjPierce
Copy link
Copy Markdown
Contributor Author

Update: unfortunately tests do seem to need the full location information, so I have added it back.

But I used a type alias to hopefully make things a little more clear.

@urben1680
Copy link
Copy Markdown
Contributor

urben1680 commented May 30, 2025

An alternative could also be Result<Option<EntityLocation>, EntityDoesNotExistError>, though it is not so nice since Ok(None) also means the entity kinda does not exist.

@ElliottjPierce
Copy link
Copy Markdown
Contributor Author

An alternative could also be Result<Option<EntityLocation>, EntityDoesNotExistError>, though it is not so nice since Ok(None) also means the entity kinda does not exist.

Option<EntityIdLocation> to Result<EntityIdLocation, EndityDoesNotExistError> is not a bad idea. I'm not opposed to it, but I'm not sure what it would buy us. We still need EntityIdLocation for set, flush, etc. Maybe I'll try it out in a future pr.

///
/// Setting a location to `None` is often helpful when you want to destruct an entity or yank it from the ECS without allowing another system to reuse the id for something else.
/// It is also useful for reserving an id; commands will often allocate an `Entity` but not provide it a location until the command is applied.
pub type EntityIdLocation = Option<EntityLocation>;
Copy link
Copy Markdown
Contributor

@NyxAlexandra NyxAlexandra May 30, 2025

Choose a reason for hiding this comment

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

I'm not sure if this name is very clear ("location of an entity" versus "location of an entity identifier"). Then again, I'm not sure of a better name (nor am I a maintaner or anything like that).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I couldn't think of a better name either, but might be worth revisiting it later.

Comment on lines +1186 to +1187
|| (meta.location.is_none()
&& (meta.generation == entity.generation.after_versions(1))))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is the condition change here expected?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think the extra parentheses doesn't change behavior; just makes it more clear. If not, the old behavior is a bug: Either the generations must mach (for spawned), or both the location must be none (for despawned) and the generation must be the one right before it (the one just despawned).

Copy link
Copy Markdown
Contributor

@urben1680 urben1680 May 31, 2025

Choose a reason for hiding this comment

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

It does indeed not change the behavior, though mixing || and && without parentheses does make the behavior not easy to see (to me). So I like this here more than how it was.

ElliottjPierce and others added 2 commits May 31, 2025 09:14
Co-authored-by: François Mockers <[email protected]>
@alice-i-cecile alice-i-cecile enabled auto-merge May 31, 2025 16:19
@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 31, 2025
Merged via the queue into bevyengine:main with commit 61bd3af May 31, 2025
32 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Nov 3, 2025
…nd despawning (#19451)

# Objective

This is the next step for #19430 and is also convinient for #18670.

For context, the way entities work on main is as a "allocate and use"
system. Entity ids are allocated, and given a location. The location can
then be changed, etc. Entities that are free have an invalid location.
To allocate an entity, one must also set its location. This introduced
the need for pending entities, where an entity would be reserved,
pending, and at some point flushed. Pending and free entities have an
invalid location, and others are assumed to have a valid one.

This paradigm has a number of downsides: First, the entities metadata
table is inseparable from the allocator, which makes remote reservation
challenging. Second, the `World` must be flushed, even to do simple
things, like allocate a temporary entity id. Third, users have little
control over entity ids, only interacting with conceptual entities. This
made things like `Entities::alloc_at` clunky and slow, leading to its
removal, despite some users still having valid need of it.

So the goal of this PR is to:
- Decouple `Entities` from entity allocation to make room for other
allocators and resolve `alloc_at` issues.
- Decouple entity allocation from spawning to make reservation a moot
point.
- Introduce constructing and destructing entities, in addition to
spawn/despawn.
- Change `reserve` and `flush` patterns to `alloc` and `construct`
patterns.

It is possible to break this up into multiple prs, as I originally
intended, but doing so would require lots of temporary scaffolding that
would both hurt performance and make things harder to review.

## Solution

This solution builds on #19433, which changed the representation of
invalid entity locations from a constant to `None`.

There's quite a few steps to this, each somewhat controversial:

### Entities with no location

This pr introduces the idea of entity rows both with and without
locations. This corresponds to entities that are constructed (the row
has a location) and not constructed (the row has no location). When a
row is free or pending, it is not constructed. When a row is outside the
range of the meta list, it still exists; it's just not constructed.

This extends to conceptual entities; conceptual entities may now be in
one of 3 states: empty (constructed; no components), normal
(constructed; 1 or more components), or null (not constructed). This
extends to entity pointers (`EntityWorldMut`, etc): These now can point
to "null"/not constructed entities. Depending on the privilege of the
pointer, these can also construct or destruct the entity.

This also changes how `Entity` ids relate to conceptual entities. An
`Entity` now exists if its generation matches that of its row. An
`Entity` that has the right generation for its row will claim to exist,
even if it is not constructed. This means, for example, an `Entity`
manually constructed with a large index and generation of 0 *will* exist
if it has not been allocated yet.

### `Entities` is separate from the allocator

This pr separates entity allocation from `Entities`. `Entities` is now
only focused on tracking entity metadata, etc. The new
`EntitiesAllocator` on `World` manages all allocations. This forces
`Entities` to not rely on allocator state to determine if entities
exist, etc, which is convinient for remote reservation and needed for
custom allocators. It also paves the way for allocators not housed
within the `World`, makes some unsafe code easier since the allocator
and metadata live under different pointers, etc.

This separation requires thinking about interactions with `Entities` in
a new way. Previously, the `Entities` set the rules for what entities
are valid and what entities are not. Now, it has no way of knowing.
Instead, interaction with `Entities` are more like declaring some
information for it to track than changing some information it was
already tracking. To reflect this, `set` has been split up into
`declare` and `update`.

### Constructing and destructing

As mentioned, entities that have no location (not constructed) can be
constructed at any time. This takes on exactly the same meaning as the
previous `spawn_non_existent`. It creates/declares a location instead of
updating an old one. As an example, this makes spawning an entity now
literately just allocate a new id and construct it immediately.

Conversely, entities that are constructed may be destructed. This
removes all components and despawns related entities, just like
`despawn`. The only difference is that destructing does not free the
entity id for reuse. Between constructing and destructing, all needs for
`alloc_at` are resolved. If you want to keep the id for custom reuse,
just destruct instead of despawn! Despawn, now just destructs the entity
and frees it.

Destructing a not constructed entity will do nothing. Constructing an
already constructed entity will panic. This is to guard against users
constructing a manually formed `Entity` that the allocator could later
hand out. However, public construction methods have proper error
handling for this. Despawning a not constructed entity just frees its
id.

### No more flushing

All places that once needed to reserve and flush entity ids now allocate
and construct them instead. This improves performance and simplifies
things.

### Flow chart

![entity row
lifecycle](https://github.com/user-attachments/assets/3e5397ab-4ec6-4477-91de-3d002d0cf92e)

(Thanks @ItsDoot)

## Testing

- CI
- Some new tests
- A few deleted (no longer applicable) tests
- If you see something you think should have a test case, I'll gladly
add it.

## Showcase

Here's an example of constructing and destructing

```rust
let e4 = world.spawn_null();
world
    .entity_mut(e4)
    .construct((TableStored("junk"), A(0)))
    .unwrap()
    .destruct()
    .construct((TableStored("def"), A(456)))
    .unwrap();
```

## Future Work

- [x] More expansive docs. This should definitely should be done, but
I'd rather do that in a future pr to separate writing review from code
review. If you have more ideas for how to introduce users to these
concepts, I'd like to see them. As it is, we don't do a very good job of
explaining entities to users. Ex: `Entity` doesn't always correspond to
a conceptual entity.
- [ ] Try to remove panics from `EntityWorldMut`. There is (and was) a
lot of assuming the entity is constructed there (was assuming it was not
despawned).
- [ ] A lot of names are still centered around spawn/despawn, which is
more user-friendly than construct/destruct but less precise. Might be
worth changing these over.
- [ ] Making a centralized bundle despawner would make sense now.
- [ ] Of course, build on this for remote reservation and, potentially,
for paged entities.

## Performance

<details>

<summary>Benchmarks</summary>

```txt
critcmp main pr19451 -t 1
group                                                                                                     main                                     pr19451
-----                                                                                                     ----                                     -------
add_remove/sparse_set                                                                                     1.13    594.7±6.80µs        ? ?/sec      1.00    527.4±8.01µs        ? ?/sec
add_remove/table                                                                                          1.08   799.6±15.53µs        ? ?/sec      1.00   739.7±15.10µs        ? ?/sec
add_remove_big/sparse_set                                                                                 1.10    614.6±6.50µs        ? ?/sec      1.00   557.0±19.04µs        ? ?/sec
add_remove_big/table                                                                                      1.03      2.8±0.01ms        ? ?/sec      1.00      2.7±0.02ms        ? ?/sec
added_archetypes/archetype_count/100                                                                      1.01     30.9±0.50µs        ? ?/sec      1.00     30.5±0.44µs        ? ?/sec
added_archetypes/archetype_count/1000                                                                     1.00   638.0±19.77µs        ? ?/sec      1.03   657.0±73.61µs        ? ?/sec
added_archetypes/archetype_count/10000                                                                    1.02      5.5±0.14ms        ? ?/sec      1.00      5.4±0.09ms        ? ?/sec
all_added_detection/50000_entities_ecs::change_detection::Sparse                                          1.02     47.9±1.22µs        ? ?/sec      1.00     46.8±0.40µs        ? ?/sec
all_added_detection/50000_entities_ecs::change_detection::Table                                           1.02     45.4±1.89µs        ? ?/sec      1.00     44.6±0.78µs        ? ?/sec
build_schedule/1000_schedule                                                                              1.02   942.6±11.53ms        ? ?/sec      1.00   925.2±10.35ms        ? ?/sec
build_schedule/100_schedule                                                                               1.01      5.8±0.12ms        ? ?/sec      1.00      5.7±0.12ms        ? ?/sec
build_schedule/100_schedule_no_constraints                                                                1.03   803.1±28.93µs        ? ?/sec      1.00   781.1±50.11µs        ? ?/sec
build_schedule/500_schedule_no_constraints                                                                1.00      5.6±0.31ms        ? ?/sec      1.08      6.0±0.27ms        ? ?/sec
busy_systems/01x_entities_03_systems                                                                      1.00     24.4±1.35µs        ? ?/sec      1.01     24.7±1.35µs        ? ?/sec
busy_systems/03x_entities_03_systems                                                                      1.00     38.1±1.70µs        ? ?/sec      1.04     39.7±1.49µs        ? ?/sec
busy_systems/03x_entities_09_systems                                                                      1.01    111.4±2.27µs        ? ?/sec      1.00    109.9±2.46µs        ? ?/sec
busy_systems/03x_entities_15_systems                                                                      1.00    174.8±2.56µs        ? ?/sec      1.01    176.6±4.22µs        ? ?/sec
contrived/03x_entities_09_systems                                                                         1.00     59.0±2.92µs        ? ?/sec      1.01     59.8±3.03µs        ? ?/sec
contrived/03x_entities_15_systems                                                                         1.00     97.5±4.87µs        ? ?/sec      1.01     98.8±4.69µs        ? ?/sec
contrived/05x_entities_09_systems                                                                         1.00     75.3±3.76µs        ? ?/sec      1.01     76.4±4.11µs        ? ?/sec
despawn_world/10000_entities                                                                              1.32    344.8±4.47µs        ? ?/sec      1.00    261.4±4.91µs        ? ?/sec
despawn_world/100_entities                                                                                1.22      4.3±0.04µs        ? ?/sec      1.00      3.5±0.54µs        ? ?/sec
despawn_world/1_entities                                                                                  1.01    169.6±7.88ns        ? ?/sec      1.00   167.8±11.45ns        ? ?/sec
despawn_world_recursive/10000_entities                                                                    1.20  1723.0±53.82µs        ? ?/sec      1.00  1437.0±26.11µs        ? ?/sec
despawn_world_recursive/100_entities                                                                      1.16     17.9±0.10µs        ? ?/sec      1.00     15.5±0.16µs        ? ?/sec
despawn_world_recursive/1_entities                                                                        1.01   372.8±15.68ns        ? ?/sec      1.00   367.7±16.90ns        ? ?/sec
ecs::entity_cloning::hierarchy_many/clone                                                                 1.03   227.9±24.67µs 1559.9 KElem/sec    1.00   221.1±29.74µs 1607.8 KElem/sec
ecs::entity_cloning::hierarchy_many/reflect                                                               1.00   406.2±23.46µs 875.2 KElem/sec     1.02   413.9±22.45µs 858.9 KElem/sec
ecs::entity_cloning::hierarchy_tall/clone                                                                 1.01     12.2±0.34µs  4.0 MElem/sec      1.00     12.0±1.41µs  4.1 MElem/sec
ecs::entity_cloning::hierarchy_tall/reflect                                                               1.02     15.3±0.39µs  3.2 MElem/sec      1.00     15.0±2.14µs  3.2 MElem/sec
ecs::entity_cloning::single/clone                                                                         1.02  659.0±100.01ns 1481.8 KElem/sec    1.00  643.3±101.49ns 1517.9 KElem/sec
ecs::entity_cloning::single/reflect                                                                       1.03  1135.2±72.17ns 860.2 KElem/sec     1.00  1098.3±65.99ns 889.1 KElem/sec
empty_archetypes/for_each/10                                                                              1.02      8.1±0.57µs        ? ?/sec      1.00      8.0±0.37µs        ? ?/sec
empty_archetypes/for_each/100                                                                             1.01      8.1±0.34µs        ? ?/sec      1.00      8.1±0.28µs        ? ?/sec
empty_archetypes/for_each/1000                                                                            1.03      8.4±0.25µs        ? ?/sec      1.00      8.2±0.29µs        ? ?/sec
empty_archetypes/iter/100                                                                                 1.01      8.1±0.29µs        ? ?/sec      1.00      8.0±0.34µs        ? ?/sec
empty_archetypes/iter/1000                                                                                1.02      8.5±0.31µs        ? ?/sec      1.00      8.4±0.62µs        ? ?/sec
empty_archetypes/iter/10000                                                                               1.01     10.6±1.22µs        ? ?/sec      1.00     10.5±0.49µs        ? ?/sec
empty_archetypes/par_for_each/10                                                                          1.01      8.8±0.49µs        ? ?/sec      1.00      8.7±0.31µs        ? ?/sec
empty_archetypes/par_for_each/100                                                                         1.00      8.7±0.48µs        ? ?/sec      1.04      9.0±0.34µs        ? ?/sec
empty_archetypes/par_for_each/10000                                                                       1.01     21.2±0.41µs        ? ?/sec      1.00     20.9±0.44µs        ? ?/sec
empty_commands/0_entities                                                                                 1.72      3.7±0.01ns        ? ?/sec      1.00      2.1±0.02ns        ? ?/sec
empty_systems/100_systems                                                                                 1.00     82.9±3.29µs        ? ?/sec      1.07     88.3±3.77µs        ? ?/sec
empty_systems/2_systems                                                                                   1.01      8.2±0.71µs        ? ?/sec      1.00      8.2±0.38µs        ? ?/sec
empty_systems/4_systems                                                                                   1.00      8.2±0.72µs        ? ?/sec      1.03      8.4±0.71µs        ? ?/sec
entity_hash/entity_set_build/10000                                                                        1.10     45.9±1.60µs 207.7 MElem/sec     1.00     41.6±0.39µs 229.0 MElem/sec
entity_hash/entity_set_build/3162                                                                         1.06     12.7±0.77µs 236.7 MElem/sec     1.00     12.0±0.75µs 250.6 MElem/sec
entity_hash/entity_set_lookup_hit/10000                                                                   1.02     14.5±0.30µs 658.3 MElem/sec     1.00     14.2±0.07µs 672.6 MElem/sec
entity_hash/entity_set_lookup_hit/3162                                                                    1.01      4.4±0.03µs 682.7 MElem/sec     1.00      4.4±0.01µs 691.3 MElem/sec
entity_hash/entity_set_lookup_miss_gen/10000                                                              1.01     61.3±4.12µs 155.6 MElem/sec     1.00     60.6±1.47µs 157.3 MElem/sec
entity_hash/entity_set_lookup_miss_gen/3162                                                               1.00      9.5±0.02µs 316.3 MElem/sec     1.01      9.7±0.88µs 311.7 MElem/sec
entity_hash/entity_set_lookup_miss_id/100                                                                 1.00    145.5±1.49ns 655.4 MElem/sec     1.03    149.8±1.59ns 636.7 MElem/sec
entity_hash/entity_set_lookup_miss_id/10000                                                               1.85     63.9±3.57µs 149.3 MElem/sec     1.00     34.6±3.81µs 275.8 MElem/sec
entity_hash/entity_set_lookup_miss_id/316                                                                 1.00    562.0±9.58ns 536.2 MElem/sec     1.02    573.9±1.27ns 525.1 MElem/sec
entity_hash/entity_set_lookup_miss_id/3162                                                                1.03      9.1±0.10µs 330.7 MElem/sec     1.00      8.9±0.24µs 339.0 MElem/sec
event_propagation/four_event_types                                                                        1.12    541.5±3.84µs        ? ?/sec      1.00    482.7±4.64µs        ? ?/sec
event_propagation/single_event_type                                                                       1.07   769.5±10.21µs        ? ?/sec      1.00   715.9±15.16µs        ? ?/sec
event_propagation/single_event_type_no_listeners                                                          1.56    393.4±2.89µs        ? ?/sec      1.00    251.4±3.68µs        ? ?/sec
events_iter/size_16_events_100                                                                            1.01     64.0±0.18ns        ? ?/sec      1.00     63.4±0.23ns        ? ?/sec
events_iter/size_4_events_100                                                                             1.02     64.8±0.90ns        ? ?/sec      1.00     63.4±0.24ns        ? ?/sec
events_iter/size_4_events_1000                                                                            1.01    586.5±8.00ns        ? ?/sec      1.00    579.1±4.93ns        ? ?/sec
events_send/size_16_events_100                                                                            1.00   142.7±24.34ns        ? ?/sec      1.03   147.1±28.36ns        ? ?/sec
events_send/size_16_events_10000                                                                          1.01     12.2±0.13µs        ? ?/sec      1.00     12.1±0.12µs        ? ?/sec
fake_commands/10000_commands                                                                              1.43     63.3±8.21µs        ? ?/sec      1.00     44.1±0.16µs        ? ?/sec
fake_commands/1000_commands                                                                               1.40      6.2±0.01µs        ? ?/sec      1.00      4.4±0.02µs        ? ?/sec
fake_commands/100_commands                                                                                1.38    629.4±1.69ns        ? ?/sec      1.00    457.1±0.84ns        ? ?/sec
few_changed_detection/50000_entities_ecs::change_detection::Table                                         1.00     57.7±0.86µs        ? ?/sec      1.07     61.6±1.19µs        ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Sparse                                         1.05      5.4±0.53µs        ? ?/sec      1.00      5.1±0.56µs        ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Table                                          1.00      4.3±0.30µs        ? ?/sec      1.18      5.1±0.35µs        ? ?/sec
insert_commands/insert                                                                                    1.11   402.5±10.75µs        ? ?/sec      1.00    363.6±8.07µs        ? ?/sec
insert_commands/insert_batch                                                                              1.00    174.9±3.03µs        ? ?/sec      1.02    177.9±5.74µs        ? ?/sec
insert_simple/base                                                                                        1.04   564.1±23.01µs        ? ?/sec      1.00   544.3±60.70µs        ? ?/sec
insert_simple/unbatched                                                                                   1.32  929.3±180.10µs        ? ?/sec      1.00  704.1±132.88µs        ? ?/sec
iter_fragmented/base                                                                                      1.02    280.0±2.86ns        ? ?/sec      1.00    274.0±4.85ns        ? ?/sec
iter_fragmented/foreach                                                                                   1.00     97.3±0.42ns        ? ?/sec      1.03    100.6±3.44ns        ? ?/sec
iter_fragmented/foreach_wide                                                                              1.04      2.7±0.04µs        ? ?/sec      1.00      2.6±0.03µs        ? ?/sec
iter_fragmented_sparse/base                                                                               1.00      5.6±0.05ns        ? ?/sec      1.04      5.8±0.06ns        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Sparse    1.00   737.7±27.38µs        ? ?/sec      1.01   747.5±30.01µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Table     1.02   678.3±25.13µs        ? ?/sec      1.00   662.1±19.63µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Sparse     1.09     76.0±9.35µs        ? ?/sec      1.00     70.0±3.29µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Table      1.03     64.7±3.40µs        ? ?/sec      1.00     62.8±1.80µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_100_entities_ecs::change_detection::Table       1.02      7.6±0.12µs        ? ?/sec      1.00      7.5±0.16µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10_entities_ecs::change_detection::Sparse       1.00  1003.5±12.38ns        ? ?/sec      1.01  1013.7±32.64ns        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10_entities_ecs::change_detection::Sparse        1.03   187.1±21.18ns        ? ?/sec      1.00   181.9±22.86ns        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10_entities_ecs::change_detection::Sparse         1.00     52.8±8.19ns        ? ?/sec      1.03     54.3±8.06ns        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10_entities_ecs::change_detection::Table          1.00     46.8±2.23ns        ? ?/sec      1.03     48.0±2.48ns        ? ?/sec
no_archetypes/system_count/0                                                                              1.00     16.3±0.17ns        ? ?/sec      1.02     16.6±0.16ns        ? ?/sec
no_archetypes/system_count/100                                                                            1.02    851.5±9.32ns        ? ?/sec      1.00    832.9±7.93ns        ? ?/sec
none_changed_detection/5000_entities_ecs::change_detection::Sparse                                        1.00      3.4±0.04µs        ? ?/sec      1.02      3.5±0.05µs        ? ?/sec
nonempty_spawn_commands/10000_entities                                                                    1.89    261.1±6.99µs        ? ?/sec      1.00    137.8±8.47µs        ? ?/sec
nonempty_spawn_commands/1000_entities                                                                     1.90     26.4±3.18µs        ? ?/sec      1.00     13.9±2.38µs        ? ?/sec
nonempty_spawn_commands/100_entities                                                                      1.87      2.6±0.07µs        ? ?/sec      1.00  1388.8±97.31ns        ? ?/sec
observe/trigger_simple                                                                                    1.09    347.5±1.51µs        ? ?/sec      1.00    317.7±2.62µs        ? ?/sec
observe/trigger_targets_simple/10000_entity                                                               1.04   696.5±15.50µs        ? ?/sec      1.00   672.0±13.88µs        ? ?/sec
par_iter_simple/with_0_fragment                                                                           1.01     34.4±0.51µs        ? ?/sec      1.00     33.9±0.53µs        ? ?/sec
par_iter_simple/with_1000_fragment                                                                        1.04     45.5±0.93µs        ? ?/sec      1.00     43.9±1.85µs        ? ?/sec
par_iter_simple/with_100_fragment                                                                         1.03     36.2±0.50µs        ? ?/sec      1.00     35.1±0.44µs        ? ?/sec
par_iter_simple/with_10_fragment                                                                          1.03     37.5±0.97µs        ? ?/sec      1.00     36.5±0.74µs        ? ?/sec
param/combinator_system/8_dyn_params_system                                                               1.00     10.4±0.73µs        ? ?/sec      1.01     10.5±0.79µs        ? ?/sec
param/combinator_system/8_piped_systems                                                                   1.05      8.0±0.65µs        ? ?/sec      1.00      7.6±0.57µs        ? ?/sec
query_get/50000_entities_sparse                                                                           1.06    136.7±0.35µs        ? ?/sec      1.00    128.6±0.44µs        ? ?/sec
query_get_many_10/50000_calls_sparse                                                                      1.02  1649.4±77.80µs        ? ?/sec      1.00  1614.4±78.91µs        ? ?/sec
query_get_many_2/50000_calls_sparse                                                                       1.00    191.3±3.66µs        ? ?/sec      1.01    193.3±0.75µs        ? ?/sec
query_get_many_2/50000_calls_table                                                                        1.00    243.9±0.55µs        ? ?/sec      1.05    257.2±8.62µs        ? ?/sec
query_get_many_5/50000_calls_sparse                                                                       1.00    585.9±7.70µs        ? ?/sec      1.03    600.6±5.99µs        ? ?/sec
query_get_many_5/50000_calls_table                                                                        1.00    673.7±7.44µs        ? ?/sec      1.07   722.3±10.77µs        ? ?/sec
run_condition/no/1000_systems                                                                             1.00     23.7±0.06µs        ? ?/sec      1.06     25.1±0.07µs        ? ?/sec
run_condition/no/100_systems                                                                              1.00   1460.5±4.28ns        ? ?/sec      1.03   1510.1±3.69ns        ? ?/sec
run_condition/no/10_systems                                                                               1.00    201.5±0.53ns        ? ?/sec      1.04    209.1±2.37ns        ? ?/sec
run_condition/yes/1000_systems                                                                            1.00  1225.7±22.58µs        ? ?/sec      1.02  1253.7±24.90µs        ? ?/sec
run_condition/yes/100_systems                                                                             1.02     89.4±3.43µs        ? ?/sec      1.00     88.0±3.96µs        ? ?/sec
run_condition/yes_using_query/1000_systems                                                                1.00  1288.3±26.57µs        ? ?/sec      1.03  1323.0±24.73µs        ? ?/sec
run_condition/yes_using_query/100_systems                                                                 1.00    108.8±2.51µs        ? ?/sec      1.03    112.3±3.09µs        ? ?/sec
run_condition/yes_using_resource/100_systems                                                              1.03     99.0±3.37µs        ? ?/sec      1.00     96.2±4.80µs        ? ?/sec
run_empty_schedule/MultiThreaded                                                                          1.03     15.3±0.10ns        ? ?/sec      1.00     14.9±0.03ns        ? ?/sec
run_empty_schedule/Simple                                                                                 1.01     15.2±0.15ns        ? ?/sec      1.00     15.0±0.25ns        ? ?/sec
sized_commands_0_bytes/10000_commands                                                                     1.57     52.6±0.41µs        ? ?/sec      1.00     33.5±0.10µs        ? ?/sec
sized_commands_0_bytes/1000_commands                                                                      1.57      5.3±0.01µs        ? ?/sec      1.00      3.4±0.00µs        ? ?/sec
sized_commands_0_bytes/100_commands                                                                       1.56    536.5±4.83ns        ? ?/sec      1.00    343.6±1.12ns        ? ?/sec
sized_commands_12_bytes/10000_commands                                                                    1.22     63.0±0.53µs        ? ?/sec      1.00     51.5±6.06µs        ? ?/sec
sized_commands_12_bytes/1000_commands                                                                     1.25      5.7±0.01µs        ? ?/sec      1.00      4.6±0.05µs        ? ?/sec
sized_commands_12_bytes/100_commands                                                                      1.27    579.3±1.28ns        ? ?/sec      1.00    455.4±0.85ns        ? ?/sec
sized_commands_512_bytes/10000_commands                                                                   1.11   248.4±85.81µs        ? ?/sec      1.00   224.3±52.11µs        ? ?/sec
sized_commands_512_bytes/1000_commands                                                                    1.09     22.8±0.18µs        ? ?/sec      1.00     21.0±0.17µs        ? ?/sec
sized_commands_512_bytes/100_commands                                                                     1.13  1852.2±11.21ns        ? ?/sec      1.00   1635.3±4.91ns        ? ?/sec
spawn_commands/10000_entities                                                                             1.04   844.2±11.96µs        ? ?/sec      1.00   811.5±13.25µs        ? ?/sec
spawn_commands/1000_entities                                                                              1.05     84.9±3.66µs        ? ?/sec      1.00     80.5±4.13µs        ? ?/sec
spawn_commands/100_entities                                                                               1.06      8.6±0.12µs        ? ?/sec      1.00      8.1±0.12µs        ? ?/sec
spawn_world/10000_entities                                                                                1.03   413.2±25.20µs        ? ?/sec      1.00   400.9±49.97µs        ? ?/sec
spawn_world/100_entities                                                                                  1.02      4.1±0.62µs        ? ?/sec      1.00      4.1±0.69µs        ? ?/sec
spawn_world/1_entities                                                                                    1.04     42.2±3.23ns        ? ?/sec      1.00     40.6±6.81ns        ? ?/sec
world_entity/50000_entities                                                                               1.18     88.3±0.42µs        ? ?/sec      1.00     74.7±0.16µs        ? ?/sec
world_get/50000_entities_sparse                                                                           1.02    182.2±0.32µs        ? ?/sec      1.00    179.5±0.84µs        ? ?/sec
world_get/50000_entities_table                                                                            1.01    198.3±0.46µs        ? ?/sec      1.00    196.2±0.63µs        ? ?/sec
world_query_for_each/50000_entities_sparse                                                                1.00     32.7±0.12µs        ? ?/sec      1.01     33.1±0.46µs        ? ?/sec
```
</details>

This roughly doubles command spawning speed! Despawning also sees a
20-30% improvement. Dummy commands improve by 10-50% (due to not needing
an entity flush). Other benchmarks seem to be noise and are negligible.
It looks to me like a massive performance win!

---------

Co-authored-by: Alice Cecile <[email protected]>
Co-authored-by: Christian Hughes <[email protected]>
Co-authored-by: urben1680 <[email protected]>
Co-authored-by: Chris Russell <[email protected]>
Co-authored-by: Trashtalk217 <[email protected]>
Co-authored-by: James Liu <[email protected]>
Co-authored-by: Carter Anderson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants