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

[dart:ui] add an explicit API for loading assets #21153

Closed

Conversation

jonahwilliams
Copy link
Member

@jonahwilliams jonahwilliams commented Sep 14, 2020

Description

create an API which returns an asset ByteData synchronously given the asset key (usually a file path). Improves asset loading time by 5-10x.

This still requires the data to be copied from the mapping, since the Dart VM does not accept read-only extenal typed data. Its unclear whether the ByteData return type is idea either, perhaps it should be a Uint8List or similar.

Example loading times (microseconds)

Built in release mode using the flutter gallery on a pixel4

channel-based

AssetManifest.json took 1566
packages/flutter_gallery_assets/logos/flutter_white/2.5x/logo.png took 411
packages/shrine_images/3.0x/slanted_menu.png took 23857
packages/shrine_images/3.0x/diamond.png took 25442
packages/shrine_images/3.0x/1-0.jpg took 15292
packages/shrine_images/3.0x/0-0.jpg took 13669
packages/shrine_images/3.0x/2-0.jpg took 42750
packages/shrine_images/3.0x/4-0.jpg took 55717
packages/shrine_images/3.0x/3-0.jpg took 54790
packages/shrine_images/3.0x/5-0.jpg took 1367
packages/shrine_images/3.0x/7-0.jpg took 2141
packages/shrine_images/3.0x/6-0.jpg took 1500
packages/shrine_images/3.0x/8-0.jpg took 2905
packages/shrine_images/3.0x/10-0.jpg took 4836
packages/shrine_images/3.0x/9-0.jpg took 4171
packages/shrine_images/3.0x/11-0.jpg took 5339
packages/shrine_images/3.0x/13-0.jpg took 4269
packages/shrine_images/3.0x/12-0.jpg took 3598
packages/shrine_images/3.0x/14-0.jpg took 1592
packages/shrine_images/3.0x/16-0.jpg took 3197
packages/shrine_images/3.0x/15-0.jpg took 2446
packages/shrine_images/3.0x/17-0.jpg took 1367

loadAsset based

AssetManifest.json took 175
packages/flutter_gallery_assets/logos/flutter_white/2.5x/logo.png took 92
packages/shrine_images/3.0x/slanted_menu.png took 102
packages/shrine_images/3.0x/diamond.png took 58
packages/shrine_images/3.0x/1-0.jpg took 200
packages/shrine_images/3.0x/0-0.jpg took 284
packages/shrine_images/3.0x/2-0.jpg took 233
packages/shrine_images/3.0x/4-0.jpg took 209
packages/shrine_images/3.0x/3-0.jpg took 170
packages/shrine_images/3.0x/5-0.jpg took 191
packages/shrine_images/3.0x/7-0.jpg took 189
packages/shrine_images/3.0x/6-0.jpg took 162
packages/shrine_images/3.0x/8-0.jpg took 239
packages/shrine_images/3.0x/10-0.jpg took 274
packages/shrine_images/3.0x/9-0.jpg took 206
packages/shrine_images/3.0x/11-0.jpg took 323
packages/shrine_images/3.0x/13-0.jpg took 272
packages/shrine_images/3.0x/12-0.jpg took 281
packages/shrine_images/3.0x/14-0.jpg took 220
packages/shrine_images/3.0x/16-0.jpg took 193
packages/shrine_images/3.0x/15-0.jpg took 160
packages/shrine_images/3.0x/17-0.jpg took 368
packages/shrine_images/3.0x/19-0.jpg took 227
packages/shrine_images/3.0x/18-0.jpg took 182
packages/shrine_images/3.0x/20-0.jpg took 205
packages/shrine_images/3.0x/22-0.jpg took 340
packages/shrine_images/3.0x/21-0.jpg took 217
packages/shrine_images/3.0x/23-0.jpg took 279
packages/shrine_images/3.0x/25-0.jpg took 336
packages/shrine_images/3.0x/24-0.jpg took 208
packages/shrine_images/3.0x/26-0.jpg took 305

With unsafe use of Dart_NewExternalTypedDataWithFinalizer

I/flutter ( 4801): AssetManifest.json 145
I/flutter ( 4801): packages/flutter_gallery_assets/logos/flutter_white/2.5x/logo.png 78
I/flutter ( 4801): packages/shrine_images/3.0x/slanted_menu.png 163
I/flutter ( 4801): packages/shrine_images/3.0x/diamond.png 77
I/flutter ( 4801): packages/shrine_images/3.0x/1-0.jpg 105
I/flutter ( 4801): packages/shrine_images/3.0x/0-0.jpg 115
I/flutter ( 4801): packages/shrine_images/3.0x/2-0.jpg 113
I/flutter ( 4801): packages/shrine_images/3.0x/4-0.jpg 121
I/flutter ( 4801): packages/shrine_images/3.0x/3-0.jpg 97
I/flutter ( 4801): packages/shrine_images/3.0x/5-0.jpg 120
I/flutter ( 4801): packages/shrine_images/3.0x/7-0.jpg 132
I/flutter ( 4801): packages/shrine_images/3.0x/6-0.jpg 87
I/flutter ( 4801): packages/shrine_images/3.0x/8-0.jpg 165
I/flutter ( 4801): packages/shrine_images/3.0x/10-0.jpg 163
I/flutter ( 4801): packages/shrine_images/3.0x/9-0.jpg 117
I/flutter ( 4801): packages/shrine_images/3.0x/11-0.jpg 166
I/flutter ( 4801): packages/shrine_images/3.0x/13-0.jpg 126
I/flutter ( 4801): packages/shrine_images/3.0x/12-0.jpg 97
I/flutter ( 4801): packages/shrine_images/3.0x/14-0.jpg 126
I/flutter ( 4801): packages/shrine_images/3.0x/16-0.jpg 169
I/flutter ( 4801): packages/shrine_images/3.0x/15-0.jpg 117
I/flutter ( 4801): packages/shrine_images/3.0x/17-0.jpg 108
I/flutter ( 4801): packages/shrine_images/3.0x/19-0.jpg 119
I/flutter ( 4801): packages/shrine_images/3.0x/18-0.jpg 86
I/flutter ( 4801): packages/shrine_images/3.0x/20-0.jpg 146
I/flutter ( 4801): packages/shrine_images/3.0x/22-0.jpg 150
I/flutter ( 4801): packages/shrine_images/3.0x/21-0.jpg 113
I/flutter ( 4801): packages/shrine_images/3.0x/23-0.jpg 169
I/flutter ( 4801): packages/shrine_images/3.0x/25-0.jpg 165
I/flutter ( 4801): packages/shrine_images/3.0x/24-0.jpg 118
I/flutter ( 4801): packages/shrine_images/3.0x/26-0.jpg 207
I/flutter ( 4801): packages/shrine_images/3.0x/28-0.jpg 147
I/flutter ( 4801): packages/shrine_images/3.0x/27-0.jpg 91
I/flutter ( 4801): packages/shrine_images/3.0x/29-0.jpg 108
I/flutter ( 4801): packages/shrine_images/3.0x/31-0.jpg 77
I/flutter ( 4801): packages/shrine_images/3.0x/30-0.jpg 65
I/flutter ( 4801): packages/shrine_images/3.0x/32-0.jpg 92
I/flutter ( 4801): packages/shrine_images/3.0x/34-0.jpg 92
I/flutter ( 4801): packages/shrine_images/3.0x/33-0.jpg 77
I/flutter ( 4801): packages/shrine_images/3.0x/35-0.jpg 118
I/flutter ( 4801): packages/shrine_images/3.0x/37-0.jpg 87
I/flutter ( 4801): packages/shrine_images/3.0x/36-0.jpg 70

EDIT: diff for unsafe use of the Dart_* API (yes i did not really implement the finalizer):

+  void FinalizeSkData(void* isolate_callback_data,
+                    Dart_WeakPersistentHandle handle,
+                    void* peer) {
+
+}
+
 void Assets::loadAssetBytes(Dart_NativeArguments args) {
   UIDartState::ThrowIfUIOperationsProhibited();
   Dart_Handle callback = Dart_GetNativeArgument(args, 1);
@@ -47,9 +53,15 @@ void Assets::loadAssetBytes(Dart_NativeArguments args) {
   if (data == nullptr) {
     return;
   }
+  const void* bytes_ = static_cast<const void*>(data->GetMapping());
+  void* bytes = const_cast<void*>(bytes_);
+  const intptr_t length = data->GetSize();
+  void* peer = reinterpret_cast<void*>(data.release());
+  Dart_Handle byte_buffer = Dart_NewExternalTypedDataWithFinalizer(
+      Dart_TypedData_kUint8, bytes, length, peer, length, FinalizeSkData);
 
-  Dart_Handle byte_buffer =
-      tonic::DartByteData::Create(data->GetMapping(), data->GetSize());
+  // Dart_Handle byte_buffer =
+  //     tonic::DartByteData::Create(data->GetMapping(), data->GetSize());
   tonic::DartInvoke(callback, {ToDart(byte_buffer)});
 }
 
diff --git a/lib/ui/assets.dart b/lib/ui/assets.dart
index eded711bd..8a29e20ec 100644
--- a/lib/ui/assets.dart
+++ b/lib/ui/assets.dart
@@ -14,10 +14,10 @@ part of dart.ui;
 /// If the [assetKey] does not correspond to a real asset, returns `null`.
 ByteData? loadAsset(String assetKey) {
   ByteData? result;
-  _loadAsset(assetKey, (ByteData? byteData) {
-    result = byteData;
+  _loadAsset(assetKey, (Uint8List bytes) {
+    result = bytes.buffer.asByteData();
   });
   return result;
 }
 
-void _loadAsset(String asseyKey, void Function(ByteData?) onData) native 'loadAssetBytes';
+void _loadAsset(String asseyKey, void Function(Uint8List) onData) native 'loadAssetBytes';

lib/ui/assets.cc Outdated
return;
}

Dart_Handle byte_buffer =
Copy link
Member

Choose a reason for hiding this comment

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

DBC

Overall design probably needs review, but in the meantime consider creating external typed data here to avoid a copy if possible. See: https://github.com/flutter/engine/blob/master/lib/ui/painting/image_encoding.cc#L62

Copy link
Member Author

Choose a reason for hiding this comment

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

This is something I looked at initially, but I wasn't sure if it was safe to using the data from a mapping object like that.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, if the underlying memory region is mapped as read-only, then making this external typed data might have to wait on dart-lang/sdk#42785 if returning UnmodifiableUint8ListView causes problems.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, since I can only get a const void * to the underlying data, and that API accepts void *, per my basic understanding of C++ there is no real way to make that work - since writing to the typed data would presumably cause some sort of awful runtime exception.

Copy link
Member

Choose a reason for hiding this comment

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

The const qualifier can be casted away. It is safe to wrap the read-only buffer in a Dart type that disallows writes. The issue is whether other bits of dart:ui and the framework can use that type.

Copy link
Member Author

Choose a reason for hiding this comment

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

ahh TIL, maybe some sort of lint we have is preventing const_cast? At any rate, I guess that would have to wait for another day 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.

So I needed to static_cast, then const_cast.

/// into the flutter application. This API is not supported on the Web.
///
/// If the [assetKey] does not correspond to a real asset, returns `null`.
ByteData? loadAsset(String assetKey) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we use ImmutableBuffer here, we could have a nice fast path for loading these without relying on Dart.

But right now, that class is only useful when creating images. We'd want to consider extending it in some way so you could get a ByteData from it, or a Uint8List or something.

Copy link
Contributor

Choose a reason for hiding this comment

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

Today, we ahve ImmutableBuffer.fromUint8List, but this could allow for creating ImmutableBuffers directly, which would definitely be useful for e.g. Image.asset.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry I'm not remembering all of the context around ImmutableBuffer. If we had a ReadonlyUint8List from the core Dart SDK, would ImmutableBuffer still be needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ultimately we need a way to somewhat compatible with the existing API and expose the bytes to users applications. Assets are used for more than just images today, we have APIs to load strings/bytedata/json

Copy link
Member Author

Choose a reason for hiding this comment

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

that's not to say we couldn't add a special API for images though, they are one of the most common cases

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed.

ImmutableBuffer was added to avoid having to do multiple copies of image data from a Uint8List to an SkData buffer if it ends up being reused.

If the Dart SDK has an appropriate type that's useful outside of dart:ui, that's probably better. But if not we could expand ImmutableBuffer to have access as ByteData (which, when Dart added some type appropriate for it, could just become a pass through to that type).

Comment on lines +4966 to +4967
static ImmutableBuffer? fromAsset(String assetKey) {
final ImmutableBuffer buffer = ImmutableBuffer._(0);
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 we'd want to make this a factory so we could have _fromAsset return the length of the asset in bytes, and set the lenght properly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alternatively, we could make the length a getter on a private field that is mutable.

}
return buffer;
}
static void _fromAsset(String assetKey, ImmutableBuffer buffer, _Callback<void> callback) native 'ImmutableBuffer_fromAsset';
Copy link
Contributor

Choose a reason for hiding this comment

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

The more typical signature for this is String? _fromAsset(ImmutableBuffer outBuffer, String assetKey, _Callback<void> callback) native 'ImmutableBuffer_fromAsset;. The String is used to return an error message rather than throwing in C++, which has undesirable runtime overhead/stack trace strangeness. You can then use _futureize with it.

It doesn't need to be static, and if the callback calls back with an int it could return the length.

@@ -30,6 +32,9 @@ ImmutableBuffer::~ImmutableBuffer() {}
void ImmutableBuffer::RegisterNatives(tonic::DartLibraryNatives* natives) {
natives->Register({{"ImmutableBuffer_init", ImmutableBuffer::init, 3, true},
FOR_EACH_BINDING(DART_REGISTER_NATIVE)});
natives->Register({{"ImmutableBuffer_fromAsset", ImmutableBuffer::fromAsset, 3, true},
Copy link
Contributor

Choose a reason for hiding this comment

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

If we do it this way, we don't have to pass the ImmutableBuffer itself as an argument. We also don't want it to be static.

final ImmutableBuffer buffer = ImmutableBuffer._(0);
bool success = false;
_fromAsset(assetKey, buffer, (void result) {
success = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is racy - see below. We want to use _futurize for this, or make our own completer.

Copy link
Member Author

Choose a reason for hiding this comment

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

is this actually racy? I'm not really familiar with the native interop but it appears to be fully synchronous to me

Copy link
Contributor

Choose a reason for hiding this comment

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

If someone sets the return value in the C++ side before calling the completer here I believe it will have unexpected results.

std::unique_ptr<fml::Mapping> data = asset_manager->GetAsMapping(asset_name);

if (data == nullptr) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Return a Dart String here with something like "Asset not found".

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, Dart_SetReturnValue

Copy link
Member Author

Choose a reason for hiding this comment

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

The contract for asset loading is already that we return null if it isn't found, and the framework can turn this into an exception

Comment on lines +74 to +75
Dart_PropagateError(result);
return;
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 we try to avoid this and instead use a String error.

Comment on lines +94 to +98
SkData::ReleaseProc proc = [](const void* ptr, void* context) {
// TODO: what do
};

auto sk_data = SkData::MakeWithProc(bytes, size, proc, peer);
Copy link
Contributor

Choose a reason for hiding this comment

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

We might want to just store the mapping object in our heap which should keep it alive, and then reset it in the release proc. I think that matches the semantics of the fml::Mapping, but @jason-simmons or @chinmaygarde would know for sure.

Copy link
Member

Choose a reason for hiding this comment

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

The peer will be passed to the ReleaseProc as the context parameter. The ReleaseProc can cast it back to an fml::Mapping* and delete it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Will that be enough to keep the Mapping pointer alive?

Copy link
Member

Choose a reason for hiding this comment

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

The mapping pointer should remain valid as long as the fml::Mapping and the AssetManager that created it have not been deleted.

@jonahwilliams
Copy link
Member Author

I don't have time to work on this right now

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants