Unverified Commit 8d4fd89c authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Revert "Reland "Speed up first asset load by using the binary-formatted asset...

Revert "Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution (#121322)" (#122449)

Revert "Reland "Speed up first asset load by using the binary-formatted asset manifest for image resolution"
parent aaa9cead
......@@ -4,12 +4,15 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'image_provider.dart';
const String _kAssetManifestFileName = 'AssetManifest.json';
/// A screen with a device-pixel ratio strictly less than this value is
/// considered a low-resolution screen (typically entry-level to mid-range
/// laptops, desktop screens up to QHD, low-end tablets such as Kindle Fire).
......@@ -281,18 +284,18 @@ class AssetImage extends AssetBundleImageProvider {
Completer<AssetBundleImageKey>? completer;
Future<AssetBundleImageKey>? result;
AssetManifest.loadFromAssetBundle(chosenBundle)
.then((AssetManifest manifest) {
final Iterable<AssetMetadata>? candidateVariants = manifest.getAssetVariants(keyName);
final AssetMetadata chosenVariant = _chooseVariant(
chosenBundle.loadStructuredData<Map<String, List<String>>?>(_kAssetManifestFileName, manifestParser).then<void>(
(Map<String, List<String>>? manifest) {
final String chosenName = _chooseVariant(
keyName,
configuration,
candidateVariants,
);
manifest == null ? null : manifest[keyName],
)!;
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle,
name: chosenVariant.key,
scale: chosenVariant.targetDevicePixelRatio ?? _naturalResolution,
name: chosenName,
scale: chosenScale,
);
if (completer != null) {
// We already returned from this function, which means we are in the
......@@ -306,15 +309,14 @@ class AssetImage extends AssetBundleImageProvider {
// ourselves.
result = SynchronousFuture<AssetBundleImageKey>(key);
}
})
.onError((Object error, StackTrace stack) {
// We had an error. (This guarantees we weren't called synchronously.)
// Forward the error to the caller.
assert(completer != null);
assert(result == null);
completer!.completeError(error, stack);
});
},
).catchError((Object error, StackTrace stack) {
// We had an error. (This guarantees we weren't called synchronously.)
// Forward the error to the caller.
assert(completer != null);
assert(result == null);
completer!.completeError(error, stack);
});
if (result != null) {
// The code above ran synchronously, and came up with an answer.
// Return the SynchronousFuture that we created above.
......@@ -326,24 +328,35 @@ class AssetImage extends AssetBundleImageProvider {
return completer.future;
}
AssetMetadata _chooseVariant(String mainAssetKey, ImageConfiguration config, Iterable<AssetMetadata>? candidateVariants) {
if (candidateVariants == null) {
return AssetMetadata(key: mainAssetKey, targetDevicePixelRatio: null, main: true);
/// Parses the asset manifest string into a strongly-typed map.
@visibleForTesting
static Future<Map<String, List<String>>?> manifestParser(String? jsonData) {
if (jsonData == null) {
return SynchronousFuture<Map<String, List<String>>?>(null);
}
// TODO(ianh): JSON decoding really shouldn't be on the main thread.
final Map<String, dynamic> parsedJson = json.decode(jsonData) 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>),
};
// TODO(ianh): convert that data structure to the right types.
return SynchronousFuture<Map<String, List<String>>?>(parsedManifest);
}
if (config.devicePixelRatio == null) {
return candidateVariants.firstWhere((AssetMetadata variant) => variant.main);
String? _chooseVariant(String main, ImageConfiguration config, List<String>? candidates) {
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty) {
return main;
}
final SplayTreeMap<double, AssetMetadata> candidatesByDevicePixelRatio =
SplayTreeMap<double, AssetMetadata>();
for (final AssetMetadata candidate in candidateVariants) {
candidatesByDevicePixelRatio[candidate.targetDevicePixelRatio ?? _naturalResolution] = candidate;
// TODO(ianh): Consider moving this parsing logic into _manifestParser.
final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
for (final String candidate in candidates) {
mapping[_parseScale(candidate)] = candidate;
}
// TODO(ianh): implement support for config.locale, config.textDirection,
// config.size, config.platform (then document this over in the Image.asset
// docs)
return _findBestVariant(candidatesByDevicePixelRatio, config.devicePixelRatio!);
return _findBestVariant(mapping, config.devicePixelRatio!);
}
// Returns the "best" asset variant amongst the available `candidates`.
......@@ -358,17 +371,17 @@ class AssetImage extends AssetBundleImageProvider {
// lowest key higher than `value`.
// - If the screen has high device pixel ratio, choose the variant with the
// key nearest to `value`.
AssetMetadata _findBestVariant(SplayTreeMap<double, AssetMetadata> candidatesByDpr, double value) {
if (candidatesByDpr.containsKey(value)) {
return candidatesByDpr[value]!;
String? _findBestVariant(SplayTreeMap<double, String> candidates, double value) {
if (candidates.containsKey(value)) {
return candidates[value]!;
}
final double? lower = candidatesByDpr.lastKeyBefore(value);
final double? upper = candidatesByDpr.firstKeyAfter(value);
final double? lower = candidates.lastKeyBefore(value);
final double? upper = candidates.firstKeyAfter(value);
if (lower == null) {
return candidatesByDpr[upper]!;
return candidates[upper];
}
if (upper == null) {
return candidatesByDpr[lower]!;
return candidates[lower];
}
// On screens with low device-pixel ratios the artifacts from upscaling
......@@ -376,10 +389,30 @@ class AssetImage extends AssetBundleImageProvider {
// ratios because the physical pixels are larger. Choose the higher
// resolution image in that case instead of the nearest one.
if (value < _kLowDprLimit || value > (lower + upper) / 2) {
return candidatesByDpr[upper]!;
return candidates[upper];
} else {
return candidatesByDpr[lower]!;
return candidates[lower];
}
}
static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
double _parseScale(String key) {
if (key == assetName) {
return _naturalResolution;
}
final Uri assetUri = Uri.parse(key);
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
}
@override
......
......@@ -30,12 +30,14 @@ abstract class AssetManifest {
/// information.
List<String> listAssets();
/// Retrieves metadata about an asset and its variants. Returns null if the
/// key was not found in the asset manifest.
/// Retrieves metadata about an asset and its variants.
///
/// This method considers a main asset to be a variant of itself and
/// includes it in the returned list.
List<AssetMetadata>? getAssetVariants(String key);
///
/// Throws an [ArgumentError] if [key] cannot be found within the manifest. To
/// avoid this, use a key obtained from the [listAssets] method.
List<AssetMetadata> getAssetVariants(String key);
}
// Lazily parses the binary asset manifest into a data structure that's easier to work
......@@ -62,14 +64,14 @@ class _AssetManifestBin implements AssetManifest {
final Map<String, List<AssetMetadata>> _typeCastedData = <String, List<AssetMetadata>>{};
@override
List<AssetMetadata>? getAssetVariants(String key) {
List<AssetMetadata> getAssetVariants(String key) {
// We lazily delay typecasting to prevent a performance hiccup when parsing
// large asset manifests. This is important to keep an app's first asset
// load fast.
if (!_typeCastedData.containsKey(key)) {
final Object? variantData = _data[key];
if (variantData == null) {
return null;
throw ArgumentError('Asset key $key was not found within the asset manifest.');
}
_typeCastedData[key] = ((_data[key] ?? <Object?>[]) as Iterable<Object?>)
.cast<Map<Object?, Object?>>()
......
......@@ -2,6 +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 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
......@@ -12,14 +13,18 @@ import 'package:flutter_test/flutter_test.dart';
class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle(this._assetBundleMap);
final Map<String, List<Map<Object?, Object?>>> _assetBundleMap;
final Map<String, List<String>> _assetBundleMap;
Map<String, int> loadCallCount = <String, int>{};
String get _assetBundleContents {
return json.encode(_assetBundleMap);
}
@override
Future<ByteData> load(String key) async {
if (key == 'AssetManifest.bin') {
return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
if (key == 'AssetManifest.json') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer);
}
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
......@@ -40,10 +45,9 @@ class TestAssetBundle extends CachingAssetBundle {
void main() {
group('1.0 scale device tests', () {
void buildAndTestWithOneAsset(String mainAssetPath) {
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
<String, List<Map<Object?, Object?>>>{};
final Map<String, List<String>> assetBundleMap = <String, List<String>>{};
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
assetBundleMap[mainAssetPath] = <String>[];
final AssetImage assetImage = AssetImage(
mainAssetPath,
......@@ -89,13 +93,11 @@ void main() {
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
<String, List<Map<Object?, Object?>>>{};
final Map<String, List<String>> assetBundleMap =
<String, List<String>>{};
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
final Map<Object?, Object?> mainAssetVariantManifestEntry = <Object?, Object?>{};
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[mainAssetVariantManifestEntry];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
final AssetImage assetImage = AssetImage(
......@@ -121,10 +123,10 @@ void main() {
test('When high-res device and high-res asset not present in bundle then return main variant', () {
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
<String, List<Map<Object?, Object?>>>{};
final Map<String, List<String>> assetBundleMap =
<String, List<String>>{};
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];
assetBundleMap[mainAssetPath] = <String>[mainAssetPath];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......@@ -160,13 +162,10 @@ void main() {
double chosenAssetRatio,
String expectedAssetPath,
) {
final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
<String, List<Map<Object?, Object?>>>{};
final Map<String, List<String>> assetBundleMap =
<String, List<String>>{};
final Map<Object?, Object?> mainAssetVariantManifestEntry = <Object?, Object?>{};
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[mainAssetVariantManifestEntry];
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......
......@@ -41,7 +41,7 @@ void main() {
expect(manifest.listAssets(), unorderedEquals(<String>['assets/foo.png', 'assets/bar.png']));
final List<AssetMetadata> fooVariants = manifest.getAssetVariants('assets/foo.png')!;
final List<AssetMetadata> fooVariants = manifest.getAssetVariants('assets/foo.png');
expect(fooVariants.length, 2);
final AssetMetadata firstFooVariant = fooVariants[0];
expect(firstFooVariant.key, 'assets/foo.png');
......@@ -52,7 +52,7 @@ void main() {
expect(secondFooVariant.targetDevicePixelRatio, 2.0);
expect(secondFooVariant.main, false);
final List<AssetMetadata> barVariants = manifest.getAssetVariants('assets/bar.png')!;
final List<AssetMetadata> barVariants = manifest.getAssetVariants('assets/bar.png');
expect(barVariants.length, 1);
final AssetMetadata firstBarVariant = barVariants[0];
expect(firstBarVariant.key, 'assets/bar.png');
......@@ -60,8 +60,9 @@ void main() {
expect(firstBarVariant.main, true);
});
test('getAssetVariants returns null if the key not contained in the asset manifest', () async {
test('getAssetVariants throws if given a key not contained in the asset manifest', () async {
final AssetManifest manifest = await AssetManifest.loadFromAssetBundle(TestAssetBundle());
expect(manifest.getAssetVariants('invalid asset key'), isNull);
expect(() => manifest.getAssetVariants('invalid asset key'), throwsArgumentError);
});
}
......@@ -5,7 +5,6 @@
@TestOn('!chrome')
library;
import 'dart:convert';
import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart';
......@@ -19,31 +18,27 @@ import '../image_data.dart';
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
double scaleOf(ByteData data) => data.getFloat64(0);
final Map<Object?, Object?> testManifest = json.decode('''
const String testManifest = '''
{
"assets/image.png" : [
{"asset": "assets/1.5x/image.png", "dpr": 1.5},
{"asset": "assets/2.0x/image.png", "dpr": 2.0},
{"asset": "assets/3.0x/image.png", "dpr": 3.0},
{"asset": "assets/4.0x/image.png", "dpr": 4.0}
"assets/image.png",
"assets/1.5x/image.png",
"assets/2.0x/image.png",
"assets/3.0x/image.png",
"assets/4.0x/image.png"
]
}
''') as Map<Object?, Object?>;
''';
class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle({ required Map<Object?, Object?> manifest }) {
this.manifest = const StandardMessageCodec().encodeMessage(manifest)!;
}
TestAssetBundle({ this.manifest = testManifest });
late final ByteData manifest;
final String manifest;
@override
Future<ByteData> load(String key) {
late ByteData data;
switch (key) {
case 'AssetManifest.bin':
data = manifest;
break;
case 'assets/image.png':
data = testByteData(1.0);
break;
......@@ -66,6 +61,14 @@ class TestAssetBundle extends CachingAssetBundle {
return SynchronousFuture<ByteData>(data);
}
@override
Future<String> loadString(String key, { bool cache = true }) {
if (key == 'AssetManifest.json') {
return SynchronousFuture<String>(manifest);
}
return SynchronousFuture<String>('');
}
@override
String toString() => '${describeIdentity(this)}()';
}
......@@ -104,7 +107,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize
devicePixelRatio: ratio,
),
child: DefaultAssetBundle(
bundle: bundle ?? TestAssetBundle(manifest: testManifest),
bundle: bundle ?? TestAssetBundle(),
child: Center(
child: inferSize ?
Image(
......@@ -257,21 +260,46 @@ void main() {
expect(getRenderImage(tester, key).scale, 4.0);
});
testWidgets('Image for device pixel ratio 1.0, with no main asset', (WidgetTester tester) async {
const String manifest = '''
{
"assets/image.png" : [
"assets/1.5x/image.png",
"assets/2.0x/image.png",
"assets/3.0x/image.png",
"assets/4.0x/image.png"
]
}
''';
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
const double ratio = 1.0;
Key key = GlobalKey();
await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
expect(getRenderImage(tester, key).scale, 1.5);
key = GlobalKey();
await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
expect(getRenderImage(tester, key).scale, 1.5);
});
testWidgets('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async {
// If both a main asset and a 1.0x asset are specified, then prefer
// the 1.0x asset.
final Map<Object?, Object?> manifest = json.decode('''
const String manifest = '''
{
"assets/image.png" : [
{"asset": "assets/1.0x/image.png", "dpr": 1.0},
{"asset": "assets/1.5x/image.png", "dpr": 1.5},
{"asset": "assets/2.0x/image.png", "dpr": 2.0},
{"asset": "assets/3.0x/image.png", "dpr": 3.0},
{"asset": "assets/4.0x/image.png", "dpr": 4.0}
"assets/image.png",
"assets/1.0x/image.png",
"assets/1.5x/image.png",
"assets/2.0x/image.png",
"assets/3.0x/image.png",
"assets/4.0x/image.png"
]
}
''') as Map<Object?, Object?>;
''';
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
const double ratio = 1.0;
......@@ -310,14 +338,14 @@ void main() {
// if higher resolution assets are not available we will pick the best
// available.
testWidgets('Low-resolution assets', (WidgetTester tester) async {
final Map<Object?, Object?> manifest = json.decode('''
final AssetBundle bundle = TestAssetBundle(manifest: '''
{
"assets/image.png" : [
{"asset": "assets/1.5x/image.png", "dpr": 1.5}
"assets/image.png",
"assets/1.5x/image.png"
]
}
''') as Map<Object?, Object?>;
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
''');
Future<void> testRatio({required double ratio, required double expectedScale}) async {
Key key = GlobalKey();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment