Make WorldQuery use &World for initialization#22670
Make WorldQuery use &World for initialization#22670ElliottjPierce wants to merge 21 commits intobevyengine:mainfrom
WorldQuery use &World for initialization#22670Conversation
This required a little bundle work too.
|
Am I right that queueing the reservations is going to be slower? Do we have any idea how much slower this is for initializing the state? |
Regardless of using queued vs un-queued registration, it first looks to see if the component is already registered. In the un-queued case, if not, it starts the whole registration process, which could take a while. In the queued case, it acquires a lock the the queue, reserves an id, and drops the lock. The registration happens after in So, I think the only way this could have perf problems is if a ton of query states are made about components that are not registered, and nothing flushes the world, spawns the component, or does anything in between to register it. And even if all that does happen, the only real perf cost is getting a |
|
When it comes to |
| /// | ||
| /// - Asset changes are registered in the [`AssetEventSystems`] system set. | ||
| /// - Removed assets are not detected. | ||
| /// - The asset must be initialized ([`App::init_asset`](crate::AssetApp::init_asset)). |
There was a problem hiding this comment.
This is a totally fine limitation: this filter is useless without initialized assets.
|
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. |
|
The ability to do There's also some docs and deprecation work to do, but please ping me for a re-review when those comments are addressed. |
|
I have two concerns:
My suggestion would be: keep |
This is a sensible concern and should be discussed / debated. Tests to verify that this works during multithreaded operation would be appreciated at the least.
I am completely fine with this as a breaking change given Bevy's level of stability. The migration is very easy, and that's what migration guides are for. |
What I was mentioning is that In fact, from my parallel computing experience, implicit locks and sync points are not desired in the base framework. The API In addition, the query may return If everyone is comfortable with this, then I consider the PR good to go. |
You're absolutely right about the compiler here, but I'm not sure what kind of warning would be appropriate. The only time this locks is the first time a world sees a component type. There's no locking during flushing or anything, and most queries would not lock besides those made during startup. This shouldn't cause any non-determinism because component registration is order independent. I do get your concern though. Choosing which API is best is not up to me, but I think this is a win for most users. If anyone is worried about the order their components are registered in, they are probably registering them manually already.
Maybe I'm not understanding, but there is no way to get mutable world data from |
I have forgotten if we could query |
I think the issue is that this is exactly what happens when initializing the schedule for the first time! Components that get spawned by the startup schedule will be registered on spawn, but most other components are registered for the first time when we initialize a system that has a |
chescock
left a comment
There was a problem hiding this comment.
Yay, I'm glad this is happening!
I think someone needs to check the performance implications, both of always initializing AssetChanges<A> and of the extra atomic RwLocks during system initialization. But I don't feel qualified to know what's acceptable there, so I get to click Approve :).
| )); | ||
| } | ||
| self.insert_resource(assets) | ||
| .init_resource::<AssetChanges<A>>() |
There was a problem hiding this comment.
I think the reason this used to be initialized only in the AssetChanged filter is to avoid the overhead of updating the resource if nothing was using it. There's an asset_events system that takes Option<ResMut<AssetChanges<A>>> and only updates it if the resource exists:
bevy/crates/bevy_asset/src/assets.rs
Line 594 in 78166fb
I don't know enough about assets to evaluate how important that is, though.
If we do need to preserve that behavior, I think there are still ways to do it with only &World. Maybe a resource with a ConcurrentQueue<fn(&mut World)>? AssetChanged::init_state would push |world| world.init_resource::<AssetChanges<A>>() and an exclusive system that runs before(AssetEventSystems) would drain the queue and run the function pointers? AssetChanged filters are rare enough that the extra atomics during initialization shouldn't be too expensive.
There was a problem hiding this comment.
Yeah, it's not ideal. I'd like more information about the perf implications here.
There was a problem hiding this comment.
I don't have a strong opinion here. It's quite unfortunate to have this running for every asset type though - some asset types may mutate frequently (e.g., Materials) but those asset types are also most likely to actually take advantage of this feature.
I wouldn't block on preserving this unless folks complain (which seems unlikely). With assets-as-entities unblocked, we may also throw out this type entirely, so putting the work in to fix it seems a little early.
| let event_key = world.register_event_key::<E>(); | ||
| let components = B::component_ids(&mut world.components_registrator()); | ||
| let components = B::component_ids(&mut world.components_registrator()) | ||
| .collect::<smallvec::SmallVec<[ComponentId; 16]>>(); |
There was a problem hiding this comment.
If I understand correctly, this collect is because component_ids captures the lifetime of the ComponentsRegistrator type now that it's a generic parameter, which means this now conflicts with the world borrow in world.get_mut::<Observer>. And the compiler forces it to capture that lifetime, even though the return types are always actually 'static and the use<> syntax looks like it would support leaving some parameters out.
But this only actually allocates when creating an observer with more than 16 components, which should basically never happen and which is already allocating to box the system, so the cost is pretty low.
There was a problem hiding this comment.
Exactly. We could monomorphize component_ids ourselves to fix this, but that didn't feel worth it. And this should be fixable once we can specify the use<> bounds. I also tried adding a 'static bound, but that didn't work either.
|
@andriyDev, can I have your opinions here on the asset changes? |
Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
|
I think at this point we only have three unresolved questions: Performance impact of queued component registrationFor components that have not been registered, this adds a Is this area's performance important? If so, I'll add some benches for it. (Right now, nothing benches app startup costs like registering systems.) We can also consider Does app startup performance matter enough to create benchmarks for it? Performance impact of
|
For Bevy, IMO no. At least not unless we're talking about seconds. I also expect that windowing and rendering costs are going to absolutely dwarf micro-optimizations inside the ECS.
My feeling is that this is in the noise. Virtually every asset type should be using AssetChanged, although not all of them currently are, leading to subtle bugs when they're updated.
We should avoid adding complexity unless we have concrete evidence that this is a problem we need to solve. Even then, we should probably split that into a separate PR.
We could open a draft PR for the crate, targeting this branch, and see if we can get it to compile? |
|
I haven’t been deeply involved in this work, but I’ve caught up on the related discussion and reviewed the implementation here. From an implementation standpoint, this looks solid. That said, I want to echo some of the concerns raised elsewhere. Using an internal RwLock to bypass the &World / &mut World distinction makes me uneasy—not so much for performance reasons (this likely only runs during startup for most SystemParams), but because it adds complexity and pessimizes the common case in order to relax an exclusivity boundary. I’m also not yet convinced of the utility this provides. Requiring In cases where this comes up today, I've found it acceptable to initialize a query state via I don’t have a strong objection to this change, but I'm not convinced that the benefits outweigh the added complexity and the shift it may encourage in usage patterns. |
Objective
Works towards #18276.
This does not do system initialization with
&World.This makes
WorldQuery::init_stateonly take&World, and by extensionQueryState::new, etc.Solution
Bundle::component_idsalso work with queued component registration.WorldQuery::init_statefrom&mut Worldto&World.AssetChangesresource ininit_assetinstead ofAssetChanged::init_state.Testing