[go: nahoru, domu]

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

Speed up first asset load by using the binary-formatted asset manifest for image resolution #118782

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fd5a0cc
add asset manifest bin loading and asset manifest api
andrewkolos Jan 12, 2023
c70d5cb
Merge branch 'master' into add-asset-manifest-api-2
andrewkolos Jan 12, 2023
2828bf4
use new api for image resolution
andrewkolos Jan 12, 2023
aa86d59
remove upfront smc data casting
andrewkolos Jan 12, 2023
82859e6
fix typecasting issue
andrewkolos Jan 12, 2023
ce8cd3e
remove unused import
andrewkolos Jan 12, 2023
5e3b9cd
fix tests
andrewkolos Jan 12, 2023
4ccf193
lints
andrewkolos Jan 12, 2023
668315d
lints
andrewkolos Jan 12, 2023
542de1e
fix import
andrewkolos Jan 13, 2023
62a659c
Merge remote-tracking branch 'upstream/master' into add-asset-manifes…
andrewkolos Jan 13, 2023
cb23626
Merge remote-tracking branch 'upstream/master' into use-binary-asset-…
andrewkolos Feb 2, 2023
0944f0c
fix outdated type name
andrewkolos Feb 2, 2023
1881d67
restore AssetManifest docstrings
andrewkolos Feb 2, 2023
f4855c1
update test
andrewkolos Feb 7, 2023
36f0908
update other test
andrewkolos Feb 7, 2023
14f0564
Merge remote-tracking branch 'upstream/master' into use-binary-asset-…
andrewkolos Feb 7, 2023
f55356b
make error message for invalid keys more useful
andrewkolos Feb 7, 2023
e60f932
Merge branch 'master' into use-binary-asset-manifest-for-image-resolu…
andrewkolos Feb 8, 2023
4b3a2c1
Merge remote-tracking branch 'upstream/master' into use-binary-asset-…
andrewkolos Feb 8, 2023
10904d5
Merge remote-tracking branch 'upstream/master' into use-binary-asset-…
andrewkolos Feb 10, 2023
466c501
Merge remote-tracking branch 'upstream/master' into use-binary-asset-…
andrewkolos Feb 15, 2023
f43a38b
Merge remote-tracking branch 'upstream/master' into use-binary-asset-…
andrewkolos Feb 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add asset manifest bin loading and asset manifest api
  • Loading branch information
andrewkolos committed Jan 12, 2023
commit fd5a0cc5ee604405d7bbf2ff4b97ba09a05a7621
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show PlatformAssetBundle;
import 'package:flutter/services.dart' show AssetManifest;
import 'package:flutter/widgets.dart';

import '../common.dart';
Expand All @@ -18,16 +15,10 @@ void main() async {
final BenchmarkResultPrinter printer = BenchmarkResultPrinter();
WidgetsFlutterBinding.ensureInitialized();
final Stopwatch watch = Stopwatch();
final PlatformAssetBundle bundle = PlatformAssetBundle();

final ByteData assetManifestBytes = await bundle.load('money_asset_manifest.json');
watch.start();
for (int i = 0; i < _kNumIterations; i++) {
bundle.clear();
final String json = utf8.decode(assetManifestBytes.buffer.asUint8List());
// This is a test, so we don't need to worry about this rule.
// ignore: invalid_use_of_visible_for_testing_member
await AssetImage.manifestParser(json);
AssetManifest.loadFromRootBundle(cache: false);
}
watch.stop();

Expand Down
1 change: 1 addition & 0 deletions packages/flutter/lib/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
library services;

export 'src/services/asset_bundle.dart';
export 'src/services/asset_manifest.dart';
export 'src/services/autofill.dart';
export 'src/services/binary_messenger.dart';
export 'src/services/binding.dart';
Expand Down
83 changes: 81 additions & 2 deletions packages/flutter/lib/src/services/asset_bundle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,25 @@ abstract class AssetBundle {
}

/// Retrieve a string from the asset bundle, parse it with the given function,
/// and return the function's result.
/// and return that function's result.
///
/// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle.
Future<T> loadStructuredData<T>(String key, Future<T> Function(String value) parser);

/// Retrieve [ByteData] from the asset bundle, parse it with the given function,
/// and return that function's result.
///
/// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle.
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
final ByteData data = await load(key);
if (data == null) {
throw FlutterError('Unable to load asset: $key');
}
return parser(data);
}

/// If this is a caching asset bundle, and the given key describes a cached
/// asset, then evict the asset from the cache so that the next time it is
/// loaded, the cache will be reread from the asset bundle.
Expand Down Expand Up @@ -156,6 +169,18 @@ class NetworkAssetBundle extends AssetBundle {
return parser(await loadString(key));
}

/// Retrieve [ByteData] from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// The result is not cached. The parser is run each time the resource is
/// fetched.
@override
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
assert(key != null);
assert(parser != null);
return parser(await load(key));
}

// TODO(ianh): Once the underlying network logic learns about caching, we
// should implement evict().

Expand All @@ -175,6 +200,7 @@ abstract class CachingAssetBundle extends AssetBundle {
// TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568
final Map<String, Future<String>> _stringCache = <String, Future<String>>{};
final Map<String, Future<dynamic>> _structuredDataCache = <String, Future<dynamic>>{};
final Map<String, Future<dynamic>> _structuredBinaryDataCache = <String, Future<dynamic>>{};

@override
Future<String> loadString(String key, { bool cache = true }) {
Expand Down Expand Up @@ -225,16 +251,69 @@ abstract class CachingAssetBundle extends AssetBundle {
return completer.future;
}

/// Retrieve bytedata from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// The result of parsing the bytedata is cached (the bytedata itself is not).
/// For any given `key`, the `parser` is only run the first time.
///
/// Once the value has been parsed, the future returned by this function for
/// subsequent calls will be a [SynchronousFuture], which resolves its
/// callback synchronously.
@override
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) {
assert(key != null);
assert(parser != null);

if (_structuredBinaryDataCache.containsKey(key)) {
return _structuredBinaryDataCache[key]! as Future<T>;
}

// load can return a SynchronousFuture in certain cases, like in the
// flutter_test framework. So, we need to support both async and sync flows.
Completer<T>? completer; // For async flow.
SynchronousFuture<T>? result; // For sync flow.

load(key)
.then<T>(parser)
.then<void>((T value) {
result = SynchronousFuture<T>(value);
if (completer != null) {
// The load and parse operation ran asynchronously. We already returned
// from the loadStructuredBinaryData function and therefore the caller
// was given the future of the completer.
completer.complete(value);
}
}, onError: (Object err, StackTrace? stack) {
completer!.completeError(err, stack);
});

if (result != null) {
// The above code ran synchronously. We can synchronously return the result.
_structuredBinaryDataCache[key] = result!;
return result!;
}

// Since the above code is being run asynchronously and thus hasn't run its
// `then` handler yet, we'll return a completer that will be completed
// when the handler does run.
completer = Completer<T>();
_structuredBinaryDataCache[key] = completer.future;
return completer.future;
}

@override
void evict(String key) {
_stringCache.remove(key);
_structuredDataCache.remove(key);
_structuredBinaryDataCache.remove(key);
}

@override
void clear() {
_stringCache.clear();
_structuredDataCache.clear();
_structuredBinaryDataCache.clear();
}

@override
Expand Down Expand Up @@ -276,7 +355,7 @@ class PlatformAssetBundle extends CachingAssetBundle {
bool debugUsePlatformChannel = false;
assert(() {
// dart:io is safe to use here since we early return for web
// above. If that code is changed, this needs to be gaurded on
// above. If that code is changed, this needs to be guarded on
// web presence. Override how assets are loaded in tests so that
// the old loader behavior that allows tests to load assets from
// the current package using the package prefix.
Expand Down
181 changes: 181 additions & 0 deletions packages/flutter/lib/src/services/asset_manifest.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

const String _kLegacyAssetManifestFilename = 'AssetManifest.json';
const String _kAssetManifestFilename = 'AssetManifest.bin';

/// Contains details about available assets.
abstract class AssetManifest {
/// Loads asset manifest data from an [AssetBundle] object and creates an
/// [AssetManifest] object from that data.
static Future<AssetManifest> loadFromAssetBundle(AssetBundle bundle, {bool cache = true}) {
// TODO(andrewkolos): Once google3 and google-fonts-flutter are migrated
// away from using AssetManifest.json, remove all references to it.
// See https://github.com/flutter/flutter/issues/114913.
Future<AssetManifest> loadJsonAssetManifest(AssetBundle bundle) =>
bundle.loadStructuredData(_kLegacyAssetManifestFilename,
(String data) => SynchronousFuture<AssetManifest>(_LegacyAssetManifest.fromJsonString(data)));

Future<AssetManifest>? result;
// Since AssetBundle load calls can be synchronous (e.g. in the case of tests),
// it is not sufficient to only use catchError/onError or the onError parameter
// of Future.then--we also have to use a synchronous try/catch. Once google3
// tooling starts producing AssetManifest.bin, this block can be removed.
try {
result = bundle.loadStructuredBinaryData(_kAssetManifestFilename, _AssetManifestBin.fromStandardMessageCodecMessage);
} catch (error) {
result = loadJsonAssetManifest(bundle);
}

// To understand why we use this no-op `then` instead of `catchError`/`onError`,
// see https://github.com/flutter/flutter/issues/115601
return result.then((AssetManifest manifest) => manifest,
onError: (Object? error, StackTrace? stack) => loadJsonAssetManifest(bundle));
}

/// Loads asset manifest data from the root bundle and creates an
/// [AssetManifest] from that data.
static Future<AssetManifest> loadFromRootBundle({bool cache = true}) {
return loadFromAssetBundle(rootBundle, cache: cache);
}

/// Lists the keys of all known assets, not including asset variants.
Iterable<String> listAssets() {
throw UnimplementedError();
}

/// Gets available variants of an asset.
Iterable<AssetVariant> getAssetVariants(String key) {
throw UnimplementedError();
}
}

/// Parses the binary asset manifest into a data structure that's easier to work with.
///
/// The asset manifest is a map of asset files to a list of objects containing
/// information about variants of that asset.
///
/// The entries with each variant object are:
/// - "asset": the location of this variant to load it from.
/// - "dpr": The device-pixel-ratio that the asset is best-suited for.
///
/// New fields could be added to this object schema to support new asset variation
/// features, such as themes, locale/region support, reading directions, and so on.
class _AssetManifestBin implements AssetManifest {
_AssetManifestBin(Map<String, Object?> standardMessageData): _data = standardMessageData;

factory _AssetManifestBin.fromStandardMessageCodecMessage(ByteData message) {
final Object? data = const StandardMessageCodec().decodeMessage(message);
return _AssetManifestBin(data! as Map<String, Object?>);
}

final Map<String, Object?> _data;
final Map<String, Iterable<AssetVariant>> _typeCastedData = <String, Iterable<AssetVariant>>{};

@override
Iterable<AssetVariant> getAssetVariants(String key) {
// We lazily delay typecasting to prevent a performance hiccup when parsing
// large asset manifests.
if (!_typeCastedData.containsKey(key)) {
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as List<Object?>)
.cast<Map<String, Object?>>()
.map((Map<String, Object?> data) => AssetVariant(
key: data['asset']! as String,
targetDevicePixelRatio: data['dpr']! as double,
));

_data.remove(key);
}
return _typeCastedData[key]!;
}

@override
Iterable<String> listAssets() {
return <String>[..._data.keys, ..._typeCastedData.keys];
}
}

class _LegacyAssetManifest implements AssetManifest {

_LegacyAssetManifest({
required Map<String, Iterable<AssetVariant>> manifest,
}) : _manifest = manifest;

factory _LegacyAssetManifest.fromJsonString(String jsonString) {
List<AssetVariant> adaptLegacyVariantList(String mainAsset, List<String> variants) {

double parseScale(String mainAsset, String variant) {
// The legacy asset manifest includes the main asset within its variant list.
if (mainAsset == variant) {
return _naturalResolution;
}

final Uri assetUri = Uri.parse(variant);
String directoryPath = '';
if (assetUri.pathSegments.length > 1) {
directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
}

final Match? match = _extractRatioRegExp.firstMatch(directoryPath);
if (match != null && match.groupCount > 0) {
return double.parse(match.group(1)!);
}

return _naturalResolution; // i.e. default to 1.0x
}

return variants
.map((String variant) =>
AssetVariant(key: variant, targetDevicePixelRatio: parseScale(mainAsset, variant)))
.toList();
}

if (jsonString == null) {
return _LegacyAssetManifest(manifest: <String, List<AssetVariant>>{});
}
final Map<String, Object?> parsedJson = json.decode(jsonString) as Map<String, dynamic>;
final Iterable<String> keys = parsedJson.keys;
final Map<String, List<String>> parsedManifest = <String, List<String>> {
for (final String key in keys) key: List<String>.from(parsedJson[key]! as List<dynamic>),
};
final Map<String, List<AssetVariant>> manifestWithParsedVariants =
parsedManifest.map((String asset, List<String> variants) =>
MapEntry<String, List<AssetVariant>>(asset, adaptLegacyVariantList(asset, variants)));

return _LegacyAssetManifest(manifest: manifestWithParsedVariants);
}
// We assume the main asset is designed for a device pixel ratio of 1.0
static const double _naturalResolution = 1.0;

final Map<String, Iterable<AssetVariant>> _manifest;

static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');

@override
Iterable<AssetVariant> getAssetVariants(String key) {
return _manifest[key] ?? <AssetVariant>[];
}

@override
Iterable<String> listAssets() {
return _manifest.keys;
}
}

/// Contains information about an asset that is a variant of another asset.
class AssetVariant {
/// Creates an object containing information about an asset variant.
AssetVariant({
required this.key,
this.targetDevicePixelRatio,
});

/// The device pixel ratio that the asset is most ideal for, if any.
final double? targetDevicePixelRatio;

/// The asset's key. This can also be thought of as the logical name of an asset,
/// and it typically resembles a file location.
final String key;
}