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

Generalized and atomic staging utils #17701

Open
wants to merge 45 commits into
base: main
Choose a base branch
from

Conversation

ElliottjPierce
Copy link
Contributor

Objective

The goal here is to provide a general alternative to a RwLock for rarely changed collections. When a change is made, it stages it in temporary storage without actually modifying the data. Then, at user defined points, the changes are applied to the data, draining the staged changes.

This has come up a lot regarding components, ex: #17569, but the same treatment has been discussed for reflection and other areas where a type has to be registered once and only once, on demand. This allows that functionality to be put on separate threads, which could unblock a lot of additional ideas, like assets as entities and read-only queries.

Further, it may be useful to put these "StageOnWrite" data structures in an Arc, so that has been implemented too. This could unlock benefits like not having to translate component ids when spawning scenes or cloning entities, but more on that in future PRs and discussions.

There's a lot of things this PR benefits but it should be evaluated as a stand-alone addition for these utilities. Still, those goals and use cases did influence the design, so keep that in mind.

Solution

  • Implement a generic StagedChanges trait that tracks changes to a "target" data structure, Cold.
  • Implement types that track these staged changes, StageOnWrite and AtomicStageOnWrite.
  • Implement both locking and unlocked ways of accessing the staged and un-staged data together, mutably and immutably.

Testing

I did create a test module with an example, but the potential for bugs here is mostly in systems using this feature, so I kept it kind of minimal for now.

@bushrat011899
Copy link
Contributor

The no_std CI failure can be resolved by feature-gating your new types behind feature = "alloc"

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.

Can we put this in bevy_platform_support? I think it's likely more fitting there, and I'm nervous about adding anything new to bevy_utils, since we're trying to eliminate it.

@ElliottjPierce
Copy link
Contributor Author

Can we put this in bevy_platform_support? I think it's likely more fitting there, and I'm nervous about adding anything new to bevy_utils, since we're trying to eliminate it.

Ah. I wasn't aware of that. I can absolutely move it.

@bushrat011899
Copy link
Contributor

I'd personally prefer if it stayed in bevy_utils until it could get yeeted into it's own micro crate. The goal for bevy_platform_support is to just get std-like functionality for all platforms and I'd like to keep its scope narrowed to that, otherwise I see it just being more bevy_utils. But I'm willing to be overruled on that front if we consider this something that should be in the standard library.

Hopefully this also fixes no_std
Copy link
Contributor

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

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

This should fix the alloc issue you were having before and also allow using most of the functionality here even without alloc enabled.

crates/bevy_utils/src/staging.rs Outdated Show resolved Hide resolved
crates/bevy_utils/src/staging.rs Outdated Show resolved Hide resolved
crates/bevy_utils/src/staging.rs Outdated Show resolved Hide resolved
crates/bevy_utils/src/staging.rs Show resolved Hide resolved
crates/bevy_utils/src/staging.rs Outdated Show resolved Hide resolved
crates/bevy_platform_support/src/staging.rs Outdated Show resolved Hide resolved
crates/bevy_platform_support/src/staging.rs Outdated Show resolved Hide resolved
crates/bevy_platform_support/src/staging.rs Outdated Show resolved Hide resolved
crates/bevy_platform_support/src/staging.rs Outdated Show resolved Hide resolved
crates/bevy_platform_support/src/staging.rs Outdated Show resolved Hide resolved
@bushrat011899 bushrat011899 added D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 6, 2025
ElliottjPierce and others added 5 commits February 6, 2025 11:07
Mostly optimizing docs for better diff views in the future. Great idea.

Co-authored-by: Zachary Harrold <[email protected]>
This opens up the possibility that invalid data is created. For example, if a thread crashes while actively staging a change, there might be an incomplete staged change. However, in the context that this will be used, that seems unlikely, and this behavior gives more flexibility, etc.
@NthTensor
Copy link
Contributor

NthTensor commented Feb 6, 2025

I'll defer to @alice-i-cecile as to judging whether this is properly motivated, since I don't feel I have the full context here. Some thoughts, having skimmed but not really reviewed:

  • It would be nice to see performance comparison between this and RwLock. I'm generally hesitant to merge ostensible optimizations without seeing numbers.
  • There's existing crates that do this. How does this differ from, for example, left_right?

@ElliottjPierce
Copy link
Contributor Author

I'll defer to @alice-i-cecile as to judging whether this is properly motivated.

Agreed. This is definitely tricky to determine. The end goal is to enable &Components to be effectively passed around freely, with full rights to register components etc. This would have lots of exciting implications, some of which have been discussed elsewhere. This staging idea is the best architecture for that I could come up with, but if there is a better one, I'm all for it.

It would be nice to see performance comparison between this and RwLock. I'm generally hesitant to merge ostensible optimizations without seeing numbers.

Totally agree here too. The simplicity of RwLock is appealing. I haven't bothered to test it yet because when this idea was first discussed on discord Cart was pretty sure we should avoid solutions where registering a new component would block old components from being read. RwLock<Components> would have that problem. Maybe that simplicity would still be worth it, but his points steered me away from that for now.

There's existing crates that do this. How does this differ from, for example, left_right?

I wish left_right would work for this too. It was one of the first things I thought about, but it's just barely not enough for what we need (I think). It only allows one writer at a time. We could just through the writer in an Arc<RwLock<Writer>>, but there are other problems. For example, what if a threads want to register the a component and then read its ComponentInfo? Well, the info will only exist on the writer, so the thread will have to use only the writer instead of a reader. This could be fixed by pushing the changes on each registration, buy know you're back to effectively a RwLock.

left_right could be incorporated into a direct solution, but it would need a lot of wrappers to make it safe, and the synchronizing in that might be even slower than RwLock. Besides all, it wouldn't work for no_std. I'm no expert on rust's third party synchronizing crates, but the ones I researched ahead of time, like left_right, don't feel promising to me. I could always be wrong.

That's my thoughts at least. Believe me, I wouldn't be making this PR if I saw an easier way, but I don't. But if anyone else wants to save the day here, I'm all ears.

Copy link
Contributor

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

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

Minor fixes for compile-check-no-std

crates/bevy_utils/src/staging.rs Show resolved Hide resolved
crates/bevy_utils/src/staging.rs Outdated Show resolved Hide resolved
@bushrat011899
Copy link
Contributor

I wish left_right would work for this too. It was one of the first things I thought about, but it's just barely not enough for what we need (I think). It only allows one writer at a time. We could just through the writer in an Arc<RwLock<Writer>>, but there are other problems. For example, what if a threads want to register the a component and then read its ComponentInfo? Well, the info will only exist on the writer, so the thread will have to use only the writer instead of a reader. This could be fixed by pushing the changes on each registration, buy know you're back to effectively a RwLock.

left_right could be incorporated into a direct solution, but it would need a lot of wrappers to make it safe, and the synchronizing in that might be even slower than RwLock. Besides all, it wouldn't work for no_std. I'm no expert on rust's third party synchronizing crates, but the ones I researched ahead of time, like left_right, don't feel promising to me. I could always be wrong.

Definitely worth documenting this thought process either in the PR description or even the documentation on the module itself. Knowing exactly what niche this is trying to fill helps justify its existence, and will inform future maintainers about why certain tradeoffs have been made.

@ElliottjPierce
Copy link
Contributor Author

There's a few things I'm trying to clean up at the moment, and then I'll go hands off for a bit. Making the example showed me some areas to improve.

@NthTensor
Copy link
Contributor

Great, thanks for the thorough response. Seems like we should def proceed with this then.

@ElliottjPierce
Copy link
Contributor Author

@bushrat011899 I'm satisfied with where it's at at the moment. You had a lot of great suggestions in the last round. Sorry it took so long to do them. Looking forward to more!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Utils Utility functions and types C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants