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

Optimize common usages of AssetReader #14082

Merged
merged 30 commits into from
Jul 1, 2024
Merged

Conversation

JoJoJet
Copy link
Member

@JoJoJet JoJoJet commented Jun 30, 2024

Objective

The AssetReader trait allows customizing the behavior of fetching bytes for an AssetPath, and expects implementors to return dyn AsyncRead + AsyncSeek. This gives implementors of AssetLoader great flexibility to tightly integrate their asset loading behavior with the asynchronous task system.

However, almost all implementors of AssetLoader don't use the async functionality at all, and just call AsyncReadExt::read_to_end(&mut Vec<u8>). This is incredibly inefficient, as this method repeatedly calls poll_read on the trait object, filling the vector 32 bytes at a time. At my work we have assets that are hundreds of megabytes which makes this a meaningful overhead.

Solution

Turn the Reader type alias into an actual trait, with a provided method read_to_end. This provided method should be more efficient than the existing extension method, as the compiler will know the underlying type of Reader when generating this function, which removes the repeated dynamic dispatches and allows the compiler to make further optimizations after inlining. Individual implementors are able to override the provided implementation -- for simple asset readers that just copy bytes from one buffer to another, this allows removing a large amount of overhead from the provided implementation.

Now that Reader is an actual trait, I also improved the ergonomics for implementing AssetReader. Currently, implementors are expected to box their reader and return it as a trait object, which adds unnecessary boilerplate to implementations. This PR changes that trait method to return a pseudo trait alias, which allows implementors to return impl Reader instead of Box<dyn Reader>. Now, the boilerplate for boxing occurs in ErasedAssetReader.

Testing

I made identical changes to my company's fork of bevy. Our app, which makes heavy use of read_to_end for asset loading, still worked properly after this. I am not aware if we have a more systematic way of testing asset loading for correctness.


Migration Guide

The trait method bevy_asset::io::AssetReader::read (and read_meta) now return an opaque type instead of a boxed trait object. Implementors of these methods should change the type signatures appropriately

impl AssetReader for MyReader {
    // Before
    async fn read<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
        let reader = // construct a reader
        Box::new(reader) as Box<Reader<'a>>
    }

    // After
    async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
        // create a reader
    }
}

bevy::asset::io::Reader is now a trait, rather than a type alias for a trait object. Implementors of AssetLoader::load will need to adjust the method signature accordingly

impl AssetLoader for MyLoader {
    async fn load<'a>(
        &'a self,
        // Before:
        reader: &'a mut bevy::asset::io::Reader,
        // After:
        reader: &'a mut dyn bevy::asset::io::Reader,
        _: &'a Self::Settings,
        load_context: &'a mut LoadContext<'_>,
    ) -> Result<Self::Asset, Self::Error> {
}

Additionally, implementors of AssetReader that return a type implementing futures_io::AsyncRead and AsyncSeek might need to explicitly implement bevy::asset::io::Reader for that type.

impl bevy::asset::io::Reader for MyAsyncReadAndSeek {}

@alice-i-cecile alice-i-cecile added A-Assets Load files from disk to use for things like images, models, and sounds C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jun 30, 2024
@alice-i-cecile alice-i-cecile added this to the 0.15 milestone Jun 30, 2024
@JoJoJet JoJoJet added the C-Performance A change motivated by improving speed, memory usage or compile times label Jun 30, 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.

Nice to see this getting stress-tested and polished :) Thanks!

Minor nits about docs.

@cart cart added this pull request to the merge queue Jul 1, 2024
Merged via the queue into bevyengine:main with commit 5876352 Jul 1, 2024
31 checks passed
@JoJoJet JoJoJet deleted the optimize-asset-read branch July 1, 2024 23:45
zmbush pushed a commit to zmbush/bevy that referenced this pull request Jul 3, 2024
# Objective

The `AssetReader` trait allows customizing the behavior of fetching
bytes for an `AssetPath`, and expects implementors to return `dyn
AsyncRead + AsyncSeek`. This gives implementors of `AssetLoader` great
flexibility to tightly integrate their asset loading behavior with the
asynchronous task system.

However, almost all implementors of `AssetLoader` don't use the async
functionality at all, and just call `AsyncReadExt::read_to_end(&mut
Vec<u8>)`. This is incredibly inefficient, as this method repeatedly
calls `poll_read` on the trait object, filling the vector 32 bytes at a
time. At my work we have assets that are hundreds of megabytes which
makes this a meaningful overhead.

## Solution

Turn the `Reader` type alias into an actual trait, with a provided
method `read_to_end`. This provided method should be more efficient than
the existing extension method, as the compiler will know the underlying
type of `Reader` when generating this function, which removes the
repeated dynamic dispatches and allows the compiler to make further
optimizations after inlining. Individual implementors are able to
override the provided implementation -- for simple asset readers that
just copy bytes from one buffer to another, this allows removing a large
amount of overhead from the provided implementation.

Now that `Reader` is an actual trait, I also improved the ergonomics for
implementing `AssetReader`. Currently, implementors are expected to box
their reader and return it as a trait object, which adds unnecessary
boilerplate to implementations. This PR changes that trait method to
return a pseudo trait alias, which allows implementors to return `impl
Reader` instead of `Box<dyn Reader>`. Now, the boilerplate for boxing
occurs in `ErasedAssetReader`.

## Testing

I made identical changes to my company's fork of bevy. Our app, which
makes heavy use of `read_to_end` for asset loading, still worked
properly after this. I am not aware if we have a more systematic way of
testing asset loading for correctness.

---

## Migration Guide

The trait method `bevy_asset::io::AssetReader::read` (and `read_meta`)
now return an opaque type instead of a boxed trait object. Implementors
of these methods should change the type signatures appropriately

```rust
impl AssetReader for MyReader {
    // Before
    async fn read<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
        let reader = // construct a reader
        Box::new(reader) as Box<Reader<'a>>
    }

    // After
    async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
        // create a reader
    }
}
```

`bevy::asset::io::Reader` is now a trait, rather than a type alias for a
trait object. Implementors of `AssetLoader::load` will need to adjust
the method signature accordingly

```rust
impl AssetLoader for MyLoader {
    async fn load<'a>(
        &'a self,
        // Before:
        reader: &'a mut bevy::asset::io::Reader,
        // After:
        reader: &'a mut dyn bevy::asset::io::Reader,
        _: &'a Self::Settings,
        load_context: &'a mut LoadContext<'_>,
    ) -> Result<Self::Asset, Self::Error> {
}
```

Additionally, implementors of `AssetReader` that return a type
implementing `futures_io::AsyncRead` and `AsyncSeek` might need to
explicitly implement `bevy::asset::io::Reader` for that type.

```rust
impl bevy::asset::io::Reader for MyAsyncReadAndSeek {}
```
MrGVSV pushed a commit to MrGVSV/bevy that referenced this pull request Jul 5, 2024
# Objective

The `AssetReader` trait allows customizing the behavior of fetching
bytes for an `AssetPath`, and expects implementors to return `dyn
AsyncRead + AsyncSeek`. This gives implementors of `AssetLoader` great
flexibility to tightly integrate their asset loading behavior with the
asynchronous task system.

However, almost all implementors of `AssetLoader` don't use the async
functionality at all, and just call `AsyncReadExt::read_to_end(&mut
Vec<u8>)`. This is incredibly inefficient, as this method repeatedly
calls `poll_read` on the trait object, filling the vector 32 bytes at a
time. At my work we have assets that are hundreds of megabytes which
makes this a meaningful overhead.

## Solution

Turn the `Reader` type alias into an actual trait, with a provided
method `read_to_end`. This provided method should be more efficient than
the existing extension method, as the compiler will know the underlying
type of `Reader` when generating this function, which removes the
repeated dynamic dispatches and allows the compiler to make further
optimizations after inlining. Individual implementors are able to
override the provided implementation -- for simple asset readers that
just copy bytes from one buffer to another, this allows removing a large
amount of overhead from the provided implementation.

Now that `Reader` is an actual trait, I also improved the ergonomics for
implementing `AssetReader`. Currently, implementors are expected to box
their reader and return it as a trait object, which adds unnecessary
boilerplate to implementations. This PR changes that trait method to
return a pseudo trait alias, which allows implementors to return `impl
Reader` instead of `Box<dyn Reader>`. Now, the boilerplate for boxing
occurs in `ErasedAssetReader`.

## Testing

I made identical changes to my company's fork of bevy. Our app, which
makes heavy use of `read_to_end` for asset loading, still worked
properly after this. I am not aware if we have a more systematic way of
testing asset loading for correctness.

---

## Migration Guide

The trait method `bevy_asset::io::AssetReader::read` (and `read_meta`)
now return an opaque type instead of a boxed trait object. Implementors
of these methods should change the type signatures appropriately

```rust
impl AssetReader for MyReader {
    // Before
    async fn read<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
        let reader = // construct a reader
        Box::new(reader) as Box<Reader<'a>>
    }

    // After
    async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
        // create a reader
    }
}
```

`bevy::asset::io::Reader` is now a trait, rather than a type alias for a
trait object. Implementors of `AssetLoader::load` will need to adjust
the method signature accordingly

```rust
impl AssetLoader for MyLoader {
    async fn load<'a>(
        &'a self,
        // Before:
        reader: &'a mut bevy::asset::io::Reader,
        // After:
        reader: &'a mut dyn bevy::asset::io::Reader,
        _: &'a Self::Settings,
        load_context: &'a mut LoadContext<'_>,
    ) -> Result<Self::Asset, Self::Error> {
}
```

Additionally, implementors of `AssetReader` that return a type
implementing `futures_io::AsyncRead` and `AsyncSeek` might need to
explicitly implement `bevy::asset::io::Reader` for that type.

```rust
impl bevy::asset::io::Reader for MyAsyncReadAndSeek {}
```
@UkoeHB
Copy link
Contributor

UkoeHB commented Jul 10, 2024

EDIT: Solved this by removing <'_> from the reader. It should be mentioned in the migration guide.

This PR messed up my (very basic) asset loaders:

return type references an anonymous lifetime, which is not constrained by the fn input types
lifetimes appearing in an associated or opaque type are not considered constrained
consider introducing a named lifetime parameter

It's claiming the '_ lifetime in Reader<'_> is being captured. My loader does call reader.read_to_end(...).await?; so presumably the lifetime is now leaking into the future.

Example:

struct CobwebAssetLoader;

impl AssetLoader for CobwebAssetLoader
{
    type Asset = CobwebAssetFile;
    type Settings = ();
    type Error = CobwebAssetLoaderError;

    async fn load<'a>(
        &'a self,
        reader: &'a mut dyn Reader<'_>,
        _settings: &'a (),
        load_context: &'a mut LoadContext<'_>,
    ) -> Result<Self::Asset, Self::Error>
    {
        let mut bytes = Vec::new();
        reader.read_to_end(&mut bytes).await?;
        let data: serde_json::Value = from_slice(&bytes)?;
        Ok(CobwebAssetFile {
            file: LoadableFile::new(&load_context.asset_path().path().to_string_lossy()),
            data,
        })
    }

    fn extensions(&self) -> &[&str]
    {
        &[".caf.json"]
    }
}

@JoJoJet
Copy link
Member Author

JoJoJet commented Jul 10, 2024

Seems like a misleading error message for what should be a simple fix (just remove the explicit elided lifetime). Do you mind making a rustc issue?

@UkoeHB
Copy link
Contributor

UkoeHB commented Jul 10, 2024

Ok, rust-lang/rust#127585

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-Performance A change motivated by improving speed, memory usage or compile times C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide 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