Unverified Commit 56cad89b authored by Andrew Kolos's avatar Andrew Kolos Committed by GitHub

Speed up first asset load by encoding asset manifest in binary rather than JSON (#113637)

parent d52e2de5
......@@ -3,9 +3,9 @@
// found in the LICENSE file.
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show PlatformAssetBundle;
import 'package:flutter/services.dart' show PlatformAssetBundle, StandardMessageCodec;
import 'package:flutter/widgets.dart';
import '../common.dart';
......@@ -18,16 +18,14 @@ 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');
final ByteData assetManifest = await loadAssetManifest();
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.
// This is effectively a test.
// ignore: invalid_use_of_visible_for_testing_member
await AssetImage.manifestParser(json);
AssetImage.parseAssetManifest(assetManifest);
}
watch.stop();
......@@ -40,3 +38,49 @@ void main() async {
printer.printToStdout();
}
final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
Future<ByteData> loadAssetManifest() async {
double parseScale(String key) {
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 1.0;
}
final Map<String, dynamic> result = <String, dynamic>{};
final PlatformAssetBundle bundle = PlatformAssetBundle();
// For the benchmark, we use the older JSON format and then convert it to the modern binary format.
final ByteData jsonAssetManifestBytes = await bundle.load('money_asset_manifest.json');
final String jsonAssetManifest = utf8.decode(jsonAssetManifestBytes.buffer.asUint8List());
final Map<String, dynamic> assetManifest = json.decode(jsonAssetManifest) as Map<String, dynamic>;
for (final MapEntry<String, dynamic> manifestEntry in assetManifest.entries) {
final List<dynamic> resultVariants = <dynamic>[];
final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>();
for (final String variant in entries) {
if (variant == manifestEntry.key) {
// With the newer binary format, don't include the main asset in it's
// list of variants. This reduces parsing time at runtime.
continue;
}
final Map<String, dynamic> resultVariant = <String, dynamic>{};
final double variantDevicePixelRatio = parseScale(variant);
resultVariant['asset'] = variant;
resultVariant['dpr'] = variantDevicePixelRatio;
resultVariants.add(resultVariant);
}
result[manifestEntry.key] = resultVariants;
}
return const StandardMessageCodec().encodeMessage(result)!;
}
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_gallery/gallery/example_code_parser.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -58,4 +60,9 @@ class TestAssetBundle extends AssetBundle {
@override
String toString() => '$runtimeType@$hashCode()';
@override
Future<T> loadStructuredBinaryData<T>(String key, FutureOr<T> Function(ByteData data) parser) async {
return parser(await load(key));
}
}
......@@ -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.
......@@ -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().
......@@ -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 }) {
......@@ -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
......@@ -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.
......
......@@ -13,18 +13,14 @@ import 'package:flutter_test/flutter_test.dart';
class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle(this._assetBundleMap);
final Map<String, List<String>> _assetBundleMap;
final Map<String, List<Map<dynamic, dynamic>>> _assetBundleMap;
Map<String, int> loadCallCount = <String, int>{};
String get _assetBundleContents {
return json.encode(_assetBundleMap);
}
@override
Future<ByteData> load(String key) async {
if (key == 'AssetManifest.json') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(_assetBundleContents)).buffer);
if (key == 'AssetManifest.bin') {
return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
}
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
......@@ -42,12 +38,71 @@ class TestAssetBundle extends CachingAssetBundle {
}
}
class BundleWithoutAssetManifestBin extends CachingAssetBundle {
BundleWithoutAssetManifestBin(this._legacyAssetBundleMap);
final Map<dynamic, List<String>> _legacyAssetBundleMap;
Map<String, int> loadCallCount = <String, int>{};
@override
Future<ByteData> load(String key) async {
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
if (key == 'AssetManifest.bin') {
throw FlutterError('AssetManifest.bin was not found.');
}
if (key == 'AssetManifest.json') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert(json.encode(_legacyAssetBundleMap))).buffer);
}
switch (key) {
case 'assets/image.png':
return testByteData(1.0); // see "...with a main asset and a 1.0x asset"
case 'assets/2.0x/image.png':
return testByteData(1.5);
}
throw FlutterError('Unexpected key: $key');
}
@override
Future<ui.ImmutableBuffer> loadBuffer(String key) async {
final ByteData data = await load(key);
return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List());
}
}
void main() {
// TODO(andrewkolos): Once google3 is migrated away from using AssetManifest.json,
// remove all references to it. See https://github.com/flutter/flutter/issues/114913.
test('AssetBundle falls back to using AssetManifest.json if AssetManifest.bin cannot be found.', () async {
const String assetPath = 'assets/image.png';
final Map<dynamic, List<String>> assetBundleMap = <dynamic, List<String>>{};
assetBundleMap[assetPath] = <String>[];
final AssetImage assetImage = AssetImage(assetPath, bundle: BundleWithoutAssetManifestBin(assetBundleMap));
final AssetBundleImageKey key = await assetImage.obtainKey(ImageConfiguration.empty);
expect(key.name, assetPath);
expect(key.scale, 1.0);
});
test('When using AssetManifest.json, on a high DPR device, a high dpr variant is selected.', () async {
const String assetPath = 'assets/image.png';
const String asset2xPath = 'assets/2.0x/image.png';
final Map<dynamic, List<String>> assetBundleMap = <dynamic, List<String>>{};
assetBundleMap[assetPath] = <String>[asset2xPath];
final AssetImage assetImage = AssetImage(assetPath, bundle: BundleWithoutAssetManifestBin(assetBundleMap));
final AssetBundleImageKey key = await assetImage.obtainKey(const ImageConfiguration(devicePixelRatio: 2.0));
expect(key.name, asset2xPath);
expect(key.scale, 2.0);
});
group('1.0 scale device tests', () {
void buildAndTestWithOneAsset(String mainAssetPath) {
final Map<String, List<String>> assetBundleMap = <String, List<String>>{};
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <String>[];
assetBundleMap[mainAssetPath] = <Map<dynamic,dynamic>>[];
final AssetImage assetImage = AssetImage(
mainAssetPath,
......@@ -93,10 +148,13 @@ void main() {
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
final Map<String, List<String>> assetBundleMap =
<String, List<String>>{};
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......@@ -123,10 +181,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<String>> assetBundleMap =
<String, List<String>>{};
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <String>[mainAssetPath];
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......@@ -156,16 +214,18 @@ void main() {
const String mainAssetPath = 'assets/normalFolder/normalFile.png';
const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';
void buildBundleAndTestVariantLogic(
double deviceRatio,
double chosenAssetRatio,
String expectedAssetPath,
) {
final Map<String, List<String>> assetBundleMap =
<String, List<String>>{};
final Map<String, List<Map<dynamic, dynamic>>> assetBundleMap =
<String, List<Map<dynamic, dynamic>>>{};
assetBundleMap[mainAssetPath] = <String>[mainAssetPath, variantPath];
final Map<dynamic, dynamic> mainAssetVariantManifestEntry = <dynamic, dynamic>{};
mainAssetVariantManifestEntry['asset'] = variantPath;
mainAssetVariantManifestEntry['dpr'] = 3.0;
assetBundleMap[mainAssetPath] = <Map<dynamic, dynamic>>[mainAssetVariantManifestEntry];
final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);
......
......@@ -9,14 +9,14 @@ import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
class TestAssetBundle extends CachingAssetBundle {
class _TestAssetBundle extends CachingAssetBundle {
Map<String, int> loadCallCount = <String, int>{};
@override
Future<ByteData> load(String key) async {
loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
if (key == 'AssetManifest.json') {
return ByteData.view(Uint8List.fromList(const Utf8Encoder().convert('{"one": ["one"]}')).buffer);
if (key == 'AssetManifest.bin') {
return const StandardMessageCodec().encodeMessage(json.decode('{"one":[]}'))!;
}
if (key == 'one') {
......@@ -30,7 +30,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('Caching asset bundle test', () async {
final TestAssetBundle bundle = TestAssetBundle();
final _TestAssetBundle bundle = _TestAssetBundle();
final ByteData assetData = await bundle.load('one');
expect(assetData.getInt8(0), equals(49));
......@@ -53,7 +53,7 @@ void main() {
test('AssetImage.obtainKey succeeds with ImageConfiguration.empty', () async {
// This is a regression test for https://github.com/flutter/flutter/issues/12392
final AssetImage assetImage = AssetImage('one', bundle: TestAssetBundle());
final AssetImage assetImage = AssetImage('one', bundle: _TestAssetBundle());
final AssetBundleImageKey key = await assetImage.obtainKey(ImageConfiguration.empty);
expect(key.name, 'one');
expect(key.scale, 1.0);
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
@TestOn('!chrome')
import 'dart:convert';
import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart';
......@@ -16,27 +17,32 @@ import '../image_data.dart';
ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
double scaleOf(ByteData data) => data.getFloat64(0);
const String testManifest = '''
final Map<dynamic, dynamic> testManifest = json.decode('''
{
"assets/image.png" : [
"assets/image.png",
"assets/1.5x/image.png",
"assets/2.0x/image.png",
"assets/3.0x/image.png",
"assets/4.0x/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}
]
}
''';
''') as Map<dynamic, dynamic>;
class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle({ this.manifest = testManifest });
final String manifest;
TestAssetBundle({ required Map<dynamic, dynamic> manifest }) {
this.manifest = const StandardMessageCodec().encodeMessage(manifest)!;
}
late final ByteData 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;
......@@ -59,14 +65,6 @@ 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)}()';
}
......@@ -106,7 +104,7 @@ Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize
devicePixelRatio: ratio,
),
child: DefaultAssetBundle(
bundle: bundle ?? TestAssetBundle(),
bundle: bundle ?? TestAssetBundle(manifest: testManifest),
child: Center(
child: inferSize ?
Image(
......@@ -259,46 +257,21 @@ 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.
const String manifest = '''
final Map<dynamic, dynamic> manifest = json.decode('''
{
"assets/image.png" : [
"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"
{"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}
]
}
''';
''') as Map<dynamic, dynamic>;
final AssetBundle bundle = TestAssetBundle(manifest: manifest);
const double ratio = 1.0;
......@@ -337,14 +310,13 @@ void main() {
// if higher resolution assets are not available we will pick the best
// available.
testWidgets('Low-resolution assets', (WidgetTester tester) async {
final AssetBundle bundle = TestAssetBundle(manifest: '''
final AssetBundle bundle = TestAssetBundle(manifest: json.decode('''
{
"assets/image.png" : [
"assets/image.png",
"assets/1.5x/image.png"
{"asset": "assets/1.5x/image.png", "dpr": 1.5}
]
}
''');
''') as Map<dynamic, dynamic>);
Future<void> testRatio({required double ratio, required double expectedScale}) async {
Key key = GlobalKey();
......
......@@ -2,8 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:standard_message_codec/standard_message_codec.dart';
import 'base/context.dart';
import 'base/deferred_component.dart';
......@@ -136,6 +139,9 @@ class ManifestAssetBundle implements AssetBundle {
_splitDeferredAssets = splitDeferredAssets,
_licenseCollector = LicenseCollector(fileSystem: fileSystem);
// We assume the main asset is designed for a device pixel ratio of 1.0
static const double _defaultResolution = 1.0;
final Logger _logger;
final FileSystem _fileSystem;
final LicenseCollector _licenseCollector;
......@@ -161,7 +167,8 @@ class ManifestAssetBundle implements AssetBundle {
DateTime? _lastBuildTimestamp;
static const String _kAssetManifestJson = 'AssetManifest.json';
static const String _kAssetManifestBinFileName = 'AssetManifest.bin';
static const String _kAssetManifestJsonFileName = 'AssetManifest.json';
static const String _kNoticeFile = 'NOTICES';
// Comically, this can't be name with the more common .gz file extension
// because when it's part of an AAR and brought into another APK via gradle,
......@@ -229,8 +236,13 @@ class ManifestAssetBundle implements AssetBundle {
// device.
_lastBuildTimestamp = DateTime.now();
if (flutterManifest.isEmpty) {
entries[_kAssetManifestJson] = DevFSStringContent('{}');
entryKinds[_kAssetManifestJson] = AssetKind.regular;
entries[_kAssetManifestJsonFileName] = DevFSStringContent('{}');
entryKinds[_kAssetManifestJsonFileName] = AssetKind.regular;
final ByteData emptyAssetManifest =
const StandardMessageCodec().encodeMessage(<dynamic, dynamic>{})!;
entries[_kAssetManifestBinFileName] =
DevFSByteContent(emptyAssetManifest.buffer.asUint8List(0, emptyAssetManifest.lengthInBytes));
entryKinds[_kAssetManifestBinFileName] = AssetKind.regular;
return 0;
}
......@@ -426,7 +438,11 @@ class ManifestAssetBundle implements AssetBundle {
_wildcardDirectories[uri] ??= _fileSystem.directory(uri);
}
final DevFSStringContent assetManifest = _createAssetManifest(assetVariants, deferredComponentsAssetVariants);
final Map<String, List<String>> assetManifest =
_createAssetManifest(assetVariants, deferredComponentsAssetVariants);
final DevFSStringContent assetManifestJson = DevFSStringContent(json.encode(assetManifest));
final DevFSByteContent assetManifestBinary = _createAssetManifestBinary(assetManifest);
final DevFSStringContent fontManifest = DevFSStringContent(json.encode(fonts));
final LicenseResult licenseResult = _licenseCollector.obtainLicenses(packageConfig, additionalLicenseFiles);
if (licenseResult.errorMessages.isNotEmpty) {
......@@ -450,7 +466,8 @@ class ManifestAssetBundle implements AssetBundle {
_fileSystem.file('DOES_NOT_EXIST_RERUN_FOR_WILDCARD$suffix').absolute);
}
_setIfChanged(_kAssetManifestJson, assetManifest, AssetKind.regular);
_setIfChanged(_kAssetManifestJsonFileName, assetManifestJson, AssetKind.regular);
_setIfChanged(_kAssetManifestBinFileName, assetManifestBinary, AssetKind.regular);
_setIfChanged(kFontManifestJson, fontManifest, AssetKind.regular);
_setLicenseIfChanged(licenseResult.combinedLicenses, targetPlatform);
return 0;
......@@ -459,17 +476,31 @@ class ManifestAssetBundle implements AssetBundle {
@override
List<File> additionalDependencies = <File>[];
void _setIfChanged(String key, DevFSStringContent content, AssetKind assetKind) {
if (!entries.containsKey(key)) {
entries[key] = content;
entryKinds[key] = assetKind;
return;
void _setIfChanged(String key, DevFSContent content, AssetKind assetKind) {
bool areEqual(List<int> o1, List<int> o2) {
if (o1.length != o2.length) {
return false;
}
for (int index = 0; index < o1.length; index++) {
if (o1[index] != o2[index]) {
return false;
}
}
return true;
}
final DevFSStringContent? oldContent = entries[key] as DevFSStringContent?;
if (oldContent?.string != content.string) {
entries[key] = content;
entryKinds[key] = assetKind;
final DevFSContent? oldContent = entries[key];
// In the case that the content is unchanged, we want to avoid an overwrite
// as the isModified property may be reset to true,
if (oldContent is DevFSByteContent && content is DevFSByteContent &&
areEqual(oldContent.bytes, content.bytes)) {
return;
}
entries[key] = content;
entryKinds[key] = assetKind;
}
void _setLicenseIfChanged(
......@@ -621,14 +652,14 @@ class ManifestAssetBundle implements AssetBundle {
return deferredComponentsAssetVariants;
}
DevFSStringContent _createAssetManifest(
Map<String, List<String>> _createAssetManifest(
Map<_Asset, List<_Asset>> assetVariants,
Map<String, Map<_Asset, List<_Asset>>> deferredComponentsAssetVariants
) {
final Map<String, List<String>> jsonObject = <String, List<String>>{};
final Map<_Asset, List<String>> jsonEntries = <_Asset, List<String>>{};
final Map<String, List<String>> manifest = <String, List<String>>{};
final Map<_Asset, List<String>> entries = <_Asset, List<String>>{};
assetVariants.forEach((_Asset main, List<_Asset> variants) {
jsonEntries[main] = <String>[
entries[main] = <String>[
for (final _Asset variant in variants)
variant.entryUri.path,
];
......@@ -636,26 +667,69 @@ class ManifestAssetBundle implements AssetBundle {
if (deferredComponentsAssetVariants != null) {
for (final Map<_Asset, List<_Asset>> componentAssets in deferredComponentsAssetVariants.values) {
componentAssets.forEach((_Asset main, List<_Asset> variants) {
jsonEntries[main] = <String>[
entries[main] = <String>[
for (final _Asset variant in variants)
variant.entryUri.path,
];
});
}
}
final List<_Asset> sortedKeys = jsonEntries.keys.toList()
final List<_Asset> sortedKeys = entries.keys.toList()
..sort((_Asset left, _Asset right) => left.entryUri.path.compareTo(right.entryUri.path));
for (final _Asset main in sortedKeys) {
final String decodedEntryPath = Uri.decodeFull(main.entryUri.path);
final List<String> rawEntryVariantsPaths = jsonEntries[main]!;
final List<String> rawEntryVariantsPaths = entries[main]!;
final List<String> decodedEntryVariantPaths = rawEntryVariantsPaths
.map((String value) => Uri.decodeFull(value))
.toList();
jsonObject[decodedEntryPath] = decodedEntryVariantPaths;
manifest[decodedEntryPath] = decodedEntryVariantPaths;
}
return manifest;
}
DevFSByteContent _createAssetManifestBinary(
Map<String, List<String>> assetManifest
) {
double parseScale(String key) {
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 _defaultResolution;
}
final Map<String, dynamic> result = <String, dynamic>{};
for (final MapEntry<String, dynamic> manifestEntry in assetManifest.entries) {
final List<dynamic> resultVariants = <dynamic>[];
final List<String> entries = (manifestEntry.value as List<dynamic>).cast<String>();
for (final String variant in entries) {
if (variant == manifestEntry.key) {
// With the newer binary format, don't include the main asset in it's
// list of variants. This reduces parsing time at runtime.
continue;
}
final Map<String, dynamic> resultVariant = <String, dynamic>{};
final double variantDevicePixelRatio = parseScale(variant);
resultVariant['asset'] = variant;
resultVariant['dpr'] = variantDevicePixelRatio;
resultVariants.add(resultVariant);
}
result[manifestEntry.key] = resultVariants;
}
return DevFSStringContent(json.encode(jsonObject));
final ByteData message = const StandardMessageCodec().encodeMessage(result)!;
return DevFSByteContent(message.buffer.asUint8List(0, message.lengthInBytes));
}
static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
/// Prefixes family names and asset paths of fonts included from packages with
/// 'packages/<package_name>'
List<Font> _parsePackageFonts(
......
......@@ -57,6 +57,8 @@ dependencies:
vm_service: 9.4.0
standard_message_codec: 0.0.1+3
_fe_analyzer_shared: 50.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
analyzer: 5.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
boolean_selector: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
......@@ -88,7 +90,6 @@ dependencies:
watcher: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
dev_dependencies:
collection: 1.17.0
file_testing: 3.0.0
pubspec_parse: 1.2.1
......@@ -97,9 +98,10 @@ dev_dependencies:
json_annotation: 4.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_preamble: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test: 1.22.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.17.0
dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
# PUBSPEC CHECKSUM: 65eb
# PUBSPEC CHECKSUM: 408d
......@@ -220,7 +220,7 @@ loading-units-spelled-wrong:
expect(logger.statusText, contains('Errors checking the following files:'));
expect(logger.statusText, contains("Invalid loading units yaml file, 'loading-units' entry did not exist."));
expect(logger.statusText.contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n'), false);
expect(logger.statusText, isNot(contains('Previously existing loading units no longer exist:\n\n LoadingUnit 2\n Libraries:\n - lib1\n')));
});
testWithoutContext('loadingUnitCache validator detects malformed file: not a list', () async {
......@@ -382,7 +382,7 @@ loading-units:
validator.displayResults();
validator.attemptToolExit();
expect(logger.statusText.contains('Errors checking the following files:'), false);
expect(logger.statusText, isNot(contains('Errors checking the following files:')));
});
testWithoutContext('androidStringMapping modifies strings file', () async {
......@@ -448,9 +448,10 @@ loading-units:
.childDirectory('main')
.childFile('AndroidManifest.xml');
expect(manifestOutput.existsSync(), true);
expect(manifestOutput.readAsStringSync().contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'), true);
expect(manifestOutput.readAsStringSync().contains('android:value="invalidmapping"'), false);
expect(manifestOutput.readAsStringSync().contains("<!-- Don't delete the meta-data below."), true);
final String manifestOutputString = manifestOutput.readAsStringSync();
expect(manifestOutputString, contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'));
expect(manifestOutputString, isNot(contains('android:value="invalidmapping"')));
expect(manifestOutputString, contains("<!-- Don't delete the meta-data below."));
});
testWithoutContext('androidStringMapping adds mapping when no existing mapping', () async {
......@@ -695,8 +696,8 @@ loading-units:
.childDirectory('main')
.childFile('AndroidManifest.xml');
expect(manifestOutput.existsSync(), true);
expect(manifestOutput.readAsStringSync().contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'), true);
expect(manifestOutput.readAsStringSync().contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"')), false);
expect(manifestOutput.readAsStringSync().contains("<!-- Don't delete the meta-data below."), true);
expect(manifestOutput.readAsStringSync(), contains('<meta-data android:name="io.flutter.embedding.engine.deferredcomponents.DeferredComponentManager.loadingUnitMapping" android:value="3:component1,2:component2,4:component2"/>'));
expect(manifestOutput.readAsStringSync(), isNot(contains(RegExp(r'android:value[\s\n]*=[\s\n]*"invalidmapping"'))));
expect(manifestOutput.readAsStringSync(), contains("<!-- Don't delete the meta-data below."));
});
}
......@@ -111,8 +111,9 @@ $fontsSection
final AssetBundle bundle = AssetBundleFactory.instance.createBundle();
await bundle.build(packagesPath: '.packages');
expect(bundle.entries.length, 3); // LICENSE, AssetManifest, FontManifest
expect(bundle.entries.containsKey('FontManifest.json'), isTrue);
expect(bundle.entries.keys, containsAll(
<String>['AssetManifest.bin', 'AssetManifest.json', 'FontManifest.json', 'NOTICES.Z']
));
}, overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(),
......
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