Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Add support for loading asset directly from ImmutableBuffer #32999

Merged
merged 31 commits into from
May 11, 2022

Conversation

jonahwilliams
Copy link
Member

@jonahwilliams jonahwilliams commented Apr 29, 2022

Removes multiple copies from asset loading by directly instantiating an SkData from a mapping in the engine. This about doubles asset loading speed on a release windows desktop build. Need to test on a lower end device too. Avoids round-triping bytes through the Dart heap which should also help with GC pressure. Only useful for Image APIs though, anywhere the framework ends up reading the bytes requires a Uint8List.

This will require substantial changes to the ImageProvider APIs to take advantage of this change

Based off of previous attempt #21153

@flutter-dashboard flutter-dashboard bot added the platform-web Code specifically for the web engine label Apr 29, 2022
///
/// The returned future can complete with an error if the image decoding has
/// failed.
Future<Codec> instantiateImageCodecFromBuffer(
Copy link
Member Author

Choose a reason for hiding this comment

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

Couldn't think of what to do besides copy the API

static ImmutableBuffer? fromAsset(String assetKey) {
final ImmutableBuffer buffer = ImmutableBuffer._(0);
bool success = false;
buffer._initFromAsset(assetKey, (void result) {
Copy link
Member Author

Choose a reason for hiding this comment

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

As far as I can tell, neither this method nor fromUint8List are in any way async, and instead they fully perform all synchronous work on the event loop they are called from and only delay returning. I don't see the point in doing that

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this works.

Today asset loading happens to run on the UI thread, but I'm hesitant to write this API in such a way that boxes us into doing that or breaking people.

Because of the length field, the data is not completely opaque - so we can't really even do a trick here and say that we'll return eagerly and join a thread later when we actually need the data, because the caller might immediately query the lenght.

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right that the Uint8List one could have been sync - I think it was made async because the idea was someday we'd have things that needed to be async, and it'd be better if callsites using this already were prepared for async code than not.

Copy link
Member Author

Choose a reason for hiding this comment

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

makes sense!

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
static ImmutableBuffer? fromAsset(String assetKey) {
final ImmutableBuffer buffer = ImmutableBuffer._(0);
bool success = false;
buffer._initFromAsset(assetKey, (void result) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this works.

Today asset loading happens to run on the UI thread, but I'm hesitant to write this API in such a way that boxes us into doing that or breaking people.

Because of the length field, the data is not completely opaque - so we can't really even do a trick here and say that we'll return eagerly and join a thread later when we actually need the data, because the caller might immediately query the lenght.

static ImmutableBuffer? fromAsset(String assetKey) {
final ImmutableBuffer buffer = ImmutableBuffer._(0);
bool success = false;
buffer._initFromAsset(assetKey, (void result) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You're right that the Uint8List one could have been sync - I think it was made async because the idea was someday we'd have things that needed to be async, and it'd be better if callsites using this already were prepared for async code than not.

success = true;
});
if (!success) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should throw rather than returning null.

For example, if you try to open an asset or file that doesn't exist that should throw. In other cases like this API we do not ignore the error message sent from native.


void main() {
test('Loading an asset that does not exist returns null', () {
expect(ui.ImmutableBuffer.fromAsset('ThisDoesNotExist'), null);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above - I think this should throw.

});

test('returns the bytes of a bundled asset', () {
expect(ui.ImmutableBuffer.fromAsset('assets/DashInNooglerHat.jpg'), isNotNull);
Copy link
Contributor

Choose a reason for hiding this comment

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

consider asserting the length is right.

Copy link
Member Author

Choose a reason for hiding this comment

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

let me check what it actually is first...

@dnfield dnfield requested a review from chinmaygarde April 29, 2022 23:11
@dnfield
Copy link
Contributor

dnfield commented Apr 29, 2022

@chinmaygarde FYI - when this was added we talked about use cases like this, I think.

@jonahwilliams
Copy link
Member Author

Yeah, we can definitely make it async to give us more room in the future.

static Future<ImmutableBuffer> fromAsset(String assetKey) {
final ImmutableBuffer instance = ImmutableBuffer._(0);
return _futurize((_Callback<void> callback) {
return instance._initFromAsset(assetKey, callback);
Copy link
Member Author

Choose a reason for hiding this comment

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

_futurize handles throwing an exception if this method returns a non-null string.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
@jonahwilliams
Copy link
Member Author

updated to make the process async

@jonahwilliams jonahwilliams requested a review from dnfield April 29, 2022 23:53
Copy link
Contributor

@dnfield dnfield left a comment

Choose a reason for hiding this comment

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

This LGTM but I'd appreciate a review from Chinmay too.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
const void* bytes = static_cast<const void*>(data->GetMapping());
auto size = data->GetSize();
void* peer = reinterpret_cast<void*>(data.release());
SkData::ReleaseProc proc = [](const void* ptr, void* context) {};
Copy link
Member Author

Choose a reason for hiding this comment

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

I think for this to work correctly it needs to release the mapping somehow

fml/mapping.cc Outdated
Comment on lines 67 to 74
MappingReleaseProc FileMapping::GetReleaseProc() {
MappingReleaseProc proc = [](const void* ptr, void* context) {
auto* mapping = static_cast<fml::FileMapping*>(context);
mapping->~FileMapping();
};
return proc;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the idiom for this is ot use a NonOwnedMapping.

Directly calling a destructor is pretty weird and should be avoided.

Copy link
Contributor

Choose a reason for hiding this comment

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

In particular, NonOwnedMapping can be handed a release proc to run, and for this case we'd give it the SkData release proc.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that is backwards of what we need though - non owned mapping calls the release proc given in its destructor but the problem is that with the release required to pass the mapping to skdata, the destructor won't get called - right?

@jonahwilliams
Copy link
Member Author

Updated to just use delete

Copy link
Member

@chinmaygarde chinmaygarde left a comment

Choose a reason for hiding this comment

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

Other than the one question about thread safety of the asset manager mappings, lgtm.

->platform_configuration()
->client()
->GetAssetManager();
std::unique_ptr<fml::Mapping> data = asset_manager->GetAsMapping(asset_name);
Copy link
Member

Choose a reason for hiding this comment

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

Are these mappings thread safe? These usually come from flutter::AssetResolvers and looking at the inheritance diagram from these, flutter::APKAssetProvider seems to be a subclass. Its assets are backed by AAsset handles whose thread safety notes say "AAsset objects are NOT thread-safe, and should not be shared across threads.". Since there used to be a copy earlier, it sort of didn't matter. But now that these are referenced across thread (for texture decompression), there may be threading violations that we have perhaps not contended with.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm. Sounds like we can't avoid the copy in that case.

If we didn't expose the lenght, we could get away with this being something that's done entirely on the decoder thread though right? We could just give that a way to acquire the data and then let it do the whole thing there.

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't need the length in the framework for this case. We could always say "length might be -1 for certain instances not created from a uint8list"

Copy link
Member Author

Choose a reason for hiding this comment

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

we could also copy once into an skdata - that would still reduce the number of copies by 1. Though having a single createCodecFromAsset method would mean we could do the entire thing on the right thread, i'm not sure how well that works with the framework wanting to abstract byte acquisition from image decoding.

I don't think adding a special case for asset images would be a bad thing fwiw, but it would be a special case.

@jonahwilliams
Copy link
Member Author

Looked at this some more:

We'd have to move the load method all the way back to the Codec decode method in order to avoid needing to hold an SkData. I'm a bit concerned that this won't really fit into the image provider/asset bundle logic we support today. I think next week I'll compare the performance of ImmutableBuffer.fromAsset with a single copy to see what the cost is

@jonahwilliams
Copy link
Member Author

ImmutableBuffer.fromAsset on android w/ copy: 28, 26, 26, 26, 21, 28
rootBundle.load + ImmutableBuffer.fromUint8List: 46, 46, 41, 43, 46,

So even with a copy, we still improve asset load time. And on non-android platforms, its even better

@jonahwilliams jonahwilliams requested a review from dnfield May 10, 2022 21:48
@jonahwilliams
Copy link
Member Author

Also: this avoids round-tripping the bytes through the dart heap, which is beneficial from a GC perspective. Potentially more so than avoiding a copy.

Comment on lines 90 to 99
#if FML_OS_ANDROID
// fml::Mapping backed by android assets are not thread safe.
auto sk_data = MakeSkDataWithCopy(bytes, size);
#else
SkData::ReleaseProc proc = [](const void* ptr, void* context) {
delete static_cast<fml::Mapping*>(context);
};
void* peer = reinterpret_cast<void*>(data.release());
auto sk_data = SkData::MakeWithProc(bytes, size, proc, peer);
#endif // FML_OS_ANDROID
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we can assume this is thread safe on any platform - we should just default to making copies everywhere, since the fml::Mapping interface doesn't declare itself as thread safe.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

Comment on lines 90 to 95
#if FML_OS_ANDROID
auto sk_data = MakeSkDataWithCopy(bytes, size);
#else
auto sk_data = SkData::MakeWithCopy(bytes, size);
#endif // FML_OS_ANDROID

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think you can just call MakeSkDataWithCopy since it's already ifdefed like this below anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

oh woops, misread that - thanks!

Copy link
Contributor

@dnfield dnfield left a comment

Choose a reason for hiding this comment

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

LGTM with nit. Would appreciate LGTM from @chinmaygarde as well.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
@jonahwilliams
Copy link
Member Author

Since this requires a framework change to utilize, i'll land this before I add a benchmark.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
affects: engine affects: framework platform-web Code specifically for the web engine waiting for tree to go green This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants