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

Prevent creation of superfluous empty table #16935

Merged

Conversation

omaskery
Copy link
Contributor

@omaskery omaskery commented Dec 22, 2024

Objective

  • To fix a tiny bug in bevy_ecs::storage::Tables that, in one case, means it accidentally allocates an additional "empty" Table, resulting in two "empty" Tables:
    • The one pre-allocated empty table at index 0 whose index is designed to match up with TableId::empty()
    • One extra empty table, at some non-0 index, that does not match up with TableId::empty().
  • This PR aims to prevent this extraneous Table, ensuring that entities with no components in table-storage reliably have their archetype's table ID be equal to TableId::empty().

Solution

Background

The issue occurs because:

  • Tables contains:
    • tables: Vec<Table> - The set of all Tables allocated in the world.
    • table_ids: HashMap<Box<[ComponentId]>, TableId> - An index to rapidly lookup the Table in tables by a set of ComponentIds.
  • When Tables is constructed it pre-populates the tables Vec with an empty Table.
    • This ensures that the first entry (index 0) is always the Table for entities with no components in table storage.
    • In particular, TableId::empty() is a utility that returns a TableId of 0.
  • However, the table_ids map is not initialised to associate an empty [ComponentId] with TableId 0.
  • This means, the first time a structural change tries to access a Table for an archetype with 0 table components:
    • Tables::get_id_or_insert is used to retrieve the target Table
    • The function attempts to lookup the entry in the table_ids HashMap whose key is the empty ComponentId set
    • The empty Table created at startup won't be found, because it was never inserted into table_ids
    • It will instead create a new table, insert it into the HashMap (preventing further instances of this issue), and return it.

Changes

  • I considered simply initialising the table_ids HashMap to know about the pre-allocated Table
  • However, I ended up using the proposed solution discussed on Discord #ecs-dev:
    • Make Tables::get_id_or_insert simply early-exit if the requested component_ids was empty.
    • This avoids unnecessarily hashing the empty slice and looking it up in the HashMap.
    • The table_ids HashMap is not exposed outside this struct, and is only used within get_id_or_insert, so it seems wasteful to defensively populate it with the empty Table.

Testing

This is my first Bevy contribution, so I don't really know the processes that well. That said:

  • I have introduced a little test that exercises the original issue and shows that it is now resolved.
  • I have run the bevy_ecs tests locally, so I have reasonable confidence I haven't broken that.
  • I haven't run any further test suites, mostly as when I tried to run test suites for the whole project it filled my entire SSD with >600GB of target directory output 😱😱😱

Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@omaskery omaskery force-pushed the prevent-superfluous-empty-table branch from 8857d67 to a571d5b Compare December 22, 2024 21:10
@omaskery omaskery force-pushed the prevent-superfluous-empty-table branch from a571d5b to 2583764 Compare December 22, 2024 21:22
@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-ECS Entities, components, systems, and events S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Dec 22, 2024
@alice-i-cecile alice-i-cecile added the D-Straightforward Simple bug fixes and API improvements, docs, test and examples label Dec 22, 2024
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Good find, and good tests! Very weird about the cargo test output!

Copy link
Contributor

@Victoronz Victoronz 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!

@Victoronz Victoronz 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 Dec 22, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Dec 22, 2024
Merged via the queue into bevyengine:main with commit 022c6b1 Dec 22, 2024
33 checks passed
@omaskery omaskery deleted the prevent-superfluous-empty-table branch December 22, 2024 23:37
pcwalton pushed a commit to pcwalton/bevy that referenced this pull request Dec 25, 2024
# Objective

- To fix a tiny bug in `bevy_ecs::storage::Tables` that, in one case,
means it accidentally allocates an additional "empty" `Table`, resulting
in two "empty" `Table`s:
- The one pre-allocated empty table at index 0 whose index is designed
to match up with `TableId::empty()`
- One extra empty table, at some non-0 index, that does not match up
with `TableId::empty()`.
- This PR aims to prevent this extraneous `Table`, ensuring that
entities with no components in table-storage reliably have their
archetype's table ID be equal to `TableId::empty()`.

## Solution

### Background

The issue occurs because:

- `Tables` contains:
- `tables: Vec<Table>` - The set of all `Table`s allocated in the world.
- `table_ids: HashMap<Box<[ComponentId]>, TableId>` - An index to
rapidly lookup the `Table` in `tables` by a set of `ComponentId`s.
- When `Tables` is constructed it pre-populates the `tables` `Vec` with
an empty `Table`.
- This ensures that the first entry (index 0) is always the `Table` for
entities with no components in table storage.
- In particular, `TableId::empty()` is a utility that returns a
`TableId` of `0`.
- However, the `table_ids` map is not initialised to associate an empty
`[ComponentId]` with `TableId` `0`.
- This means, the first time a structural change tries to access a
`Table` for an archetype with 0 table components:
  - `Tables::get_id_or_insert` is used to retrieve the target `Table`
- The function attempts to lookup the entry in the `table_ids` `HashMap`
whose key is the empty `ComponentId` set
- The empty `Table` created at startup won't be found, because it was
never inserted into `table_ids`
- It will instead create a new table, insert it into the `HashMap`
(preventing further instances of this issue), and return it.

### Changes

- I considered simply initialising the `table_ids` `HashMap` to know
about the pre-allocated `Table`
- However, I ended up using the proposed solution discussed on Discord
[#ecs-dev](https://discord.com/channels/691052431525675048/749335865876021248/1320430933152759958):
- Make `Tables::get_id_or_insert` simply early-exit if the requested
`component_ids` was empty.
- This avoids unnecessarily hashing the empty slice and looking it up in
the `HashMap`.
- The `table_ids` `HashMap` is not exposed outside this struct, and is
only used within `get_id_or_insert`, so it seems wasteful to defensively
populate it with the empty `Table`.

## Testing

This is my first Bevy contribution, so I don't really know the processes
that well. That said:
- I have introduced a little test that exercises the original issue and
shows that it is now resolved.
- I have run the `bevy_ecs` tests locally, so I have reasonable
confidence I haven't broken that.
- I haven't run any further test suites, mostly as when I tried to run
test suites for the whole project it filled my entire SSD with >600GB of
target directory output 😱😱😱
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-Bug An unexpected or incorrect behavior 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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants