Fragmenting value components
Objective
This PR allows to fragment archetypes by component value, which is useful for implementing some other ECS features such as:
- Fragmenting relations
- Shared components
- Indexes
- Maybe more?
Solution
Challenges
To implement fragmentation by component value there are a couple of problems that needed solving:
Archetypes are defined as "a group of entities that share the same components: a world only has one archetype for each unique combination of components", which might not necessarily be true if we also fragment by values (depending on implementation)Bundleis a wholly type-level concept. Bundles of components uniquely identify transitions between different archetypes. Fragmenting by value requires taking into account actual value of components, not just their type.Queryis a type-level solution - there is no way to describe by which value components it should filter.
This PR addresses all 3 of these problems to varying levels of success.
Archetype
There are multiple ways to achieve fragmentation of archetypes based on component value. One of the most well-know solutions is to encode each value as a different ComponentId. #17608 goes event further and encodes each value as a unique combination of "Fragmenting Marker Components". These solutions are valid and (probably) more correct, however at the time of writing creating large number ComponentIds is problematic in Bevy, although there is ongoing work to fix this. To make this usable before all the issues are fixed, this PR takes a different approach, which doesn't inflate ComponentId and is designed to be replaceable by whatever approach will turn out to be more optimal.
This PR changes archetype's identity to also contain value components:
#[derive(Hash, PartialEq, Eq)]
struct ArchetypeComponents {
table_components: Box<[ComponentId]>,
sparse_set_components: Box<[ComponentId]>,
value_components: FragmentingValuesOwned, // NEW
}
Now some component values can become a part of archetype's identity. It also changes Edges to take component values into account when building transition graph between archetypes.
Bundle
Internally every combination of components, Bundle, has a unique BundleId. This id depends only on type-level information, which is the reason for why it is problematic to make it utilize component values. This PR addresses this issue by allowing to create FragmentingValuesBorrowed from bundles of supported components, which can be used to traverse the archetype graph based on component values. Various batched operations also have been modified to make them aware of the fact that some bundles (can be determined at compile time) might need special handling.
pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static {
// ...
/// Returns all component values that can fragment the archetype.
fn get_fragmenting_values<'a>(
&'a self,
components: &mut ComponentsRegistrator,
values: &mut impl FnMut(ComponentId, &'a dyn FragmentingValue),
);
/// Returns `true` if this bundle contains any components that can fragment the archetype by value.
fn has_fragmenting_values() -> bool;
}
Query
Removed from this PR to make reviewing it easier. It is best to implement filtering by component values for queries in a separate PR that'll be more focused on that.
Previous explanation
We cannot express which component values to choose in system's query signature and must be used inside the system body. Since filtering by component value is supposed to be archetype-level filtering, this complicates things, as we have to remove already matched archetypes from the QueryState. To work around this issue, this PR adds filter_by_component_value and filter_by_component_value_and_id to queries, which returns a QueryLens. It's not the most optimal implementation - there is an inherent QueryState cloning overhead, as well as the extra filtering of already matched archetypes. This part of the PR is probably the weakest since it was more of an afterthought - my main reason for implementing fragmentation by value was to be utilized by shared components, which require only archetype fragmentation, not filtering by query. Either way, I think it is best to implement more robust query filtering in a separate PR.
fn system_fragmenting(query: Query<&Fragmenting>) {
for fragmenting in query.filter_by_component_value(&Fragmenting(1)).query().iter() {
assert_eq!(fragmenting, &Fragmenting(1));
}
}
Components
There are certain restrictions that components must follow to be usable as fragmenting value components:
- Must be immutable
- Must implement
FragmentingValuetrait, which is implemented automatically for anyC: Component<Mutability = Immutable> + Eq + Hash + Clone
All of these restrictions are checked statically at compile time using a new associated type for Component: Key. There are 2 main types of component keys for now: NoKey which is the default component behavior, and SelfKey which is the new fragmenting by value behavior. I also have plans to use OtherComponentKey to implement "component groups"/"reverse requires" (where any component can opt-into being added together with some other "component key", which can be defined without changing implementation of the key component), but that is still a WIP idea.
#[derive(Component, Clone, Eq, PartialEq, Hash)]
#[component(
key=Self,
immutable,
)]
struct Fragmenting(u32); // Each u32 value will be in a different archetype.
This practically ensures that all components within the archetype contain the same value (or however component's PartialEq::eq implementation defines "same") and it seems a bit wasteful to store all of them. This is something I'm planning to solve in a later PR by introducing new storage type - Shared, which would allow to store one component value per archetype instead of one per entity.
Testing
fragmenting_value.rs contains some tests covering the added functionality.
Benchmarks
group fragmenting-value main-no-boost
----- ----------------- -------------
add_remove/sparse_set 1.16 1447.3±34.71µs ? ?/sec 1.00 1243.6±13.78µs ? ?/sec
add_remove/table 1.14 1829.7±6.85µs ? ?/sec 1.00 1608.3±11.27µs ? ?/sec
add_remove_big/sparse_set 1.02 1497.3±61.67µs ? ?/sec 1.00 1462.5±46.84µs ? ?/sec
add_remove_big/table 1.00 3.1±0.12ms ? ?/sec 1.03 3.2±0.10ms ? ?/sec
add_remove_fragmenting_value/fragmenting 1.00 1253.1±150.66µs ? ?/sec
add_remove_fragmenting_value/non_fragmenting 1.00 2.7±0.02ms ? ?/sec
add_remove_very_big/table 1.17 80.0±2.25ms ? ?/sec 1.00 68.6±0.94ms ? ?/sec
added_archetypes/archetype_count/100 1.03 34.1±1.15µs ? ?/sec 1.00 33.2±1.90µs ? ?/sec
added_archetypes/archetype_count/1000 1.00 788.8±7.29µs ? ?/sec 1.01 793.7±7.57µs ? ?/sec
added_archetypes/archetype_count/10000 1.00 15.5±0.45ms ? ?/sec 1.11 17.2±1.00ms ? ?/sec
despawn_world/10000_entities 1.06 683.5±14.03µs ? ?/sec 1.00 645.6±12.35µs ? ?/sec
despawn_world/100_entities 1.05 8.2±0.22µs ? ?/sec 1.00 7.8±0.16µs ? ?/sec
despawn_world/1_entities 1.00 282.0±24.57ns ? ?/sec 1.05 297.4±27.52ns ? ?/sec
despawn_world_recursive/10000_entities 1.00 3.7±0.04ms ? ?/sec 1.00 3.7±0.08ms ? ?/sec
despawn_world_recursive/100_entities 1.00 37.9±0.45µs ? ?/sec 1.01 38.1±0.63µs ? ?/sec
despawn_world_recursive/1_entities 1.00 728.2±39.53ns ? ?/sec 1.00 731.4±42.00ns ? ?/sec
ecs::entity_cloning::hierarchy_many/clone 1.00 306.1±53.63µs 1161.3 KElem/sec 1.11 341.0±62.33µs 1042.4 KElem/sec
ecs::entity_cloning::hierarchy_many/reflect 1.00 675.1±15.20µs 526.6 KElem/sec 1.01 685.0±58.53µs 519.0 KElem/sec
ecs::entity_cloning::hierarchy_tall/clone 1.07 24.3±64.69µs 2.0 MElem/sec 1.00 22.8±65.65µs 2.1 MElem/sec
ecs::entity_cloning::hierarchy_tall/reflect 1.08 28.2±61.31µs 1764.4 KElem/sec 1.00 26.1±56.81µs 1909.8 KElem/sec
ecs::entity_cloning::hierarchy_wide/clone 1.06 13.9±0.66µs 3.5 MElem/sec 1.00 13.1±0.80µs 3.7 MElem/sec
ecs::entity_cloning::hierarchy_wide/reflect 1.13 18.3±0.32µs 2.7 MElem/sec 1.00 16.2±0.32µs 3.0 MElem/sec
ecs::entity_cloning::single/clone 1.08 915.5±200.13ns 1066.7 KElem/sec 1.00 850.5±175.14ns 1148.3 KElem/sec
ecs::entity_cloning::single/reflect 1.03 1772.4±143.57ns 551.0 KElem/sec 1.00 1726.5±145.17ns 565.6 KElem/sec
insert_fragmenting_value/base 1.00 1103.2±10.66µs ? ?/sec
insert_fragmenting_value/high_fragmentation_base 1.00 1276.4±13.56µs ? ?/sec
insert_fragmenting_value/high_fragmentation_unbatched 1.00 1356.3±15.01µs ? ?/sec
insert_fragmenting_value/unbatched 1.00 1149.3±5.08µs ? ?/sec
insert_simple/base 1.06 442.5±4.42µs ? ?/sec 1.00 418.2±2.30µs ? ?/sec
insert_simple/unbatched 1.13 914.0±11.34µs ? ?/sec 1.00 809.3±8.07µs ? ?/sec
no_archetypes/system_count/0 1.00 17.1±0.09ns ? ?/sec 1.02 17.3±0.10ns ? ?/sec
no_archetypes/system_count/10 1.02 161.8±1.45ns ? ?/sec 1.00 159.1±1.41ns ? ?/sec
no_archetypes/system_count/100 1.00 1529.1±10.17ns ? ?/sec 1.00 1530.3±37.93ns ? ?/sec
spawn_world/10000_entities 1.02 1139.8±268.54µs ? ?/sec 1.00 1120.6±248.69µs ? ?/sec
spawn_world/100_entities 1.02 9.6±2.30µs ? ?/sec 1.00 9.5±2.21µs ? ?/sec
spawn_world/1_entities 1.04 99.4±23.02ns ? ?/sec 1.00 95.4±22.78ns ? ?/sec
Future work
Storing fragmenting values as dyn FragmentingValue is optimal for one-off operations such as insert or spawn, however when doing bulk operations or filtering Query it has unnecessary dyn vtable lookup overhead for every compared fragmenting value, and it also means that fragmenting values have to be stored multiple times in multiple places (Edges, ArchetypeComponents, ArchetypeComponentInfo), which can lead to excessive memory usage when fragmenting value component is large. One approach to solve this is to have a global map dyn FragmentingValue -> ComponentId and make all operations compare ComponentId's instead. This has the downside of making one-off operations less optimal since there is an additional hashmap lookup we have to perform every time before we can even use the values, even if we didn't need to do that with the current approach. Another way this could be solved is to make FragmetingValuesOwned and ArchetypeComponentInfo store Arc<dyn FragmetingValue>'s instead. This would allow best of both worlds: when doing the one-off operations we can just compare &dyn FragmentingValue's, and when doing bulk operations we can map dyn FragmentingValue -> Arc<dyn FragmentingValue> and skip the dyn overhead by using pointer comparison instead. Creating new FragmetingValuesOwned will be slightly more expensive, but that is not a very common operation.
TODO
- [x] Better documentation
- [x] Migration guide
- [x] Benchmarks
- [ ] More tests?
Your PR increases Bevy Minimum Supported Rust Version. Please update the rust-version field in the root Cargo.toml file.
It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.
Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.
It looks like your PR is a breaking change, but you didn't provide a migration guide.
Please review the instructions for writing migration guides, then expand or revise the content in the migration guides directory to reflect your changes.
Would it make sense to just remove querying from this PR entirely, then? The implementation here proves that it's possible to query these, which is important! But removing it from this PR will make it a little easier to review, and should make the future PR to make querying work well easier to review and bikeshed since it will be starting from scratch.
I can do that. There wouldn't be any way to use this new functionality without query implementation, so I though I had to provide at least some way to justify the changes. But in general it is not required for what I want to use this functionality for later.
- Must not have
Tablestorage type (maybe possible to remove this restriction?)What is the reason for this restriction in the current implementation? I didn't see anything obvious that would fail without the check.
I think this was because I needed the components to be filterable on set_archetype in WorldQuery? I wanted to do the filtering somewhere in fetch implementation first, but later the plans changed and I probably don't need this anymore. The INVARIANT_ASSERT would still need to exist for the Shared storage type I'm planning on adding next, but maybe that can be done in the PR that would actually add it then?
I can do that. There wouldn't be any way to use this new functionality without query implementation, so I though I had to provide at least some way to justify the changes. But in general it is not required for what I want to use this functionality for later.
I think folks here are usually okay with splitting things up even if the intermediate steps aren't immediately useful, and I think this is well motivated by the future work, but I'm just a random reviewer with no actual authority :). If you leave it in, I probably won't be able to resist bikeshedding it a bit, though :).
I think this was because I needed the components to be filterable on
set_archetypeinWorldQuery? I wanted to do the filtering somewhere in fetch implementation first, but later the plans changed and I probably don't need this anymore. TheINVARIANT_ASSERTwould still need to exist for theSharedstorage type I'm planning on adding next, but maybe that can be done in the PR that would actually add it then?
I don't have strong opinions here; I was just worried that I missed something. Once Shared exists, I can't imagine there will be a reason to use anything else, so it makes sense that we'll want the checks eventually.
@alice-i-cecile
Now that I've removed filter_by_component_value implementation for Query, I don't think this needs a release note or new examples anymore? The changes are purely internal now and don't introduce any new user-usable features.
Figured out how to disable cpu dynamic boost on my laptop and ran some benchmarks:
Benchmarks
group fragmenting-value main-no-boost
----- ----------------- -------------
add_remove/sparse_set 1.16 1447.3±34.71µs ? ?/sec 1.00 1243.6±13.78µs ? ?/sec
add_remove/table 1.14 1829.7±6.85µs ? ?/sec 1.00 1608.3±11.27µs ? ?/sec
add_remove_big/sparse_set 1.02 1497.3±61.67µs ? ?/sec 1.00 1462.5±46.84µs ? ?/sec
add_remove_big/table 1.00 3.1±0.12ms ? ?/sec 1.03 3.2±0.10ms ? ?/sec
add_remove_fragmenting_value/fragmenting 1.00 1253.1±150.66µs ? ?/sec
add_remove_fragmenting_value/non_fragmenting 1.00 2.7±0.02ms ? ?/sec
add_remove_very_big/table 1.17 80.0±2.25ms ? ?/sec 1.00 68.6±0.94ms ? ?/sec
added_archetypes/archetype_count/100 1.03 34.1±1.15µs ? ?/sec 1.00 33.2±1.90µs ? ?/sec
added_archetypes/archetype_count/1000 1.00 788.8±7.29µs ? ?/sec 1.01 793.7±7.57µs ? ?/sec
added_archetypes/archetype_count/10000 1.00 15.5±0.45ms ? ?/sec 1.11 17.2±1.00ms ? ?/sec
despawn_world/10000_entities 1.06 683.5±14.03µs ? ?/sec 1.00 645.6±12.35µs ? ?/sec
despawn_world/100_entities 1.05 8.2±0.22µs ? ?/sec 1.00 7.8±0.16µs ? ?/sec
despawn_world/1_entities 1.00 282.0±24.57ns ? ?/sec 1.05 297.4±27.52ns ? ?/sec
despawn_world_recursive/10000_entities 1.00 3.7±0.04ms ? ?/sec 1.00 3.7±0.08ms ? ?/sec
despawn_world_recursive/100_entities 1.00 37.9±0.45µs ? ?/sec 1.01 38.1±0.63µs ? ?/sec
despawn_world_recursive/1_entities 1.00 728.2±39.53ns ? ?/sec 1.00 731.4±42.00ns ? ?/sec
ecs::entity_cloning::hierarchy_many/clone 1.00 306.1±53.63µs 1161.3 KElem/sec 1.11 341.0±62.33µs 1042.4 KElem/sec
ecs::entity_cloning::hierarchy_many/reflect 1.00 675.1±15.20µs 526.6 KElem/sec 1.01 685.0±58.53µs 519.0 KElem/sec
ecs::entity_cloning::hierarchy_tall/clone 1.07 24.3±64.69µs 2.0 MElem/sec 1.00 22.8±65.65µs 2.1 MElem/sec
ecs::entity_cloning::hierarchy_tall/reflect 1.08 28.2±61.31µs 1764.4 KElem/sec 1.00 26.1±56.81µs 1909.8 KElem/sec
ecs::entity_cloning::hierarchy_wide/clone 1.06 13.9±0.66µs 3.5 MElem/sec 1.00 13.1±0.80µs 3.7 MElem/sec
ecs::entity_cloning::hierarchy_wide/reflect 1.13 18.3±0.32µs 2.7 MElem/sec 1.00 16.2±0.32µs 3.0 MElem/sec
ecs::entity_cloning::single/clone 1.08 915.5±200.13ns 1066.7 KElem/sec 1.00 850.5±175.14ns 1148.3 KElem/sec
ecs::entity_cloning::single/reflect 1.03 1772.4±143.57ns 551.0 KElem/sec 1.00 1726.5±145.17ns 565.6 KElem/sec
insert_fragmenting_value/base 1.00 1103.2±10.66µs ? ?/sec
insert_fragmenting_value/high_fragmentation_base 1.00 1276.4±13.56µs ? ?/sec
insert_fragmenting_value/high_fragmentation_unbatched 1.00 1356.3±15.01µs ? ?/sec
insert_fragmenting_value/unbatched 1.00 1149.3±5.08µs ? ?/sec
insert_simple/base 1.06 442.5±4.42µs ? ?/sec 1.00 418.2±2.30µs ? ?/sec
insert_simple/unbatched 1.13 914.0±11.34µs ? ?/sec 1.00 809.3±8.07µs ? ?/sec
no_archetypes/system_count/0 1.00 17.1±0.09ns ? ?/sec 1.02 17.3±0.10ns ? ?/sec
no_archetypes/system_count/10 1.02 161.8±1.45ns ? ?/sec 1.00 159.1±1.41ns ? ?/sec
no_archetypes/system_count/100 1.00 1529.1±10.17ns ? ?/sec 1.00 1530.3±37.93ns ? ?/sec
spawn_world/10000_entities 1.02 1139.8±268.54µs ? ?/sec 1.00 1120.6±248.69µs ? ?/sec
spawn_world/100_entities 1.02 9.6±2.30µs ? ?/sec 1.00 9.5±2.21µs ? ?/sec
spawn_world/1_entities 1.04 99.4±23.02ns ? ?/sec 1.00 95.4±22.78ns ? ?/sec
I've run the benchmarks again to see if anything changed with the latest main.
ecs benchmarks with >5% threshold
group main value-components
----- ---- ----------------
add_remove_big/table 1.10 3.0±0.11ms ? ?/sec 1.00 2.8±0.04ms ? ?/sec
add_remove_very_big/table 1.05 96.2±2.24ms ? ?/sec 1.00 91.6±0.75ms ? ?/sec
all_added_detection/50000_entities_ecs::change_detection::Sparse 1.11 86.9±1.27µs ? ?/sec 1.00 78.6±0.51µs ? ?/sec
all_added_detection/5000_entities_ecs::change_detection::Sparse 1.09 8.7±0.08µs ? ?/sec 1.00 8.0±0.11µs ? ?/sec
all_changed_detection/50000_entities_ecs::change_detection::Sparse 1.00 79.5±1.53µs ? ?/sec 1.24 99.0±2.10µs ? ?/sec
all_changed_detection/5000_entities_ecs::change_detection::Sparse 1.00 8.3±0.34µs ? ?/sec 1.16 9.6±0.09µs ? ?/sec
despawn_world/1_entities 1.14 302.4±37.66ns ? ?/sec 1.00 264.7±14.16ns ? ?/sec
despawn_world_recursive/1_entities 1.05 598.9±42.75ns ? ?/sec 1.00 568.4±25.44ns ? ?/sec
ecs::bundles::insert_many::insert_many/all 1.05 4.7±0.08ms ? ?/sec 1.00 4.4±0.12ms ? ?/sec
ecs::bundles::spawn_many::spawn_many/static 1.00 284.7±5.84µs ? ?/sec 1.08 308.3±5.37µs ? ?/sec
ecs::entity_cloning::filter/opt_in_all 1.00 205.2±0.77ns 4.6 MElem/sec 1.06 216.7±1.41ns 4.4 MElem/sec
ecs::entity_cloning::filter/opt_in_all_keep_all 1.00 211.2±1.33ns 4.5 MElem/sec 1.07 225.5±2.06ns 4.2 MElem/sec
ecs::entity_cloning::filter/opt_in_all_keep_all_without_required 1.00 175.3±0.87ns 5.4 MElem/sec 1.07 187.0±0.31ns 5.1 MElem/sec
ecs::entity_cloning::filter/opt_in_all_keep_none 1.00 207.2±1.20ns 4.6 MElem/sec 1.07 222.1±3.40ns 4.3 MElem/sec
ecs::entity_cloning::filter/opt_in_all_keep_none_without_required 1.00 183.9±1.14ns 5.2 MElem/sec 1.06 195.3±0.81ns 4.9 MElem/sec
ecs::entity_cloning::filter/opt_in_all_without_required 1.00 178.9±1.21ns 5.3 MElem/sec 1.07 190.9±0.48ns 5.0 MElem/sec
ecs::entity_cloning::filter/opt_in_none 1.00 162.4±1.07ns 5.9 MElem/sec 1.07 173.6±1.64ns 5.5 MElem/sec
ecs::entity_cloning::filter/opt_out_all 1.00 187.5±1.30ns 5.1 MElem/sec 1.05 197.0±1.59ns 4.8 MElem/sec
ecs::entity_cloning::filter/opt_out_none 1.00 186.5±0.74ns 5.1 MElem/sec 1.06 198.4±2.44ns 4.8 MElem/sec
ecs::entity_cloning::filter/opt_out_none_keep_all 1.00 171.9±4.03ns 5.5 MElem/sec 1.07 183.8±0.51ns 5.2 MElem/sec
ecs::entity_cloning::filter/opt_out_none_keep_none 1.00 193.6±1.95ns 4.9 MElem/sec 1.10 213.5±1.60ns 4.5 MElem/sec
ecs::entity_cloning::hierarchy_tall/clone 1.07 24.3±69.51µs 2047.9 KElem/sec 1.00 22.8±43.01µs 2.1 MElem/sec
ecs::entity_cloning::hierarchy_wide/clone 1.00 14.0±0.68µs 3.5 MElem/sec 1.05 14.7±0.61µs 3.3 MElem/sec
entity_hash/entity_set_build/3162 1.05 19.6±1.93µs 153.8 MElem/sec 1.00 18.7±0.82µs 161.6 MElem/sec
entity_hash/entity_set_lookup_hit/10000 1.00 26.3±0.20µs 363.1 MElem/sec 1.05 27.6±0.24µs 345.6 MElem/sec
entity_hash/entity_set_lookup_miss_id/3162 1.09 12.1±0.07µs 249.2 MElem/sec 1.00 11.1±0.06µs 271.9 MElem/sec
events_send/size_4_events_100 1.00 156.9±0.59ns ? ?/sec 1.13 177.7±0.50ns ? ?/sec
events_send/size_4_events_10000 1.00 18.0±0.07µs ? ?/sec 1.05 19.0±0.13µs ? ?/sec
events_send/size_512_events_100 1.18 3.6±0.02µs ? ?/sec 1.00 3.0±0.01µs ? ?/sec
events_send/size_512_events_1000 1.18 36.6±0.22µs ? ?/sec 1.00 31.0±0.17µs ? ?/sec
events_send/size_512_events_10000 1.21 389.6±21.55µs ? ?/sec 1.00 322.0±18.08µs ? ?/sec
few_changed_detection/50000_entities_ecs::change_detection::Sparse 1.13 113.2±2.27µs ? ?/sec 1.00 100.6±0.88µs ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Sparse 1.05 5.7±0.29µs ? ?/sec 1.00 5.4±0.25µs ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Table 1.08 6.0±0.13µs ? ?/sec 1.00 5.5±0.13µs ? ?/sec
heavy_compute/base 1.06 371.2±25.89µs ? ?/sec 1.00 351.2±18.31µs ? ?/sec
insert_simple/base 1.00 431.3±4.74µs ? ?/sec 1.10 473.4±2.80µs ? ?/sec
iter_fragmented(4096)_empty/foreach_table 1.00 3.2±0.15µs ? ?/sec 1.07 3.4±0.15µs ? ?/sec
iter_fragmented/foreach 1.10 211.5±38.23ns ? ?/sec 1.00 191.6±30.56ns ? ?/sec
iter_fragmented_sparse/base 1.00 8.1±0.42ns ? ?/sec 1.06 8.6±0.03ns ? ?/sec
iter_fragmented_sparse/wide 1.35 80.0±4.51ns ? ?/sec 1.00 59.1±1.06ns ? ?/sec
iter_simple/wide 1.06 60.8±0.45µs ? ?/sec 1.00 57.2±0.25µs ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Sparse 1.11 1424.4±140.19µs ? ?/sec 1.00 1281.4±113.51µs ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Sparse 1.32 157.1±13.68µs ? ?/sec 1.00 118.8±10.11µs ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Table 1.15 84.7±6.68µs ? ?/sec 1.00 73.7±3.09µs ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_100_entities_ecs::change_detection::Sparse 1.34 18.0±0.96µs ? ?/sec 1.00 13.4±0.30µs ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_100_entities_ecs::change_detection::Table 1.10 11.4±0.90µs ? ?/sec 1.00 10.3±0.34µs ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10_entities_ecs::change_detection::Sparse 1.12 2.3±0.09µs ? ?/sec 1.00 2.0±0.10µs ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10000_entities_ecs::change_detection::Sparse 1.08 297.8±12.96µs ? ?/sec 1.00 275.4±9.04µs ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_1000_entities_ecs::change_detection::Sparse 1.08 21.6±1.15µs ? ?/sec 1.00 19.9±0.51µs ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_100_entities_ecs::change_detection::Sparse 1.10 2.7±0.18µs ? ?/sec 1.00 2.4±0.05µs ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10_entities_ecs::change_detection::Sparse 1.16 398.3±11.23ns ? ?/sec 1.00 342.7±15.53ns ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10000_entities_ecs::change_detection::Sparse 1.07 51.1±2.23µs ? ?/sec 1.00 48.0±1.01µs ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10000_entities_ecs::change_detection::Table 1.09 34.2±1.57µs ? ?/sec 1.00 31.5±0.96µs ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_1000_entities_ecs::change_detection::Sparse 1.07 5.3±0.15µs ? ?/sec 1.00 5.0±0.12µs ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_100_entities_ecs::change_detection::Sparse 1.05 626.4±29.56ns ? ?/sec 1.00 594.0±6.47ns ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10_entities_ecs::change_detection::Sparse 1.05 111.9±7.19ns ? ?/sec 1.00 106.4±3.86ns ? ?/sec
none_changed_detection/50000_entities_ecs::change_detection::Sparse 1.00 47.9±0.91µs ? ?/sec 1.06 50.9±1.69µs ? ?/sec
nonempty_spawn_commands/10000_entities 1.06 577.1±114.91µs ? ?/sec 1.00 544.1±114.56µs ? ?/sec
nonempty_spawn_commands/1000_entities 1.07 53.5±8.46µs ? ?/sec 1.00 50.0±8.25µs ? ?/sec
nonempty_spawn_commands/100_entities 1.08 4.9±0.08µs ? ?/sec 1.00 4.5±0.08µs ? ?/sec
query_get_many_10/50000_calls_sparse 1.18 6.2±0.29ms ? ?/sec 1.00 5.2±0.25ms ? ?/sec
query_get_many_10/50000_calls_table 1.16 4.5±0.40ms ? ?/sec 1.00 3.8±0.20ms ? ?/sec
query_get_many_2/50000_calls_sparse 1.00 625.4±42.58µs ? ?/sec 1.09 684.4±40.72µs ? ?/sec
query_get_many_2/50000_calls_table 1.00 742.1±54.28µs ? ?/sec 1.09 809.6±50.24µs ? ?/sec
sized_commands_512_bytes/10000_commands 1.08 333.6±42.96µs ? ?/sec 1.00 308.4±28.35µs ? ?/sec
world_entity/50000_entities 1.08 110.1±0.86µs ? ?/sec 1.00 102.1±0.48µs ? ?/sec
world_query_for_each/50000_entities_table 2.02 31.1±0.49µs ? ?/sec 1.00 15.4±0.02µs ? ?/sec
Looks like mostly noise, as far as I see.
There are a couple of small things that could be improved:
- The number of arguments to
ComponentDescriptor::new_with_layoutis getting a bit too large, and now there are also incompatible combinations (mutable + fragmenting vtable). Maybe it should be replaced with a builder? - The naming is somewhat inconsistent: PR uses "fragmenting value" and "value component" somewhat inconsistently, although I don't really like either of those names. "fragmenting value component" is the most descriptive, but is way too long. Suggestions on better names are welcome.
ComponentKeyfeels a bit too generic right now. I had plans to expand it to support "key" components, which would allow to group multiple components under a single defining "key" component (like if we hadAssetId"key" component andMesh,Material, etc. components declare it as the "key", so ifAssetIdis removed or changed, all components that use it as a key would be removed/changed as well), but now I'm not so sure if it's even a good idea in the first place. In either case, maybe this detail should be hidden for now and replaced with#[component(fragmenting_value)]that implies#[component(immutable, Key=Self)]?- I think it might be possible to remove the
Clonebound onFragmentingValueifFragmentingValuesOwnedare replaced with Arc'ed version, although this would require larger refactors to bundles, which is out of scope. HavingCloneon animmutablecomponent will probably make sense most of the time anyway, so that should be fine.
I don't think anything from the above list is blocking though, so I guess this PR should be ready for review?
I've rewritten most of this PR to simplify implementation:
- Unified
DynamicFragmentingValueanddyn FragmentingValuetrait asFragmentingValue - Made
FragmentingValueVtablemore FFI/scripting-friendly - Simplified bundle insertion logic
- Made
FragmentingValuestore each value only once using anArc, which also has nice side effect of making it faster to compare values in the eventual query filtering implementation