-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Synchronized component registration #17569
base: main
Are you sure you want to change the base?
Synchronized component registration #17569
Conversation
I realized the new components would need to know about previously registered components, so I simplified.
I deleted a lot of mostly unused utility functions too since it's so painful to implement until https://doc.rust-lang.org/std/sync/struct.MappedRwLockReadGuard.html is stabalized.
Added back in the mutable versions. Keep lock during frequent registrations.
Before, required components could be read directly from ComponentInfo. But sicne there could be staged changes to required components, this could be wrong. Hence, that interface was removed, and now, required components can be requested directly.
The new ones were cloned from the original so merging is not needed.
these were mostly originally inlined, but it was dropped along the way.
CI should fail right now since we still need to create ComponentSparseSets. Right now we aren't doing that, so the dead code causes a failure. The unwrap I mentioned in sparse set queries may also be causing crashes in examples. |
This allows for faster registration with Components when you already have a mutable reference. It also makes Components more generic.
The work I just finished doing should ensure there is no performance regression. When registering with mutable access to Components, it checks if there's any staged changes, and if not, it uses the previous implementation. The only costs would be if we use the synchronous registration methods or if we are getting data for a type that was recently registered. I think we can keep this to a minimum by only using the synchronous methods when we need to (ie. read-only queries.) Then again, with a bit more work, it might be fast enough to just use the synchronized methods out of convenience. I think the next step is to benchmark. |
I forgot to clean the components after registering.
I just added some basic benchmarks, and it looks good. Really good.
To me, these benchmarks indicate that when I adapted the previous implementation to work in ComponentsMut, I may have written if to run faster than before. Raw is slower than Mut and Full is slower than Full_Staged which seem to support this. In a future PR, it may be worth trying to improve the performance of the old registration. What the benchmarks show is that this optional synchronization is effectively free and that it can be made even faster. Also note that these benchmarks are in the worst possible order. They ones with no required components are registered before the ones that require them. If the order was reversed, there would be even less locking, and I suspect it would run faster. |
they are now additional instead of replacements
At this point, the PR is feature complete IMO. BenchesHere are the current benches for my M2 Max: Needs Careful ReviewI'm still relatively new to the guts of bevy's ecs, to it is definitely possible that I made a logic mistake somewhere. I would especially appreciate feedback on the following:
Future work
|
I figured this was a more precise name.
Objective
Registering components currently requires mutable access to a world, but conceptually, registering a component doesn't change the world's state. As discussed on discord, we want to fix that discrepancy by allowing component registration with only a read reference to the world.
If that can be done, this will open up the ability to create read only queries and could be the basis of resolving similar battles with the borrow checker.
Solution
Other solutions have been brought up in discord, but this PR attempts to solve the problem by synchronizing component registration itself.
Components
now storesComponentsData
andStagedComponents
.ComponentsData
is effectively the previous components implementation. When a component is registered or required components are set, we lock the staged components, queue those modifications on the stage, and release the lock. Eventually (during SubApp::update for now), we apply those queued changes to the normalComponentsData
, clearing the staged changes. When we are just reading data, including registering an already registered component, the lock is only engaged if the requested data still lives in the staged changes. Effectively, the lock is almost never hit unless there is some registration process happening.For performance, the original Components implementation has been kept, and both the new and the old are available via an abstraction trait,
ComponentsView
. When registering in bulk, the previous implementation may be slightly faster, and it's pretty simple to use that one instead. Additionally, when a lock needs to be used for registration, it uses a newComponentsLock
. That lock is kept between nearby registrations by default, so there is minimal locking overhead.There is some duplicated code between different implementations of
ComponentsView
. We could abstract it away, but I wanted to leave it for now since we may choose to let the implementations diverge.Testing
Current tests pass for me, but no new tests were created.
We will probably want to benchmark this vs the old implementation, but I wanted to leave that up to someone who knows better exactly what situations to benchmark.
Pros
Cons
Migration Guide
To make catching bugs and butterfly effects easier, I went ahead and adapted multiple signatures. For example, many signatures changed from &mut to &. Also,
Component::register_required_components
now accepts aComponentsView
instead ofComponents
. We can spread some of these changes over multiple PRs. It was just easier for me to follow it with the changes.Previously, when a sparse set component was registered, its set was created. Now, the set is created when the component is inserted (or spawned) in an entity. I did this during
BundleInfo creation
(Maybe there's a better place). At any rate, lots of code depended on registered components having valid sets. Now the rule is spawned components have valid sets. I updated relevant usages and safety comments, but more testing should probably be done. For now, it passes my "smell test."