Skip to content

Conversation

@ElliottjPierce
Copy link
Contributor

@ElliottjPierce ElliottjPierce commented Apr 1, 2025

fixes #18003

Objective

It's version 9 of the same objective lol. For assets as entities, we need entities to be able to be reserved from any thread. Ideally, this can be done without depending on an async context, blocking, or waiting. Any of these compromises could hurt asset performance or discontinue the completely non-blocking nature of the asset system.

As a bonus, this PR makes allocating entities only need &Entities instead of &mut. Entities::flush is now completely optional, meaning none of the Entities methods depends on the flush at all, and there is protection against flushing an entity twice.

(If you're curious, v9 actually branched from v8. v8 was focused on #18577 (never flush entities), but this still includes flushing.)

There's a doc here that builds some background for this too. If you haven't been following this I'd highly recommend reading that before diving into the code.

Solution

Organizationally, I split off the underlying EntityAllocator from Entities. This makes it easier to read, etc, now that it's more involved.

The basic problem is that we need to be able to allocate an entity from any thread at any time. We also need to be able to free an entity. So at the allocator level, that's 3 operations: free, alloc (For when you know free isn't being called), and remote_alloc (can be called any time). None of these can require mutable access.

The biggest challenge is having a list of entities that are free and waiting to be re-used. The list needs to be fully functional without mutable access, needs to be resizable, and needs to be pinned in memory. I ended up using a strategy similar to SplitVec. That dependency requires std, and knowing the max capacity ahead of time lets us simplify the implementation, so I made my own implementation here.

Testing

No new tests right now. It might be worth using loom at some point, but that requires an additional dependency, test-specific loom feature flags, and giving this treatment to multiple crates, especially bevy_platform.

Future work

#18577 is still a good end game here IMO. Ultimately, (just like @maniwani said would happen), I decided that doing this all at once would be both too challenging and add too much complexity. However, v9 makes "never flush" much, much more approachable for the future. The biggest issues I ran into were that lots of places hold a reference to an entity's Archetype (but the entity now might not have an archetype) and that checking archetypes everywhere might actually be less performant than flushing. Maybe.

We can also potentially speed up a lot of different processes now that alloc can be called without mutable access and free (etc.) can be called without needing to flush first.

Costs

Benchmarks
group                                           main_baseline                           remote_reservation_v9_baseline
-----                                           -------------                           ------------------------------
add_remove/sparse_set                           1.06   625.2±42.08µs        ? ?/sec     1.00   591.8±24.97µs        ? ?/sec
add_remove/table                                1.00   883.9±66.56µs        ? ?/sec     1.06   941.1±34.62µs        ? ?/sec
add_remove_very_big/table                       1.08     37.7±2.55ms        ? ?/sec     1.00     34.9±0.76ms        ? ?/sec
added_archetypes/archetype_count/1000           1.13  688.8±175.58µs        ? ?/sec     1.00  608.6±137.61µs        ? ?/sec
added_archetypes/archetype_count/200            1.13    72.7±18.97µs        ? ?/sec     1.00    64.2±20.78µs        ? ?/sec
added_archetypes/archetype_count/2000           1.11  1086.7±290.33µs        ? ?/sec    1.00  977.2±118.15µs        ? ?/sec
added_archetypes/archetype_count/5000           1.09      2.7±0.29ms        ? ?/sec     1.00      2.5±0.27ms        ? ?/sec
despawn_world/10_entities                       1.00   695.6±13.85ns        ? ?/sec     1.09   760.4±40.01ns        ? ?/sec
despawn_world/1_entities                        1.00   182.0±24.14ns        ? ?/sec     1.56   284.3±50.46ns        ? ?/sec
despawn_world_recursive/10000_entities          1.00  1668.8±95.30µs        ? ?/sec     1.13  1878.0±111.75µs        ? ?/sec
despawn_world_recursive/10_entities             1.00      2.3±0.04µs        ? ?/sec     1.05      2.4±0.09µs        ? ?/sec
despawn_world_recursive/1_entities              1.00   382.2±36.07ns        ? ?/sec     1.41   539.1±60.46ns        ? ?/sec
empty_archetypes/iter/10000                     1.07     12.8±1.61µs        ? ?/sec     1.00     12.0±0.49µs        ? ?/sec
empty_archetypes/par_for_each/100               1.06      9.2±1.04µs        ? ?/sec     1.00      8.7±0.35µs        ? ?/sec
empty_archetypes/par_for_each/1000              1.12     12.8±0.87µs        ? ?/sec     1.00     11.5±0.36µs        ? ?/sec
empty_archetypes/par_for_each/10000             1.19     25.4±0.98µs        ? ?/sec     1.00     21.3±0.41µs        ? ?/sec
empty_archetypes/par_for_each/2000              1.16     13.6±1.17µs        ? ?/sec     1.00     11.7±0.44µs        ? ?/sec
empty_archetypes/par_for_each/500               1.08     11.1±0.70µs        ? ?/sec     1.00     10.3±0.28µs        ? ?/sec
empty_commands/0_entities                       1.00      3.9±0.06ns        ? ?/sec     1.40      5.4±0.06ns        ? ?/sec
entity_hash/entity_set_lookup_miss_gen/10000    1.00     41.6±6.26µs 229.0 MElem/sec    1.05     43.9±5.74µs 217.1 MElem/sec
entity_hash/entity_set_lookup_miss_id/10000     1.25     44.5±5.30µs 214.2 MElem/sec    1.00     35.7±5.03µs 266.8 MElem/sec
event_propagation/four_event_types              1.13   606.5±27.06µs        ? ?/sec     1.00    535.9±5.38µs        ? ?/sec
event_propagation/single_event_type             1.12   870.3±27.20µs        ? ?/sec     1.00   776.4±18.86µs        ? ?/sec
fake_commands/2000_commands                     1.00     12.1±0.08µs        ? ?/sec     1.28     15.4±0.25µs        ? ?/sec
fake_commands/4000_commands                     1.00     24.2±0.26µs        ? ?/sec     1.28     30.9±0.38µs        ? ?/sec
fake_commands/6000_commands                     1.00     36.3±0.50µs        ? ?/sec     1.27     46.2±0.48µs        ? ?/sec
fake_commands/8000_commands                     1.00     48.3±0.15µs        ? ?/sec     1.28     61.6±0.87µs        ? ?/sec
insert_simple/base                              1.29   403.9±79.33µs        ? ?/sec     1.00   312.1±56.57µs        ? ?/sec
insert_simple/unbatched                         2.41  1021.5±234.06µs        ? ?/sec    1.00   423.2±17.00µs        ? ?/sec
iter_fragmented/base                            1.00    346.6±8.58ns        ? ?/sec     1.40    485.0±9.30ns        ? ?/sec
iter_fragmented/foreach                         1.07    141.4±6.76ns        ? ?/sec     1.00    132.4±3.85ns        ? ?/sec
iter_fragmented_sparse/base                     1.19      7.9±0.16ns        ? ?/sec     1.00      6.6±0.09ns        ? ?/sec
iter_simple/foreach_wide                        2.76     46.4±0.46µs        ? ?/sec     1.00     16.8±0.18µs        ? ?/sec
iter_simple/foreach_wide_sparse_set             1.00    80.9±13.81µs        ? ?/sec     1.12    90.3±29.10µs        ? ?/sec
observe/trigger_simple                          1.00    450.3±9.25µs        ? ?/sec     1.08    488.5±9.99µs        ? ?/sec
query_get/50000_entities_table                  1.00    138.8±0.67µs        ? ?/sec     1.05    145.9±1.23µs        ? ?/sec
query_get_many_5/50000_calls_sparse             1.06   607.3±14.47µs        ? ?/sec     1.00   570.5±42.57µs        ? ?/sec
sized_commands_0_bytes/2000_commands            1.00     10.6±1.20µs        ? ?/sec     1.26     13.3±0.18µs        ? ?/sec
sized_commands_0_bytes/4000_commands            1.00     20.6±0.33µs        ? ?/sec     1.29     26.5±0.42µs        ? ?/sec
sized_commands_0_bytes/6000_commands            1.00     30.8±0.26µs        ? ?/sec     1.29     39.9±0.61µs        ? ?/sec
sized_commands_0_bytes/8000_commands            1.00     41.4±0.77µs        ? ?/sec     1.28     53.1±0.65µs        ? ?/sec
sized_commands_12_bytes/2000_commands           1.00     11.6±0.29µs        ? ?/sec     1.23     14.2±0.32µs        ? ?/sec
sized_commands_12_bytes/4000_commands           1.00     22.8±3.30µs        ? ?/sec     1.24     28.3±0.30µs        ? ?/sec
sized_commands_12_bytes/6000_commands           1.00     33.6±0.20µs        ? ?/sec     1.27     42.8±0.63µs        ? ?/sec
sized_commands_12_bytes/8000_commands           1.00     48.8±6.10µs        ? ?/sec     1.22     59.7±0.76µs        ? ?/sec
sized_commands_512_bytes/2000_commands          1.00     46.3±1.28µs        ? ?/sec     1.06     48.9±1.73µs        ? ?/sec
sized_commands_512_bytes/4000_commands          1.00     90.5±2.06µs        ? ?/sec     1.08     97.4±3.33µs        ? ?/sec
spawn_commands/2000_entities                    1.00   155.4±11.61µs        ? ?/sec     1.22   189.5±15.70µs        ? ?/sec
spawn_commands/4000_entities                    1.00   303.7±15.69µs        ? ?/sec     1.22   371.1±18.58µs        ? ?/sec
spawn_commands/6000_entities                    1.00   463.3±31.95µs        ? ?/sec     1.20   554.8±12.55µs        ? ?/sec
spawn_commands/8000_entities                    1.00   619.7±44.57µs        ? ?/sec     1.19   734.9±16.71µs        ? ?/sec
spawn_world/1000_entities                       1.06     41.5±2.97µs        ? ?/sec     1.00     39.1±2.94µs        ? ?/sec
spawn_world/100_entities                        1.20      4.8±2.29µs        ? ?/sec     1.00      4.0±0.69µs        ? ?/sec
spawn_world/10_entities                         1.15   461.9±80.36ns        ? ?/sec     1.00   400.5±26.17ns        ? ?/sec
spawn_world/1_entities                          1.05     41.4±6.35ns        ? ?/sec     1.00     39.4±3.02ns        ? ?/sec
world_get/50000_entities_sparse                 1.00    167.1±4.23µs        ? ?/sec     1.07    178.8±2.44µs        ? ?/sec
world_query_get/50000_entities_sparse_wide      1.00    125.0±0.31µs        ? ?/sec     1.08   135.7±78.30µs        ? ?/sec
world_query_iter/50000_entities_sparse          1.00     38.7±0.08µs        ? ?/sec     1.18     45.6±0.60µs        ? ?/sec

Interpreting benches:

In most places, v9 is on par with or even faster than main. Some notable exceptions are the "sized_commands" and "fake_commands" sections, but the regression there is purely due to Entities::flush being slower, but we make up for that elsewhere. These commands don't actually do anything though, so this is not relevant to actual use cases. The benchmarks just exist to stress test CommandQueue.

The only place where v9 causes a significant and real-world applicable regression is "spawn_commands", where v9 is roughly 15% slower than main. This is something that can be changed later now that alloc doesn't need mutable access. I expect we can change this 15% regression to a 15% improvement given that "spawn_world" is roughly 20% faster on v9 than on main. For users that need really fast spawn commands though, they are already using some form of batch spawning or direct world access.

Other regressions seem to be either minimal, unrealistic, easily corrected in the future, or wrong. I feel confident saying "wrong" since running them back to back can sometimes yield different results. I'm on a M2 Max, so there might be some things jumping from perf cores to efficiency cores or something. (I look forward to standardized benchmarking hardware.)

Wins: I was worried that, without "never flush", this would be an overall regression, but I am relived that that is not the case. Some very common operations, "insert_simple/unbatched" for example, are way faster on this branch than on main. Basically, on main, alloc also adds EntityMeta for the entity immediately, but on this branch, we only do so in set. That seems to improve temporal cache locality and leads to this roughly 220% improvement. "added_arhcetype" sees 20%-80% improvements too, etc. "iter_simple/foreach_wide" also sees a 270% improvement.

I think in practice, v9 will out perform main for real-world schedules. And I think moving towards "never flush" (even for only a few operations, like Commands::spawn) will improve performance even more.

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

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

I'm on board for this! I think this approach is as close as we'll get to an ideal solve, given the constraints:

  • keep world-access allocations as fast as possible while also allowing parallel allocations
  • reuse entities across use cases
  • don't require each parallel case to have its own reserved blocks of entities
  • ideally no async

The implementation is clever! I'm going to run the benchmarks locally as one final sanity check. It seems like the entity_allocator_batched_and_freed / entity_allocator_fresh benches haven't been upstreamed. Can you add those to this PR so I can run them?

@cart
Copy link
Member

cart commented Jan 21, 2026

So far with our current benches, the only relevant changes are the despawn benchmarks, which (as expected) have regressed.

The spawn benchmarks are in the noise (+/- < 2%, oscillating back and forth across benches).

I'd still like to run the new benches before merging, but I think the despawn performance is acceptable.

image

@ElliottjPierce
Copy link
Contributor Author

So far with our current benches, the only relevant changes are the despawn benchmarks, which (as expected) have regressed.

Heads up that we might be able to improve this with batching free calls without much trouble, but that's something to try later.

The spawn benchmarks are in the noise (+/- < 2%, oscillating back and forth across benches).

That's as expected. This doesn't really test the FreeList, only the FreshAllocator.

I'd still like to run the new benches before merging, but I think the despawn performance is acceptable.

Yeah, the new benchmarks would shed some light on the situation. My original benchmarks were just quick and dirty tests, and I don't have those anymore. I just kept them around long enough to test the theory.

If you want to double check the perf change when the FreeList is used (and yeah, we probably should), there are two options:

  • We could add a some micro-benches for benching just the allocator itself under different free vs fresh loads. This is what I did originally, and the results were the 2x difference. (That is a tiny regression compared to the whole task of spawning an entity, so I don't think it really affects anything.) But these benches wouldn't really bench anything realistic, so I don't know how useful they would be beyond validating this PR.
  • We could change the existing benches relating to spawning to first pre-process the world by spawning and despawning a ton of entities. This makes running benches take a little longer but will probably make every relevant benchmark more realistic. Of course, now what is being benched will have changed, and that could mess up existing comparisons, etc.

Which route would you like me to take here, and I'm assuming I should do the benchmark changes in a different pr first?

@alice-i-cecile
Copy link
Member

I prefer the second more realistic and robust option. Do merge the benchmarks in a seperate pr first though to give us a baseline!

@cart
Copy link
Member

cart commented Jan 21, 2026

We could add a some micro-benches for benching just the allocator itself under different free vs fresh loads.

Yeah I think benching world.entities_allocator().alloc() (and remote_alloc) under different conditions is the correct way to benchmark what we care about here. Those are "real" scenarios!

Benching similar scenarios for spawning (including some components) is also useful, but it would make it harder to zoom in on what we care about here, so I'm content with skipping them until later (unless you are feeling inclined to do them now too).

@cart
Copy link
Member

cart commented Jan 21, 2026

Also we should really rename world.entities_allocator() to world.entity_allocator(). We missed that when we renamed EntitiesAllocator to EntityAllocator.

@cart
Copy link
Member

cart commented Jan 21, 2026

#22638

github-merge-queue bot pushed a commit that referenced this pull request Jan 23, 2026
…22639)

# Objective

As per
[this](#18670 (comment))
comment on #18670, this PR attempts to make entity related benchmarks
more realistic by warming up the entity allocator. This helps test the
freelist in the entity allocator.

## Solution

This PR introduces a new `WorldBuilder` type that starts with
`World::new`, allows configuration options for warming up the world via
the builder pattern, and then builds the warmed up, realistic world. For
now, the only available "warm up" is for entities, but we could also add
functionality in the future to cache bundle info, pre-create tables, etc
to make our benchmarks more realistic. That is, more closely match the
performance of a running app, rather than an app at startup.

The current implementation for entity warmups allocates some entities
and frees them in a random order. It also spawns the highest allocated
entity index to prepare `Entities`'s location storage, etc. This
involves using `rng` (deterministically), but without this, the entities
are allocated in a linear index order (0, 1, 2, ...), which is
unrealistic and extremely cache friendly (so it probably makes an impact
in performance not desirable for a benchmark).

The major downsides here are that the benches take a little longer to
run now and that startup/caching time is no longer benchmarked. That is
for example, that benchmarking despawning only one entity used to tell
us some information about performance of allocating the free list
(amongst other one time actions). Now, that information is lost since
the world is already warmed up. In practice, for N values of entities,
it used to be the case that a higher N showed the performance of the
operation, and a lower N showed the performance of the operation + any
registration/caching costs. Now, the different N values only tell us
more about how well the CPU accommodates a batch of the operation.

Currently in Bevy, making a change might make the `...1_entity` benches
much worse but the `...1000_entities` much much better because the
change added some new caching. The inverse is also common. With this PR,
that will no longer be the case, at least for entities and whatever else
we add to the `WorldBuilder` in the future. And that change may or may
not be desirable.

## Testing

Ran a sampling of the benchmarks.
@cart
Copy link
Member

cart commented Jan 23, 2026

Alrighty here are the results on my machine! (filtered to benches with "spawn" or "entity_allocator" in the name)
before: main + the two benchmark prs)
after: this pr merged with main + the two benchmark prs

group                                                   after                                  before
-----                                                   ---------                              ----------
despawn_world/10000_entities                            1.21    280.6±6.30µs        ? ?/sec    1.00    231.5±3.53µs        ? ?/sec
despawn_world/100_entities                              1.19      4.0±0.94µs        ? ?/sec    1.00      3.4±0.75µs        ? ?/sec
despawn_world/1_entities                                1.26   197.0±10.40ns        ? ?/sec    1.00    156.1±7.84ns        ? ?/sec
despawn_world_recursive/10000_entities                  1.08  1032.1±64.06µs        ? ?/sec    1.00    951.3±4.22µs        ? ?/sec
despawn_world_recursive/100_entities                    1.31     13.7±0.16µs        ? ?/sec    1.00     10.4±0.13µs        ? ?/sec
despawn_world_recursive/1_entities                      1.01   333.4±27.69ns        ? ?/sec    1.00   330.5±31.46ns        ? ?/sec
ecs::bundles::spawn_many::spawn_many/static             1.18   141.2±10.79µs        ? ?/sec    1.00    120.0±4.28µs        ? ?/sec
ecs::bundles::spawn_many_zst::spawn_many_zst/static     1.07    103.7±8.09µs        ? ?/sec    1.00     97.0±1.41µs        ? ?/sec
ecs::bundles::spawn_one_zst::spawn_one_zst/static       1.00    214.1±2.05µs        ? ?/sec    1.00    213.5±5.33µs        ? ?/sec
entity_allocator_allocate_fresh/10000_entities          1.02     77.2±3.76µs        ? ?/sec    1.00     75.7±0.61µs        ? ?/sec
entity_allocator_allocate_fresh/100_entities            1.07   842.3±43.68ns        ? ?/sec    1.00   783.8±19.69ns        ? ?/sec
entity_allocator_allocate_fresh/1_entities              1.20     10.7±0.36ns        ? ?/sec    1.00      8.9±0.34ns        ? ?/sec
entity_allocator_allocate_fresh_bulk/10000_entities     1.15     12.2±0.56µs        ? ?/sec    1.00     10.7±0.11µs        ? ?/sec
entity_allocator_allocate_fresh_bulk/100_entities       1.30   150.0±15.45ns        ? ?/sec    1.00    115.5±2.27ns        ? ?/sec
entity_allocator_allocate_fresh_bulk/1_entities         1.32     19.7±0.46ns        ? ?/sec    1.00     14.9±0.36ns        ? ?/sec
entity_allocator_allocate_reused/10000_entities         1.55     79.7±4.38µs        ? ?/sec    1.00     51.2±1.45µs        ? ?/sec
entity_allocator_allocate_reused/100_entities           1.59   874.1±37.64ns        ? ?/sec    1.00   549.8±19.90ns        ? ?/sec
entity_allocator_allocate_reused/1_entities             2.54     32.5±6.39ns        ? ?/sec    1.00     12.8±2.02ns        ? ?/sec
entity_allocator_allocate_reused_bulk/10000_entities    1.12     12.1±1.50µs        ? ?/sec    1.00     10.7±0.23µs        ? ?/sec
entity_allocator_allocate_reused_bulk/100_entities      1.43   161.2±17.06ns        ? ?/sec    1.00    113.0±4.61ns        ? ?/sec
entity_allocator_allocate_reused_bulk/1_entities        2.85     42.5±8.21ns        ? ?/sec    1.00     14.9±0.58ns        ? ?/sec
entity_allocator_free/10000_entities                    4.20     92.1±6.97µs        ? ?/sec    1.00     21.9±0.34µs        ? ?/sec
entity_allocator_free/100_entities                      2.67   984.7±21.72ns        ? ?/sec    1.00   368.8±16.15ns        ? ?/sec
entity_allocator_free/1_entities                        8.57   147.1±13.05ns        ? ?/sec    1.00     17.2±0.66ns        ? ?/sec
nonempty_spawn_commands/10000_entities                  1.11   203.6±17.13µs        ? ?/sec    1.00   183.2±14.29µs        ? ?/sec
nonempty_spawn_commands/1000_entities                   1.14     19.3±1.10µs        ? ?/sec    1.00     16.9±1.32µs        ? ?/sec
nonempty_spawn_commands/100_entities                    1.15  1985.8±92.96ns        ? ?/sec    1.00  1725.4±97.48ns        ? ?/sec
spawn_commands/10000_entities                           1.05   768.7±82.07µs        ? ?/sec    1.00   729.2±24.41µs        ? ?/sec
spawn_commands/1000_entities                            1.05     75.2±6.64µs        ? ?/sec    1.00     71.6±1.94µs        ? ?/sec
spawn_commands/100_entities                             1.01      7.2±0.27µs        ? ?/sec    1.00      7.1±0.21µs        ? ?/sec
spawn_world/10000_entities                              1.06   462.1±38.21µs        ? ?/sec    1.00   437.5±34.12µs        ? ?/sec
spawn_world/100_entities                                1.06      4.8±0.44µs        ? ?/sec    1.00      4.5±0.45µs        ? ?/sec
spawn_world/1_entities                                  1.01     44.2±5.15ns        ? ?/sec    1.00     43.9±4.81ns        ? ?/sec
spawn_world_batch/10000_entities                        1.00   350.4±47.49µs        ? ?/sec    1.00   351.7±45.63µs        ? ?/sec
spawn_world_batch/1000_entities                         1.14   405.8±47.82µs        ? ?/sec    1.00   356.1±48.36µs        ? ?/sec
spawn_world_batch/100_entities                          1.00   355.5±43.25µs        ? ?/sec    1.00   354.9±41.74µs        ? ?/sec
spawn_world_batch/1_entities                            1.03   506.6±51.10µs        ? ?/sec    1.00   491.2±44.52µs        ? ?/sec

The "free" op (and therefore despawn) has definitely taken the biggest hit. But not to a worrying degree relative to what we gain. And in the context of a "full" entity despawn (and not just the free), the regression is relatively tame.

@cart cart added this pull request to the merge queue Jan 23, 2026
github-merge-queue bot pushed a commit that referenced this pull request Jan 23, 2026
# Objective

As per
[this](#18670 (comment))
comment on #18670, this adds benchmarks for direct access to the entity
allocator.

## Solution

Add 5 groups of benchmarks:

- allocating fresh entities
- allocating fresh entities in bulk
- freeing entities
- allocating reused entities
- allocating reused entities in bulk

## Testing

- CI and benches
Merged via the queue into bevyengine:main with commit 1bd6400 Jan 23, 2026
38 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Jan 23, 2026
# Objective

#18670 introduced a small but fatal issue with batch spawning. Previous
tests did not catch this bug because they tested `alloc` and
`alloc_many` separately.

## Solution

- Fix an off by one math mistake in `FreeBufferIterator::next`.
- Fix the part where you need to `ptr.add(index)` if you want the
element at `index`. Whoops.
- Add a test to catch these issues next time

## Testing

- CI
- One new test
- This was originally found
[here](https://discord.com/channels/691052431525675048/749335865876021248/1464359124950319114),
and the reproduction crashes on main but is fine on this branch.
beicause pushed a commit to beicause/bevy that referenced this pull request Jan 24, 2026
# Objective

bevyengine#18670 introduced a small but fatal issue with batch spawning. Previous
tests did not catch this bug because they tested `alloc` and
`alloc_many` separately.

## Solution

- Fix an off by one math mistake in `FreeBufferIterator::next`.
- Fix the part where you need to `ptr.add(index)` if you want the
element at `index`. Whoops.
- Add a test to catch these issues next time

## Testing

- CI
- One new test
- This was originally found
[here](https://discord.com/channels/691052431525675048/749335865876021248/1464359124950319114),
and the reproduction crashes on main but is fine on this branch.
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-Feature A new feature, making something new possible M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-SME Decision or review from an SME is required X-Controversial There is active debate or serious implications around merging this PR

Projects

Status: Respond (With Priority)

Development

Successfully merging this pull request may close these issues.

Reserve entities from async