-
Notifications
You must be signed in to change notification settings - Fork 6k
Add support for loading asset directly from ImmutableBuffer #32999
Add support for loading asset directly from ImmutableBuffer #32999
Conversation
/// | ||
/// The returned future can complete with an error if the image decoding has | ||
/// failed. | ||
Future<Codec> instantiateImageCodecFromBuffer( |
There was a problem hiding this comment.
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
lib/ui/painting.dart
Outdated
static ImmutableBuffer? fromAsset(String assetKey) { | ||
final ImmutableBuffer buffer = ImmutableBuffer._(0); | ||
bool success = false; | ||
buffer._initFromAsset(assetKey, (void result) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense!
lib/ui/painting.dart
Outdated
static ImmutableBuffer? fromAsset(String assetKey) { | ||
final ImmutableBuffer buffer = ImmutableBuffer._(0); | ||
bool success = false; | ||
buffer._initFromAsset(assetKey, (void result) { |
There was a problem hiding this comment.
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.
lib/ui/painting.dart
Outdated
static ImmutableBuffer? fromAsset(String assetKey) { | ||
final ImmutableBuffer buffer = ImmutableBuffer._(0); | ||
bool success = false; | ||
buffer._initFromAsset(assetKey, (void result) { |
There was a problem hiding this comment.
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.
lib/ui/painting.dart
Outdated
success = true; | ||
}); | ||
if (!success) { | ||
return null; |
There was a problem hiding this comment.
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.
testing/dart/assets_test.dart
Outdated
|
||
void main() { | ||
test('Loading an asset that does not exist returns null', () { | ||
expect(ui.ImmutableBuffer.fromAsset('ThisDoesNotExist'), null); |
There was a problem hiding this comment.
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.
testing/dart/assets_test.dart
Outdated
}); | ||
|
||
test('returns the bytes of a bundled asset', () { | ||
expect(ui.ImmutableBuffer.fromAsset('assets/DashInNooglerHat.jpg'), isNotNull); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
@chinmaygarde FYI - when this was added we talked about use cases like this, I think. |
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); |
There was a problem hiding this comment.
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.
updated to make the process async |
There was a problem hiding this 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.
lib/ui/painting/immutable_buffer.cc
Outdated
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) {}; |
There was a problem hiding this comment.
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
MappingReleaseProc FileMapping::GetReleaseProc() { | ||
MappingReleaseProc proc = [](const void* ptr, void* context) { | ||
auto* mapping = static_cast<fml::FileMapping*>(context); | ||
mapping->~FileMapping(); | ||
}; | ||
return proc; | ||
} | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
Updated to just use |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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::AssetResolver
s 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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.
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 |
ImmutableBuffer.fromAsset on android w/ copy: 28, 26, 26, 26, 21, 28 So even with a copy, we still improve asset load time. And on non-android platforms, its even better |
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. |
lib/ui/painting/immutable_buffer.cc
Outdated
#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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
lib/ui/painting/immutable_buffer.cc
Outdated
#if FML_OS_ANDROID | ||
auto sk_data = MakeSkDataWithCopy(bytes, size); | ||
#else | ||
auto sk_data = SkData::MakeWithCopy(bytes, size); | ||
#endif // FML_OS_ANDROID | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this 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.
Since this requires a framework change to utilize, i'll land this before I add a benchmark. |
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