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

Asset locking #15359

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

Asset locking #15359

wants to merge 16 commits into from

Conversation

andriyDev
Copy link
Contributor

@andriyDev andriyDev commented Sep 22, 2024

Objective

Solution

Testing

  • A basic test in bevy_asset.

Showcase

fn my_task_spawning_system(my_mesh_handle: Res<MyMeshHandle>, mut meshes: ResMut<Assets<Mesh>>) {
    let handle: Handle<Mesh> = my_mesh_handle.0.clone();
    let mesh_arc: Arc<Mesh> = meshes.lock(&handle);

    AsyncComputeTaskPool::get().spawn(move async || {
        // Do something with the mesh_arc.
        std::thread::sleep(std::time::Duration::from_secs(mesh_arc.count_vertices()));
        println!("Done processing!");
    });
}

The primary new parts of the API are:

  • Assets::lock - Locks the asset if it exists
  • Assets::try_unlock - Attempts to unlock the asset if there are no more outstanding handles (otherwise the asset remains locked).

Migration Guide

  • Assets::get_or_insert_with may now return an error if the asset is locked.
  • Assets::get_mut may now return an error if the asset is locked (in addition to if the asset is missing). Consider using Assets::get_mut_or_clone if your type is Clone to avoid handling locking.
  • Assets::remove and Assets::remove_untracked now return an AssetPresence<A>. This requires you to handle the case where an asset is locked.
  • Assets::iter_mut now returns an (AssetId<A>, AssetPresenceMut<'a, A>) instead of (AssetId<A>, &'a mut A). This allows you to handle the locked case.
  • ReflectAsset::get_mut and ReflectAsset::get_unchecked_mut may now return an error if the asset is locked (in addition to if the asset is missing).
  • ReflectAsset::remove now returns a ReflectedAssetPresence to store both the locked and unlocked assets.

@andriyDev andriyDev force-pushed the asset-locking branch 2 times, most recently from b6c4ed7 to 6f72525 Compare September 22, 2024 07:35
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Assets Load files from disk to use for things like images, models, and sounds X-Contentious There are nontrivial implications that should be thought through S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 22, 2024
@andriyDev andriyDev force-pushed the asset-locking branch 2 times, most recently from 4638d29 to c547158 Compare September 26, 2024 15:41
@andriyDev
Copy link
Contributor Author

Just rebased onto main.

Copy link
Contributor

@BenjaminBrienen BenjaminBrienen left a comment

Choose a reason for hiding this comment

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

Very clean code and easy to follow!

@alice-i-cecile alice-i-cecile added this to the 0.15 milestone Sep 26, 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.

The code is really well-made: commented, clearly engineered, great use of enums. Can you add an example demonstrating how (and especially why) a user might use this API?

Once that's in place, I'm on board and am willing to merge.

@cart
Copy link
Member

cart commented Sep 26, 2024

Left some thoughts in the issue: #15346 (comment)

@alice-i-cecile alice-i-cecile added X-Controversial There is active debate or serious implications around merging this PR and removed X-Contentious There are nontrivial implications that should be thought through labels Sep 26, 2024
This currently buys us nothing, but it's a nice stepping stone to making `AssetPresence` have a locked variant.
This variant is still unused.
Now assets can be "locked" into an `Arc`.
This will make it more performant to try unlocking all the locked assets in the "happy case" that few assets are locked.
This way, if you lock an asset, you just have to drop its `Arc` and wait a frame for the asset to become mutable again (as opposed to requiring a system to figure out which assets can be unlocked and trying to unlock just those).

The price is constant polling whenever an asset is locked. We have to repeatedly try unlocking an asset until it is actually ready to be unlocked.
This makes it easier to just unwrap a mutable borrow by just doing `.as_mut().unwrap()`.
…main world.

For simplicity, we just clone the assets. In theory, we could have an enum to store the RenderAssets as either a locked Arc or an Unlocked asset, but I'd rather just avoid that branch.
This is actually a totally fine case, we just need a borrow to be able to clone the internal Arc's.
@andriyDev
Copy link
Contributor Author

I rebased (#15281 had a big blast radius), and also included a new example to show off how you can use asset locking.

I still need to figure out how to implement get_mut_or_clone as mentioned in #15346. I'm running into borrow checker issues (something something polonius something). I'll take another stab at that tomorrow.

@BenjaminBrienen BenjaminBrienen added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 27, 2024
This makes working with asset locking much more palatable, as users don't need to explicitly handle the locking case in most situations.
This is likely the common case for users, so we should have our examples use this function.
@andriyDev
Copy link
Contributor Author

Ok, I've implemented get_mut_or_clone, and all the examples now use it. I've updated the migration guide to point users to get_mut_or_clone as well.

One thing that's kinda rough here is that iter_mut doesn't allow you to "clone to unlock". I'm not sure if this is worthwhile to solve, it may require a whole new struct to wrap around AssetPresenceMut, and that seems a little overcomplicated.

@BenjaminBrienen BenjaminBrienen added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Sep 27, 2024
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.

I think this is a very interesting concept to add to Assets and opens up further discussion around other systems. I've left some notes with possible future work, but I think as-is this is good to merge.

/// asset is cloned and replaces the existing asset (the previously locked asset is still
/// readable by holders of its [`Arc`]).
#[inline]
pub fn get_mut_or_clone(&mut self, id: impl Into<AssetId<A>>) -> Option<&mut A> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Future PR: a more fundamental version of the function could be

pub fn get_mut_or_replace_with(
    &mut self,
    id: impl Into<AssetId<A>>,
    f: impl FnOnce(Option<&A>) -> A
) -> Option<&mut A>;

Which would allow the user to decide how to replace a locked asset, remove the A: Clone requirement, and handling the Missing case.


/// Locks the current asset, which prevents the asset from being modified
/// until it is unlocked. Returns [`None`] only if [`AssetPresence::None`].
pub fn lock(&mut self) -> Option<Arc<A>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Future PR: since the only way to lock an Asset is via this method, using #[track_caller] and std::panic::Location::caller(), we could store a Vec<Location> in the Locked variant, allowing the try_unlock method to return an error indicating where the Asset has been locked from. I believe the #[track_caller] attribute would need to be added to all the methods that forward to lock as well (e.g., Assets::lock)

I imagine we'd want such a thing in debug builds only, since it adds more weight to the AssetPresence type, but I imagine it would be quite helpful for diagnosing possibly frustrating deadlocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an interesting idea, but it's not clear to me how big a win this is. For one, the list of lock locations can only grow (until an asset is finally unlocked). I suppose if we assume locking is infrequent and short-lived this isn't a big deal, but I can totally see an asset being locked for a long time and constantly getting more and more lock locations. For another, that only tracks the initial lock location, but you're free to clone the Arc as much as you want. I suspect the more dangerous situation is something storing the Arc without knowing it's a locked asset and blocking mutations that way (and there's no real way to solve this AFAICT).

Regardless, this definitely seems worthy of a discussion and I could be convinced!

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah it's definitely not perfect, but it could help with at least giving the user a starting point for debugging how an asset deadlock started. Potentially, we could even wrap the returned Arc to give it better debugging functionality. Storing a HashMap of Location's on the heap and counting down each location as the wrapped Arc is dropped, and counting up as the Arc is cloned.

Definitely half-baked and up for debate, and absolutely out of scope for this PR!

@bushrat011899 bushrat011899 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 Oct 2, 2024
@alice-i-cecile alice-i-cecile added S-Needs-SME Decision or review from an SME is required and removed S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it labels Oct 2, 2024
@alice-i-cecile alice-i-cecile removed this from the 0.15 milestone Oct 2, 2024
@alice-i-cecile
Copy link
Member

Going to wait on @cart to take another look at this design: he had opinions in the linked thread and I don't feel comfortable merging this without him :)

@andriyDev andriyDev mentioned this pull request Oct 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Assets Load files from disk to use for things like images, models, and sounds C-Feature A new feature, making something new possible S-Needs-SME Decision or review from an SME is required X-Controversial There is active debate or serious implications around merging this PR
Projects
Status: Respond (With Priority)
Development

Successfully merging this pull request may close these issues.

5 participants