From 059de1537e00871b302562a48d88f2267a1dec39 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 25 Aug 2020 10:00:24 -0700 Subject: [PATCH] [flutter_tools] support code size tooling on iOS, linux, windows, macOS, and Android on Windows (#63610) Adds support for size analysis on iOS, macOS, linux, and Windows - using an uncompressed directory based approach. The output format is not currently specified. Adds support for size analysis on android on windows, switching to package:archive Updates the console format to display as a tree, allowing longer paths. Increases the number of dart libraries shown (to avoid only ever printing the flutter/dart:ui libraries, which dominate the size) --- packages/flutter_tools/bin/macos_assemble.sh | 6 + packages/flutter_tools/bin/tool_backend.dart | 3 + packages/flutter_tools/bin/xcode_backend.sh | 6 + packages/flutter_tools/gradle/flutter.gradle | 10 + .../lib/src/android/build_validation.dart | 7 + .../flutter_tools/lib/src/android/gradle.dart | 56 ++-- .../lib/src/base/analyze_size.dart | 258 +++++++++++------ .../lib/src/base/file_system.dart | 18 +- .../flutter_tools/lib/src/build_info.dart | 12 +- .../lib/src/build_system/targets/android.dart | 13 + .../lib/src/build_system/targets/common.dart | 16 ++ .../lib/src/build_system/targets/ios.dart | 28 +- .../lib/src/build_system/targets/macos.dart | 58 ++-- .../lib/src/commands/build_appbundle.dart | 1 + .../lib/src/commands/build_ios.dart | 42 +++ .../lib/src/commands/build_linux.dart | 12 +- .../lib/src/commands/build_macos.dart | 7 + .../lib/src/commands/build_windows.dart | 7 + .../lib/src/linux/build_linux.dart | 26 ++ .../lib/src/macos/build_macos.dart | 35 +++ .../lib/src/runner/flutter_command.dart | 23 +- .../lib/src/windows/build_windows.dart | 26 ++ .../general.shard/base/analyze_size_test.dart | 167 ++++++----- .../general.shard/base/file_system_test.dart | 32 ++- .../test/general.shard/build_info_test.dart | 2 + .../build_system/targets/android_test.dart | 44 +++ .../build_system/targets/dart_test.dart | 119 +++++++- .../build_system/targets/macos_test.dart | 260 +++++++++--------- .../analyze_size_apk_test.dart | 35 --- .../integration.shard/analyze_size_test.dart | 82 ++++++ 30 files changed, 1013 insertions(+), 398 deletions(-) delete mode 100644 packages/flutter_tools/test/integration.shard/analyze_size_apk_test.dart create mode 100644 packages/flutter_tools/test/integration.shard/analyze_size_test.dart diff --git a/packages/flutter_tools/bin/macos_assemble.sh b/packages/flutter_tools/bin/macos_assemble.sh index f082dd75de..00dba10103 100755 --- a/packages/flutter_tools/bin/macos_assemble.sh +++ b/packages/flutter_tools/bin/macos_assemble.sh @@ -72,6 +72,11 @@ if [[ -n "$BUNDLE_SKSL_PATH" ]]; then bundle_sksl_path="-iBundleSkSLPath=${BUNDLE_SKSL_PATH}" fi +code_size_directory="" +if [[ -n "$CODE_SIZE_DIRECTORY" ]]; then + code_size_directory="-dCodeSizeDirectory=${CODE_SIZE_DIRECTORY}" +fi + RunCommand "${FLUTTER_ROOT}/bin/flutter" \ ${verbose_flag} \ ${flutter_engine_flag} \ @@ -86,6 +91,7 @@ RunCommand "${FLUTTER_ROOT}/bin/flutter" \ -dSplitDebugInfo="${SPLIT_DEBUG_INFO}" \ -dTrackWidgetCreation="${TRACK_WIDGET_CREATION}" \ ${bundle_sksl_path} \ + ${code_size_directory} \ --DartDefines="${DART_DEFINES}" \ --ExtraGenSnapshotOptions="${EXTRA_GEN_SNAPSHOT_OPTIONS}" \ --ExtraFrontEndOptions="${EXTRA_FRONT_END_OPTIONS}" \ diff --git a/packages/flutter_tools/bin/tool_backend.dart b/packages/flutter_tools/bin/tool_backend.dart index c024c0e662..9df4aa0101 100644 --- a/packages/flutter_tools/bin/tool_backend.dart +++ b/packages/flutter_tools/bin/tool_backend.dart @@ -19,6 +19,7 @@ Future main(List arguments) async { final String flutterRoot = Platform.environment['FLUTTER_ROOT']; final String flutterTarget = Platform.environment['FLUTTER_TARGET'] ?? pathJoin(['lib', 'main.dart']); + final String codeSizeDirectory = Platform.environment['CODE_SIZE_DIRECTORY']; final String localEngine = Platform.environment['LOCAL_ENGINE']; final String projectDirectory = Platform.environment['PROJECT_DIR']; final String splitDebugInfo = Platform.environment['SPLIT_DEBUG_INFO']; @@ -70,6 +71,8 @@ or '-dDartObfuscation=$dartObfuscation', if (bundleSkSLPath != null) '-iBundleSkSLPath=$bundleSkSLPath', + if (codeSizeDirectory != null) + '-dCodeSizeDirectory=$codeSizeDirectory', if (splitDebugInfo != null) '-dSplitDebugInfo=$splitDebugInfo', if (dartDefines != null) diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh index d9ff149fb0..c548f8dc70 100755 --- a/packages/flutter_tools/bin/xcode_backend.sh +++ b/packages/flutter_tools/bin/xcode_backend.sh @@ -155,6 +155,11 @@ is set to release or run \"flutter build ios --release\", then re-run Archive fr performance_measurement_option="--performance-measurement-file=${PERFORMANCE_MEASUREMENT_FILE}" fi + local code_size_directory="" + if [[ -n "$CODE_SIZE_DIRECTORY" ]]; then + code_size_directory="-dCodeSizeDirectory=${CODE_SIZE_DIRECTORY}" + fi + RunCommand "${FLUTTER_ROOT}/bin/flutter" \ ${verbose_flag} \ ${flutter_engine_flag} \ @@ -172,6 +177,7 @@ is set to release or run \"flutter build ios --release\", then re-run Archive fr -dDartObfuscation="${DART_OBFUSCATION}" \ -dEnableBitcode="${bitcode_flag}" \ ${bundle_sksl_path} \ + ${code_size_directory} \ --ExtraGenSnapshotOptions="${EXTRA_GEN_SNAPSHOT_OPTIONS}" \ --DartDefines="${DART_DEFINES}" \ --ExtraFrontEndOptions="${EXTRA_FRONT_END_OPTIONS}" \ diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index ddf51a4954..3b4e0c5f02 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -628,6 +628,10 @@ class FlutterPlugin implements Plugin { if (project.hasProperty('performance-measurement-file')) { performanceMeasurementFileValue = project.property('performance-measurement-file') } + String codeSizeDirectoryValue; + if (project.hasProperty('code-size-directory')) { + codeSizeDirectoryValue = project.property('code-size-directory') + } def targetPlatforms = getTargetPlatforms() def addFlutterDeps = { variant -> if (shouldSplitPerAbi()) { @@ -668,6 +672,7 @@ class FlutterPlugin implements Plugin { dartDefines dartDefinesValue bundleSkSLPath bundleSkSLPathValue performanceMeasurementFile performanceMeasurementFileValue + codeSizeDirectory codeSizeDirectoryValue doLast { project.exec { if (Os.isFamily(Os.FAMILY_WINDOWS)) { @@ -862,6 +867,8 @@ abstract class BaseFlutterTask extends DefaultTask { String dartDefines @Optional @Input String bundleSkSLPath + @Optional @Input + String codeSizeDirectory; String performanceMeasurementFile; @OutputFiles @@ -938,6 +945,9 @@ abstract class BaseFlutterTask extends DefaultTask { if (bundleSkSLPath != null) { args "-iBundleSkSLPath=${bundleSkSLPath}" } + if (codeSizeDirectory != null) { + args "-dCodeSizeDirectory=${codeSizeDirectory}" + } if (extraGenSnapshotOptions != null) { args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}" } diff --git a/packages/flutter_tools/lib/src/android/build_validation.dart b/packages/flutter_tools/lib/src/android/build_validation.dart index 03d14a90e9..86528184f3 100644 --- a/packages/flutter_tools/lib/src/android/build_validation.dart +++ b/packages/flutter_tools/lib/src/android/build_validation.dart @@ -11,6 +11,13 @@ const String kSupportedAbis = 'https://flutter.dev/docs/deployment/android#what- /// Validates that the build mode and build number are valid for a given build. void validateBuild(AndroidBuildInfo androidBuildInfo) { final BuildInfo buildInfo = androidBuildInfo.buildInfo; + if (buildInfo.codeSizeDirectory != null && androidBuildInfo.targetArchs.length > 1) { + throwToolExit( + 'Cannot perform code size analysis when building for multiple ABIs. ' + 'Specify one of android-arm, android-arm64, or android-x64 in the ' + '--target-plaform flag.' + ); + } if (buildInfo.mode.isPrecompiled && androidBuildInfo.targetArchs.contains(AndroidArch.x86)) { throwToolExit( 'Cannot build ${androidBuildInfo.buildInfo.mode.name} mode for x86 ABI.\n' diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index fa9f00836e..f377cacfd8 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -357,6 +357,9 @@ Future buildGradleApp({ if (androidBuildInfo.buildInfo.performanceMeasurementFile != null) { command.add('-Pperformance-measurement-file=${androidBuildInfo.buildInfo.performanceMeasurementFile}'); } + if (buildInfo.codeSizeDirectory != null) { + command.add('-Pcode-size-directory=${buildInfo.codeSizeDirectory}'); + } command.add(assembleTask); GradleHandledError detectedGradleError; @@ -467,6 +470,10 @@ Future buildGradleApp({ ? '' // Don't display the size when building a debug variant. : ' (${getSizeAsMB(bundleFile.lengthSync())})'; + if (buildInfo.codeSizeDirectory != null) { + await _performCodeSizeAnalysis('aab', bundleFile, androidBuildInfo); + } + globals.printStatus( '$successMark Built ${globals.fs.path.relative(bundleFile.path)}$appSize.', color: TerminalColor.green, @@ -502,26 +509,41 @@ Future buildGradleApp({ color: TerminalColor.green, ); - // Call size analyzer if --analyze-size flag was provided. - if (buildInfo.analyzeSize != null && !globals.platform.isWindows) { - final SizeAnalyzer sizeAnalyzer = SizeAnalyzer( - fileSystem: globals.fs, - logger: globals.logger, - processUtils: ProcessUtils.instance, - ); - final Map output = await sizeAnalyzer.analyzeApkSizeAndAotSnapshot( - apk: apkFile, - aotSnapshot: globals.fs.file(buildInfo.analyzeSize), - ); - final File outputFile = globals.fsUtils.getUniqueFile(globals.fs.currentDirectory, 'apk-analysis', 'json') - ..writeAsStringSync(jsonEncode(output)); - // This message is used as a sentinel in analyze_apk_size_test.dart - globals.printStatus( - 'A summary of your APK analysis can be found at: ${outputFile.path}', - ); + if (buildInfo.codeSizeDirectory != null) { + await _performCodeSizeAnalysis('apk', apkFile, androidBuildInfo); } } +Future _performCodeSizeAnalysis( + String kind, + File zipFile, + AndroidBuildInfo androidBuildInfo, +) async { + final SizeAnalyzer sizeAnalyzer = SizeAnalyzer( + fileSystem: globals.fs, + logger: globals.logger, + ); + final String archName = getNameForAndroidArch(androidBuildInfo.targetArchs.single); + final BuildInfo buildInfo = androidBuildInfo.buildInfo; + final File aotSnapshot = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('snapshot.$archName.json'); + final File precompilerTrace = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('trace.$archName.json'); + final Map output = await sizeAnalyzer.analyzeZipSizeAndAotSnapshot( + zipFile: zipFile, + aotSnapshot: aotSnapshot, + precompilerTrace: precompilerTrace, + kind: kind, + ); + final File outputFile = globals.fsUtils.getUniqueFile( + globals.fs.directory(getBuildDirectory()),'$kind-code-size-analysis', 'json', + )..writeAsStringSync(jsonEncode(output)); + // This message is used as a sentinel in analyze_apk_size_test.dart + globals.printStatus( + 'A summary of your ${kind.toUpperCase()} analysis can be found at: ${outputFile.path}', + ); +} + /// Builds AAR and POM files. /// /// * [project] is typically [FlutterProject.current()]. diff --git a/packages/flutter_tools/lib/src/base/analyze_size.dart b/packages/flutter_tools/lib/src/base/analyze_size.dart index 4f9898ccc5..667a26c08f 100644 --- a/packages/flutter_tools/lib/src/base/analyze_size.dart +++ b/packages/flutter_tools/lib/src/base/analyze_size.dart @@ -4,11 +4,12 @@ import 'package:meta/meta.dart'; import 'package:vm_snapshot_analysis/treemap.dart'; +import 'package:archive/archive.dart'; +import 'package:archive/archive_io.dart'; import '../base/file_system.dart'; import '../convert.dart'; import 'logger.dart'; -import 'process.dart'; import 'terminal.dart'; /// A class to analyze APK and AOT snapshot and generate a breakdown of the data. @@ -16,13 +17,11 @@ class SizeAnalyzer { SizeAnalyzer({ @required this.fileSystem, @required this.logger, - @required this.processUtils, this.appFilenamePattern = 'libapp.so', }); final FileSystem fileSystem; final Logger logger; - final ProcessUtils processUtils; final Pattern appFilenamePattern; String _appFilename; @@ -30,44 +29,24 @@ class SizeAnalyzer { static const int tableWidth = 80; - /// Analyzes [apk] and [aotSnapshot] to output a [Map] object that includes - /// the breakdown of the both files, where the breakdown of [aotSnapshot] is placed - /// under 'lib/arm64-v8a/$_appFilename'. - /// - /// The [aotSnapshot] can be either instruction sizes snapshot or v8 snapshot. - Future> analyzeApkSizeAndAotSnapshot({ - @required File apk, + static const int _kAotSizeMaxDepth = 2; + static const int _kZipSizeMaxDepth = 1; + + /// Analyze the [aotSnapshot] in an uncompressed output directory. + Future> analyzeAotSnapshot({ + @required Directory outputDirectory, @required File aotSnapshot, + @required File precompilerTrace, + @required String type, + String excludePath, }) async { logger.printStatus('▒' * tableWidth); - _printEntitySize( - '${apk.basename} (total compressed)', - byteSize: apk.lengthSync(), - level: 0, - showColor: false, - ); logger.printStatus('━' * tableWidth); - final Directory tempApkContent = fileSystem.systemTempDirectory.createTempSync('flutter_tools.'); - // TODO(peterdjlee): Implement a way to unzip the APK for Windows. See issue #62603. - String unzipOut; - try { - // TODO(peterdjlee): Use zipinfo instead of unzip. - unzipOut = (await processUtils.run([ - 'unzip', - '-o', - '-v', - apk.path, - '-d', - tempApkContent.path - ])).stdout; - } on Exception catch (e) { - logger.printError(e.toString()); - } finally { - // We just want the the stdout printout. We don't need the files. - tempApkContent.deleteSync(recursive: true); - } - - final _SymbolNode apkAnalysisRoot = _parseUnzipFile(unzipOut); + final _SymbolNode aotAnalysisJson = _parseDirectory( + outputDirectory, + outputDirectory.parent.path, + excludePath, + ); // Convert an AOT snapshot file into a map. final Map processedAotSnapshotJson = treemapFromJson( @@ -75,66 +54,122 @@ class SizeAnalyzer { ); final _SymbolNode aotSnapshotJsonRoot = _parseAotSnapshot(processedAotSnapshotJson); - for (final _SymbolNode firstLevelPath in apkAnalysisRoot.children) { + for (final _SymbolNode firstLevelPath in aotAnalysisJson.children) { _printEntitySize( firstLevelPath.name, byteSize: firstLevelPath.byteSize, level: 1, ); // Print the expansion of lib directory to show more info for `appFilename`. - if (firstLevelPath.name == 'lib') { - _printLibChildrenPaths(firstLevelPath, '', aotSnapshotJsonRoot); + if (firstLevelPath.name == fileSystem.path.basename(outputDirectory.path)) { + _printLibChildrenPaths(firstLevelPath, '', aotSnapshotJsonRoot, _kAotSizeMaxDepth, 0); } } logger.printStatus('▒' * tableWidth); + Map apkAnalysisJson = aotAnalysisJson.toJson(); + + apkAnalysisJson['type'] = type; // one of apk, aab, ios, macos, windows, or linux. + + apkAnalysisJson = _addAotSnapshotDataToAnalysis( + apkAnalysisJson: apkAnalysisJson, + path: _locatedAotFilePath, + aotSnapshotJson: processedAotSnapshotJson, + precompilerTrace: json.decode(precompilerTrace.readAsStringSync()) as Map, + ); + + assert(_appFilename != null); + return apkAnalysisJson; + } + + /// Analyzes [apk] and [aotSnapshot] to output a [Map] object that includes + /// the breakdown of the both files, where the breakdown of [aotSnapshot] is placed + /// under 'lib/arm64-v8a/$_appFilename'. + /// + /// [kind] must be one of 'apk' or 'aab'. + /// The [aotSnapshot] can be either instruction sizes snapshot or a v8 snapshot. + Future> analyzeZipSizeAndAotSnapshot({ + @required File zipFile, + @required File aotSnapshot, + @required File precompilerTrace, + @required String kind, + }) async { + assert(kind == 'apk' || kind == 'aab'); + logger.printStatus('▒' * tableWidth); + _printEntitySize( + '${zipFile.basename} (total compressed)', + byteSize: zipFile.lengthSync(), + level: 0, + showColor: false, + ); + logger.printStatus('━' * tableWidth); + + final _SymbolNode apkAnalysisRoot = _parseUnzipFile(zipFile); + + // Convert an AOT snapshot file into a map. + final Map processedAotSnapshotJson = treemapFromJson( + json.decode(aotSnapshot.readAsStringSync()), + ); + final _SymbolNode aotSnapshotJsonRoot = _parseAotSnapshot(processedAotSnapshotJson); + for (final _SymbolNode firstLevelPath in apkAnalysisRoot.children) { + _printLibChildrenPaths(firstLevelPath, '', aotSnapshotJsonRoot, _kZipSizeMaxDepth, 0); + } + logger.printStatus('▒' * tableWidth); + Map apkAnalysisJson = apkAnalysisRoot.toJson(); - apkAnalysisJson['type'] = 'apk'; + apkAnalysisJson['type'] = kind; - // TODO(peterdjlee): Add aot snapshot for all platforms. assert(_appFilename != null); - apkAnalysisJson = _addAotSnapshotDataToApkAnalysis( + apkAnalysisJson = _addAotSnapshotDataToAnalysis( apkAnalysisJson: apkAnalysisJson, - path: 'lib/arm64-v8a/$_appFilename (Dart AOT)'.split('/'), // Pass in a list of paths by splitting with '/'. + path: _locatedAotFilePath, aotSnapshotJson: processedAotSnapshotJson, + precompilerTrace: json.decode(precompilerTrace.readAsStringSync()) as Map, ); return apkAnalysisJson; } - - // Expression to match 'Size' column to group 1 and 'Name' column to group 2. - final RegExp _parseUnzipOutput = RegExp(r'^\s*\d+\s+[\w|:]+\s+(\d+)\s+.* (.+)$'); - - // Parse the output of unzip -v which shows the zip's contents' compressed sizes. - // Example output of unzip -v: - // Length Method Size Cmpr Date Time CRC-32 Name - // -------- ------ ------- ---- ---------- ----- -------- ---- - // 11708 Defl:N 2592 78% 00-00-1980 00:00 07733eef AndroidManifest.xml - // 1399 Defl:N 1092 22% 00-00-1980 00:00 f53d952a META-INF/CERT.RSA - // 46298 Defl:N 14530 69% 00-00-1980 00:00 17df02b8 META-INF/CERT.SF - _SymbolNode _parseUnzipFile(String unzipOut) { + _SymbolNode _parseUnzipFile(File zipFile) { + final Archive archive = ZipDecoder().decodeBytes(zipFile.readAsBytesSync()); final Map, int> pathsToSize = , int>{}; - // Parse each path into pathsToSize so that the key is a list of - // path parts and the value is the size. - // For example: - // 'path/to/file' where file = 1500 => pathsToSize[['path', 'to', 'file']] = 1500 - for (final String line in const LineSplitter().convert(unzipOut)) { - final RegExpMatch match = _parseUnzipOutput.firstMatch(line); - if (match == null) { + for (final ArchiveFile archiveFile in archive.files) { + pathsToSize[fileSystem.path.split(archiveFile.name)] = archiveFile.rawContent.length; + } + return _buildSymbolTree(pathsToSize); + } + + _SymbolNode _parseDirectory(Directory directory, String relativeTo, String excludePath) { + final Map, int> pathsToSize = , int>{}; + for (final File file in directory.listSync(recursive: true).whereType()) { + if (excludePath != null && file.uri.pathSegments.contains(excludePath)) { continue; } - const int sizeGroupIndex = 1; - const int nameGroupIndex = 2; - pathsToSize[match.group(nameGroupIndex).split('/')] = int.parse(match.group(sizeGroupIndex)); + final List path = fileSystem.path.split( + fileSystem.path.relative(file.path, from: relativeTo)); + pathsToSize[path] = file.lengthSync(); } + return _buildSymbolTree(pathsToSize); + } - final _SymbolNode rootNode = _SymbolNode('Root'); + List _locatedAotFilePath; + List _buildNodeName(_SymbolNode start, _SymbolNode parent) { + final List results = [start.name]; + while (parent != null && parent.name != 'Root') { + results.insert(0, parent.name); + parent = parent.parent; + } + return results; + } + + _SymbolNode _buildSymbolTree(Map, int> pathsToSize) { + final _SymbolNode rootNode = _SymbolNode('Root'); _SymbolNode currentNode = rootNode; + for (final List paths in pathsToSize.keys) { for (final String path in paths) { _SymbolNode childWithPathAsName = currentNode.childByName(path); @@ -144,6 +179,7 @@ class SizeAnalyzer { if (matchesPattern(path, pattern: appFilenamePattern) != null) { _appFilename = path; childWithPathAsName.name += ' (Dart AOT)'; + _locatedAotFilePath = _buildNodeName(childWithPathAsName, currentNode); } else if (path == 'libflutter.so') { childWithPathAsName.name += ' (Flutter Engine)'; } @@ -154,7 +190,6 @@ class SizeAnalyzer { } currentNode = rootNode; } - return rootNode; } @@ -165,52 +200,79 @@ class SizeAnalyzer { _SymbolNode currentNode, String totalPath, _SymbolNode aotSnapshotJsonRoot, + int maxDepth, + int currentDepth, ) { totalPath += currentNode.name; assert(_appFilename != null); if (currentNode.children.isNotEmpty - && currentNode.name != '$_appFilename (Dart AOT)') { + && currentNode.name != '$_appFilename (Dart AOT)' + && currentDepth < maxDepth + && currentNode.byteSize >= 1000) { for (final _SymbolNode child in currentNode.children) { - _printLibChildrenPaths(child, '$totalPath/', aotSnapshotJsonRoot); + _printLibChildrenPaths(child, '$totalPath/', aotSnapshotJsonRoot, maxDepth, currentDepth + 1); } + _leadingPaths = totalPath.split('/') + ..removeLast(); } else { - // Print total path and size if currentNode does not have any chilren. - _printEntitySize(totalPath, byteSize: currentNode.byteSize, level: 2); - - // We picked this file because arm64-v8a is likely the most popular - // architecture. Other architecture sizes should be similar. - final String libappPath = 'lib/arm64-v8a/$_appFilename'; - // TODO(peterdjlee): Analyze aot size for all platforms. - if (totalPath.contains(libappPath)) { - _printAotSnapshotSummary(aotSnapshotJsonRoot); + // Print total path and size if currentNode does not have any children and is + // larger than 1KB + final bool isAotSnapshotPath = _locatedAotFilePath.join('/').contains(totalPath); + if (currentNode.byteSize >= 1000 || isAotSnapshotPath) { + _printEntitySize(totalPath, byteSize: currentNode.byteSize, level: 1, emphasis: currentNode.children.isNotEmpty); + if (isAotSnapshotPath) { + _printAotSnapshotSummary(aotSnapshotJsonRoot, level: totalPath.split('/').length); + } + _leadingPaths = totalPath.split('/') + ..removeLast(); } } } /// Go through the AOT gen snapshot size JSON and print out a collapsed summary /// for the first package level. - void _printAotSnapshotSummary(_SymbolNode aotSnapshotRoot, {int maxDirectoriesShown = 10}) { + void _printAotSnapshotSummary(_SymbolNode aotSnapshotRoot, {int maxDirectoriesShown = 20, @required int level}) { _printEntitySize( 'Dart AOT symbols accounted decompressed size', byteSize: aotSnapshotRoot.byteSize, - level: 3, + level: level, + emphasis: true, ); final List<_SymbolNode> sortedSymbols = aotSnapshotRoot.children.toList() + // Remove entries like @unknown, @shared, and @stubs as well as private dart libraries + // which are not interpretable by end users. + ..removeWhere((_SymbolNode node) => node.name.startsWith('@') || node.name.startsWith('dart:_')) ..sort((_SymbolNode a, _SymbolNode b) => b.byteSize.compareTo(a.byteSize)); for (final _SymbolNode node in sortedSymbols.take(maxDirectoriesShown)) { - _printEntitySize(node.name, byteSize: node.byteSize, level: 4); + // Node names will have an extra leading `package:*` name, remove it to + // avoid extra nesting. + _printEntitySize(_formatExtraLeadingPackages(node.name), byteSize: node.byteSize, level: level + 1); } } + String _formatExtraLeadingPackages(String name) { + if (!name.startsWith('package')) { + return name; + } + final List chunks = name.split('/'); + if (chunks.length < 2) { + return name; + } + chunks.removeAt(0); + return chunks.join('/'); + } + /// Adds breakdown of aot snapshot data as the children of the node at the given path. - Map _addAotSnapshotDataToApkAnalysis({ + Map _addAotSnapshotDataToAnalysis({ @required Map apkAnalysisJson, @required List path, @required Map aotSnapshotJson, + @required Map precompilerTrace, }) { Map currentLevel = apkAnalysisJson; + currentLevel['precompiler-trace'] = precompilerTrace; while (path.isNotEmpty) { final List> children = currentLevel['children'] as List>; final Map childWithPathAsName = children.firstWhere( @@ -223,14 +285,16 @@ class SizeAnalyzer { return apkAnalysisJson; } + List _leadingPaths = []; + /// Print an entity's name with its size on the same line. void _printEntitySize( String entityName, { @required int byteSize, @required int level, bool showColor = true, + bool emphasis = false, }) { - final bool emphasis = level <= 1; final String formattedSize = _prettyPrintBytes(byteSize); TerminalColor color = TerminalColor.green; @@ -240,12 +304,32 @@ class SizeAnalyzer { color = TerminalColor.yellow; } - final int spaceInBetween = tableWidth - level * 2 - entityName.length - formattedSize.length; + // Compute any preceeding directories, and compare this to the stored + // directoried (in _leadingPaths) for the last entity that was printed. The + // similary determines whether or not leading directory information needs to + // be printed. + final List localSegments = entityName.split('/') + ..removeLast(); + int i = 0; + while (i < _leadingPaths.length && i < localSegments.length && _leadingPaths[i] == localSegments[i]) { + i += 1; + } + for (; i < localSegments.length; i += 1) { + logger.printStatus( + localSegments[i] + '/', + indent: (level + i) * 2, + emphasis: true, + ); + } + _leadingPaths = localSegments; + + final String baseName = fileSystem.path.basename(entityName); + final int spaceInBetween = tableWidth - (level + i) * 2 - baseName.length - formattedSize.length; logger.printStatus( - entityName + ' ' * spaceInBetween, + baseName + ' ' * spaceInBetween, newline: false, emphasis: emphasis, - indent: level * 2, + indent: (level + i) * 2, ); logger.printStatus(formattedSize, color: showColor ? color : null); } diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart index 40ac4c8b90..2c93ccb54d 100644 --- a/packages/flutter_tools/lib/src/base/file_system.dart +++ b/packages/flutter_tools/lib/src/base/file_system.dart @@ -105,7 +105,23 @@ class FileSystemUtils { if (!file.existsSync()) { return file; } - i++; + i += 1; + } + } + + /// Appends a number to a directory name in order to make it unique under a + /// directory. + Directory getUniqueDirectory(Directory dir, String baseName) { + final FileSystem fs = dir.fileSystem; + int i = 1; + + while (true) { + final String name = '${baseName}_${i.toString().padLeft(2, '0')}'; + final Directory directory = fs.directory(_fileSystem.path.join(dir.path, name)); + if (!directory.existsSync()) { + return directory; + } + i += 1; } } diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 9074655023..04dadab3d4 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -33,7 +33,7 @@ class BuildInfo { this.performanceMeasurementFile, this.packagesPath = '.packages', this.nullSafetyMode = NullSafetyMode.autodetect, - this.analyzeSize, + this.codeSizeDirectory, }); final BuildMode mode; @@ -114,9 +114,9 @@ class BuildInfo { /// rerun tasks. final String performanceMeasurementFile; - /// If provided, an output file where a v8-style heapsnapshot will be written for size - /// profiling. - final String analyzeSize; + /// If provided, an output directory where one or more v8-style heapsnapshots + /// will be written for code size profiling. + final String codeSizeDirectory; static const BuildInfo debug = BuildInfo(BuildMode.debug, null, treeShakeIcons: false); static const BuildInfo profile = BuildInfo(BuildMode.profile, null, treeShakeIcons: kIconTreeShakerEnabledDefault); @@ -178,6 +178,8 @@ class BuildInfo { 'BUNDLE_SKSL_PATH': bundleSkSLPath, if (packagesPath != null) 'PACKAGE_CONFIG': packagesPath, + if (codeSizeDirectory != null) + 'CODE_SIZE_DIRECTORY': codeSizeDirectory, }; } } @@ -698,7 +700,7 @@ String encodeDartDefines(List defines) { /// Dart defines are encoded inside [environmentDefines] as a comma-separated list. List decodeDartDefines(Map environmentDefines, String key) { if (!environmentDefines.containsKey(key) || environmentDefines[key].isEmpty) { - return const []; + return []; } return environmentDefines[key] .split(',') diff --git a/packages/flutter_tools/lib/src/build_system/targets/android.dart b/packages/flutter_tools/lib/src/build_system/targets/android.dart index aa3e55f7d4..dfdb85cc97 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/android.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/android.dart @@ -232,6 +232,19 @@ class AndroidAot extends AotElfBase { final List extraGenSnapshotOptions = decodeDartDefines(environment.defines, kExtraGenSnapshotOptions); final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true'; + final String codeSizeDirectory = environment.defines[kCodeSizeDirectory]; + + if (codeSizeDirectory != null) { + final File codeSizeFile = environment.fileSystem + .directory(codeSizeDirectory) + .childFile('snapshot.$_androidAbiName.json'); + final File precompilerTraceFile = environment.fileSystem + .directory(codeSizeDirectory) + .childFile('trace.$_androidAbiName.json'); + extraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}'); + extraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}'); + } + final int snapshotExitCode = await snapshotter.build( platform: targetPlatform, buildMode: buildMode, diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index 38ebbb2237..c4a8646110 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -68,6 +68,9 @@ const String kIosArchs = 'IosArchs'; /// Whether to enable Dart obfuscation and where to save the symbol map. const String kDartObfuscation = 'DartObfuscation'; +/// An output directory where one or more code-size measurements may be written. +const String kCodeSizeDirectory = 'CodeSizeDirectory'; + /// Copies the pre-built flutter bundle. // This is a one-off rule for implementing build bundle in terms of assemble. class CopyFlutterBundle extends Target { @@ -295,6 +298,19 @@ abstract class AotElfBase extends Target { final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]); final String splitDebugInfo = environment.defines[kSplitDebugInfo]; final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true'; + final String codeSizeDirectory = environment.defines[kCodeSizeDirectory]; + + if (codeSizeDirectory != null) { + final File codeSizeFile = environment.fileSystem + .directory(codeSizeDirectory) + .childFile('snapshot.${environment.defines[kTargetPlatform]}.json'); + final File precompilerTraceFile = environment.fileSystem + .directory(codeSizeDirectory) + .childFile('trace.${environment.defines[kTargetPlatform]}.json'); + extraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}'); + extraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}'); + } + final int snapshotExitCode = await snapshotter.build( platform: targetPlatform, buildMode: buildMode, diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 4e6ffe42de..cd218a4c5f 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -52,7 +52,7 @@ abstract class AotAssemblyBase extends Target { final TargetPlatform targetPlatform = getTargetPlatformForName(environment.defines[kTargetPlatform]); final String splitDebugInfo = environment.defines[kSplitDebugInfo]; final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true'; - final List iosArchs = environment.defines[kIosArchs] + final List darwinArchs = environment.defines[kIosArchs] ?.split(' ') ?.map(getIOSArchForName) ?.toList() @@ -60,29 +60,41 @@ abstract class AotAssemblyBase extends Target { if (targetPlatform != TargetPlatform.ios) { throw Exception('aot_assembly is only supported for iOS applications.'); } - if (iosArchs.contains(DarwinArch.x86_64)) { + if (darwinArchs.contains(DarwinArch.x86_64)) { throw Exception( 'release/profile builds are only supported for physical devices. ' - 'attempted to build for $iosArchs.' + 'attempted to build for $darwinArchs.' ); } + final String codeSizeDirectory = environment.defines[kCodeSizeDirectory]; // If we're building multiple iOS archs the binaries need to be lipo'd // together. final List> pending = >[]; - for (final DarwinArch iosArch in iosArchs) { + for (final DarwinArch darwinArch in darwinArchs) { + final List archExtraGenSnapshotOptions = List.of(extraGenSnapshotOptions); + if (codeSizeDirectory != null) { + final File codeSizeFile = environment.fileSystem + .directory(codeSizeDirectory) + .childFile('snapshot.${getNameForDarwinArch(darwinArch)}.json'); + final File precompilerTraceFile = environment.fileSystem + .directory(codeSizeDirectory) + .childFile('trace.${getNameForDarwinArch(darwinArch)}.json'); + archExtraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}'); + archExtraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}'); + } pending.add(snapshotter.build( platform: targetPlatform, buildMode: buildMode, mainPath: environment.buildDir.childFile('app.dill').path, packagesPath: environment.projectDir.childFile('.packages').path, - outputPath: environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch)), - darwinArch: iosArch, + outputPath: environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(darwinArch)), + darwinArch: darwinArch, bitcode: bitcode, quiet: true, splitDebugInfo: splitDebugInfo, dartObfuscation: dartObfuscation, - extraGenSnapshotOptions: extraGenSnapshotOptions, + extraGenSnapshotOptions: archExtraGenSnapshotOptions, )); } final List results = await Future.wait(pending); @@ -93,7 +105,7 @@ abstract class AotAssemblyBase extends Target { environment.fileSystem.directory(resultPath).parent.createSync(recursive: true); final ProcessResult result = await environment.processManager.run([ 'lipo', - ...iosArchs.map((DarwinArch iosArch) => + ...darwinArchs.map((DarwinArch iosArch) => environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')), '-create', '-output', diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index ae9761c65a..349e33bde6 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -8,7 +8,7 @@ import '../../base/file_system.dart'; import '../../base/io.dart'; import '../../base/process.dart'; import '../../build_info.dart'; -import '../../globals.dart' as globals; +import '../../globals.dart' as globals hide fs, logger, artifacts, processManager; import '../build_system.dart'; import '../depfile.dart'; import '../exceptions.dart'; @@ -52,7 +52,7 @@ abstract class UnpackMacOS extends Target { throw MissingDefineException(kBuildMode, 'unpack_macos'); } final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); - final String basePath = globals.artifacts.getArtifactPath(Artifact.flutterMacOSFramework, mode: buildMode); + final String basePath = environment.artifacts.getArtifactPath(Artifact.flutterMacOSFramework, mode: buildMode); final Directory targetDirectory = environment .outputDir .childDirectory('FlutterMacOS.framework'); @@ -61,15 +61,15 @@ abstract class UnpackMacOS extends Target { if (targetDirectory.existsSync()) { targetDirectory.deleteSync(recursive: true); } - final List inputs = globals.fs.directory(basePath) + final List inputs = environment.fileSystem.directory(basePath) .listSync(recursive: true) .whereType() .toList(); final List outputs = inputs.map((File file) { - final String relativePath = globals.fs.path.relative(file.path, from: basePath); - return globals.fs.file(globals.fs.path.join(targetDirectory.path, relativePath)); + final String relativePath = environment.fileSystem.path.relative(file.path, from: basePath); + return environment.fileSystem.file(environment.fileSystem.path.join(targetDirectory.path, relativePath)); }).toList(); - final ProcessResult result = await globals.processManager + final ProcessResult result = await environment.processManager .run(['cp', '-R', basePath, targetDirectory.path]); if (result.exitCode != 0) { throw Exception( @@ -78,8 +78,8 @@ abstract class UnpackMacOS extends Target { ); } final DepfileService depfileService = DepfileService( - logger: globals.logger, - fileSystem: globals.fs, + logger: environment.logger, + fileSystem: environment.fileSystem, ); depfileService.writeToFile( Depfile(inputs, outputs), @@ -143,7 +143,7 @@ class DebugMacOSFramework extends Target { @override Future build(Environment environment) async { - final File outputFile = globals.fs.file(globals.fs.path.join( + final File outputFile = environment.fileSystem.file(environment.fileSystem.path.join( environment.buildDir.path, 'App.framework', 'App')); outputFile.createSync(recursive: true); final File debugApp = environment.buildDir.childFile('debug_app.cc') @@ -195,16 +195,30 @@ class CompileMacOSFramework extends Target { if (buildMode == BuildMode.debug) { throw Exception('precompiled macOS framework only supported in release/profile builds.'); } + final String codeSizeDirectory = environment.defines[kCodeSizeDirectory]; final String splitDebugInfo = environment.defines[kSplitDebugInfo]; final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true'; final List extraGenSnapshotOptions = decodeDartDefines(environment.defines, kExtraGenSnapshotOptions); + + if (codeSizeDirectory != null) { + final File codeSizeFile = environment.fileSystem + .directory(codeSizeDirectory) + .childFile('snapshot.${getNameForDarwinArch(DarwinArch.x86_64)}.json'); + extraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}'); + final File precompilerTraceFile = environment.fileSystem + .directory(codeSizeDirectory) + .childFile('trace.${getNameForDarwinArch(DarwinArch.x86_64)}.json'); + extraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}'); + extraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}'); + } + final AOTSnapshotter snapshotter = AOTSnapshotter( reportTimings: false, - fileSystem: globals.fs, - logger: globals.logger, + fileSystem: environment.fileSystem, + logger: environment.logger, xcode: globals.xcode, - artifacts: globals.artifacts, - processManager: globals.processManager + artifacts: environment.artifacts, + processManager: environment.processManager ); final int result = await snapshotter.build( bitcode: false, @@ -299,8 +313,8 @@ abstract class MacOSBundleFlutterAssets extends Target { targetPlatform: TargetPlatform.darwin_x64, ); final DepfileService depfileService = DepfileService( - fileSystem: globals.fs, - logger: globals.logger, + fileSystem: environment.fileSystem, + logger: environment.logger, ); depfileService.writeToFile( assetDepfile, @@ -345,13 +359,13 @@ abstract class MacOSBundleFlutterAssets extends Target { } // Copy precompiled runtimes. try { - final String vmSnapshotData = globals.artifacts.getArtifactPath(Artifact.vmSnapshotData, + final String vmSnapshotData = environment.artifacts.getArtifactPath(Artifact.vmSnapshotData, platform: TargetPlatform.darwin_x64, mode: BuildMode.debug); - final String isolateSnapshotData = globals.artifacts.getArtifactPath(Artifact.isolateSnapshotData, + final String isolateSnapshotData = environment.artifacts.getArtifactPath(Artifact.isolateSnapshotData, platform: TargetPlatform.darwin_x64, mode: BuildMode.debug); - globals.fs.file(vmSnapshotData).copySync( + environment.fileSystem.file(vmSnapshotData).copySync( assetDirectory.childFile('vm_snapshot_data').path); - globals.fs.file(isolateSnapshotData).copySync( + environment.fileSystem.file(isolateSnapshotData).copySync( assetDirectory.childFile('isolate_snapshot_data').path); } on Exception catch (err) { throw Exception('Failed to copy precompiled runtimes: $err'); @@ -364,7 +378,7 @@ abstract class MacOSBundleFlutterAssets extends Target { final Link currentVersion = outputDirectory.parent .childLink('Current'); if (!currentVersion.existsSync()) { - final String linkPath = globals.fs.path.relative(outputDirectory.path, + final String linkPath = environment.fileSystem.path.relative(outputDirectory.path, from: outputDirectory.parent.path); currentVersion.createSync(linkPath); } @@ -372,7 +386,7 @@ abstract class MacOSBundleFlutterAssets extends Target { final Link currentResources = frameworkRootDirectory .childLink('Resources'); if (!currentResources.existsSync()) { - final String linkPath = globals.fs.path.relative(globals.fs.path.join(currentVersion.path, 'Resources'), + final String linkPath = environment.fileSystem.path.relative(environment.fileSystem.path.join(currentVersion.path, 'Resources'), from: frameworkRootDirectory.path); currentResources.createSync(linkPath); } @@ -380,7 +394,7 @@ abstract class MacOSBundleFlutterAssets extends Target { final Link currentFramework = frameworkRootDirectory .childLink('App'); if (!currentFramework.existsSync()) { - final String linkPath = globals.fs.path.relative(globals.fs.path.join(currentVersion.path, 'App'), + final String linkPath = environment.fileSystem.path.relative(environment.fileSystem.path.join(currentVersion.path, 'App'), from: frameworkRootDirectory.path); currentFramework.createSync(linkPath); } diff --git a/packages/flutter_tools/lib/src/commands/build_appbundle.dart b/packages/flutter_tools/lib/src/commands/build_appbundle.dart index 5cc1135d19..1c9f02fa53 100644 --- a/packages/flutter_tools/lib/src/commands/build_appbundle.dart +++ b/packages/flutter_tools/lib/src/commands/build_appbundle.dart @@ -34,6 +34,7 @@ class BuildAppBundleCommand extends BuildSubCommand { usesTrackWidgetCreation(verboseHelp: verboseHelp); addNullSafetyModeOptions(hide: !verboseHelp); addEnableExperimentation(hide: !verboseHelp); + usesAnalyzeSizeFlag(); argParser.addMultiOption('target-platform', splitCommas: true, defaultsTo: ['android-arm', 'android-arm64', 'android-x64'], diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index 459f2504b6..34faaa29e2 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -4,12 +4,15 @@ import 'dart:async'; +import 'package:file/file.dart'; import 'package:meta/meta.dart'; import '../application_package.dart'; +import '../base/analyze_size.dart'; import '../base/common.dart'; import '../base/utils.dart'; import '../build_info.dart'; +import '../convert.dart'; import '../globals.dart' as globals; import '../ios/mac.dart'; import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult; @@ -35,6 +38,7 @@ class BuildIOSCommand extends BuildSubCommand { addBuildPerformanceFile(hide: !verboseHelp); addBundleSkSLPathOption(hide: !verboseHelp); addNullSafetyModeOptions(hide: !verboseHelp); + usesAnalyzeSizeFlag(); argParser ..addFlag('simulator', help: 'Build for the iOS simulator instead of the device. This changes ' @@ -103,6 +107,44 @@ class BuildIOSCommand extends BuildSubCommand { throwToolExit('Encountered error while building for $logTarget.'); } + if (buildInfo.codeSizeDirectory != null) { + final SizeAnalyzer sizeAnalyzer = SizeAnalyzer( + fileSystem: globals.fs, + logger: globals.logger, + appFilenamePattern: 'App' + ); + // Only support 64bit iOS code size analysis. + final String arch = getNameForDarwinArch(DarwinArch.arm64); + final File aotSnapshot = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('snapshot.$arch.json'); + final File precompilerTrace = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('trace.$arch.json'); + + // This analysis is only supported for release builds, which also excludes the simulator. + // Attempt to guess the correct .app by picking the first one. + final Directory candidateDirectory = globals.fs.directory( + globals.fs.path.join(getIosBuildDirectory(), 'Release-iphoneos'), + ); + final Directory appDirectory = candidateDirectory.listSync() + .whereType() + .firstWhere((Directory directory) { + return globals.fs.path.extension(directory.path) == '.app'; + }); + final Map output = await sizeAnalyzer.analyzeAotSnapshot( + aotSnapshot: aotSnapshot, + precompilerTrace: precompilerTrace, + outputDirectory: appDirectory, + type: 'ios', + ); + final File outputFile = globals.fsUtils.getUniqueFile( + globals.fs.directory(getBuildDirectory()),'ios-code-size-analysis', 'json', + )..writeAsStringSync(jsonEncode(output)); + // This message is used as a sentinel in analyze_apk_size_test.dart + globals.printStatus( + 'A summary of your iOS bundle analysis can be found at: ${outputFile.path}', + ); + } + if (result.output != null) { globals.printStatus('Built ${result.output}.'); } diff --git a/packages/flutter_tools/lib/src/commands/build_linux.dart b/packages/flutter_tools/lib/src/commands/build_linux.dart index e5a1dfddac..d14a662498 100644 --- a/packages/flutter_tools/lib/src/commands/build_linux.dart +++ b/packages/flutter_tools/lib/src/commands/build_linux.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import '../base/analyze_size.dart'; import '../base/common.dart'; import '../build_info.dart'; import '../cache.dart'; @@ -30,6 +31,7 @@ class BuildLinuxCommand extends BuildSubCommand { addBuildPerformanceFile(hide: !verboseHelp); addBundleSkSLPathOption(hide: !verboseHelp); addNullSafetyModeOptions(hide: !verboseHelp); + usesAnalyzeSizeFlag(); } @override @@ -56,7 +58,15 @@ class BuildLinuxCommand extends BuildSubCommand { if (!globals.platform.isLinux) { throwToolExit('"build linux" only supported on Linux hosts.'); } - await buildLinux(flutterProject.linux, buildInfo, target: targetFile); + await buildLinux( + flutterProject.linux, + buildInfo, + target: targetFile, + sizeAnalyzer: SizeAnalyzer( + fileSystem: globals.fs, + logger: globals.logger, + ), + ); return FlutterCommandResult.success(); } } diff --git a/packages/flutter_tools/lib/src/commands/build_macos.dart b/packages/flutter_tools/lib/src/commands/build_macos.dart index 0667a49f7a..6d2be4731c 100644 --- a/packages/flutter_tools/lib/src/commands/build_macos.dart +++ b/packages/flutter_tools/lib/src/commands/build_macos.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import '../base/analyze_size.dart'; import '../base/common.dart'; import '../build_info.dart'; import '../cache.dart'; @@ -31,6 +32,7 @@ class BuildMacosCommand extends BuildSubCommand { addBuildPerformanceFile(hide: !verboseHelp); addBundleSkSLPathOption(hide: !verboseHelp); addNullSafetyModeOptions(hide: !verboseHelp); + usesAnalyzeSizeFlag(); } @override @@ -62,6 +64,11 @@ class BuildMacosCommand extends BuildSubCommand { buildInfo: buildInfo, targetOverride: targetFile, verboseLogging: globals.logger.isVerbose, + sizeAnalyzer: SizeAnalyzer( + fileSystem: globals.fs, + logger: globals.logger, + appFilenamePattern: 'App', + ), ); return FlutterCommandResult.success(); } diff --git a/packages/flutter_tools/lib/src/commands/build_windows.dart b/packages/flutter_tools/lib/src/commands/build_windows.dart index 743b54812f..170cde0c68 100644 --- a/packages/flutter_tools/lib/src/commands/build_windows.dart +++ b/packages/flutter_tools/lib/src/commands/build_windows.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import '../base/analyze_size.dart'; import '../base/common.dart'; import '../build_info.dart'; import '../cache.dart'; @@ -33,6 +34,7 @@ class BuildWindowsCommand extends BuildSubCommand { addBuildPerformanceFile(hide: !verboseHelp); addBundleSkSLPathOption(hide: !verboseHelp); addNullSafetyModeOptions(hide: !verboseHelp); + usesAnalyzeSizeFlag(); } @override @@ -67,6 +69,11 @@ class BuildWindowsCommand extends BuildSubCommand { buildInfo, target: targetFile, visualStudioOverride: visualStudioOverride, + sizeAnalyzer: SizeAnalyzer( + fileSystem: globals.fs, + logger: globals.logger, + appFilenamePattern: 'app.so', + ), ); return FlutterCommandResult.success(); } diff --git a/packages/flutter_tools/lib/src/linux/build_linux.dart b/packages/flutter_tools/lib/src/linux/build_linux.dart index fbbbf9ea21..c5cda52e21 100644 --- a/packages/flutter_tools/lib/src/linux/build_linux.dart +++ b/packages/flutter_tools/lib/src/linux/build_linux.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import '../artifacts.dart'; +import '../base/analyze_size.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; @@ -11,6 +12,7 @@ import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; import '../cmake.dart'; +import '../convert.dart'; import '../globals.dart' as globals; import '../plugins.dart'; import '../project.dart'; @@ -20,6 +22,7 @@ Future buildLinux( LinuxProject linuxProject, BuildInfo buildInfo, { String target = 'lib/main.dart', + SizeAnalyzer sizeAnalyzer, }) async { if (!linuxProject.cmakeFile.existsSync()) { throwToolExit('No Linux desktop project configured. See ' @@ -53,6 +56,29 @@ Future buildLinux( } finally { status.cancel(); } + if (buildInfo.codeSizeDirectory != null && sizeAnalyzer != null) { + final String arch = getNameForTargetPlatform(TargetPlatform.linux_x64); + final File codeSizeFile = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('snapshot.$arch.json'); + final File precompilerTrace = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('trace.$arch.json'); + final Map output = await sizeAnalyzer.analyzeAotSnapshot( + aotSnapshot: codeSizeFile, + // This analysis is only supported for release builds. + outputDirectory: globals.fs.directory( + globals.fs.path.join(getLinuxBuildDirectory(), 'release', 'bundle'), + ), + precompilerTrace: precompilerTrace, + type: 'linux', + ); + final File outputFile = globals.fsUtils.getUniqueFile( + globals.fs.directory(getBuildDirectory()),'linux-code-size-analysis', 'json', + )..writeAsStringSync(jsonEncode(output)); + // This message is used as a sentinel in analyze_apk_size_test.dart + globals.printStatus( + 'A summary of your Linux bundle analysis can be found at: ${outputFile.path}', + ); + } } Future _runCmake(String buildModeName, Directory sourceDir, Directory buildDir) async { diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index 6e064a66de..1f7459b0d2 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -4,11 +4,13 @@ import 'package:meta/meta.dart'; +import '../base/analyze_size.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../build_info.dart'; +import '../convert.dart'; import '../globals.dart' as globals; import '../ios/xcodeproj.dart'; import '../project.dart'; @@ -25,6 +27,7 @@ Future buildMacOS({ BuildInfo buildInfo, String targetOverride, @required bool verboseLogging, + SizeAnalyzer sizeAnalyzer, }) async { if (!flutterProject.macos.xcodeWorkspace.existsSync()) { throwToolExit('No macOS desktop project configured. ' @@ -106,5 +109,37 @@ Future buildMacOS({ if (result != 0) { throwToolExit('Build process failed'); } + if (buildInfo.codeSizeDirectory != null && sizeAnalyzer != null) { + final String arch = getNameForDarwinArch(DarwinArch.x86_64); + final File aotSnapshot = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('snapshot.$arch.json'); + final File precompilerTrace = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('trace.$arch.json'); + + // This analysis is only supported for release builds. + // Attempt to guess the correct .app by picking the first one. + final Directory candidateDirectory = globals.fs.directory( + globals.fs.path.join(getMacOSBuildDirectory(), 'Build', 'Products', 'Release'), + ); + final Directory appDirectory = candidateDirectory.listSync() + .whereType() + .firstWhere((Directory directory) { + return globals.fs.path.extension(directory.path) == '.app'; + }); + final Map output = await sizeAnalyzer.analyzeAotSnapshot( + aotSnapshot: aotSnapshot, + precompilerTrace: precompilerTrace, + outputDirectory: appDirectory, + type: 'macos', + excludePath: 'Versions', // Avoid double counting caused by symlinks + ); + final File outputFile = globals.fsUtils.getUniqueFile( + globals.fs.directory(getBuildDirectory()),'macos-code-size-analysis', 'json', + )..writeAsStringSync(jsonEncode(output)); + // This message is used as a sentinel in analyze_apk_size_test.dart + globals.printStatus( + 'A summary of your macOS bundle analysis can be found at: ${outputFile.path}', + ); + } globals.flutterUsage.sendTiming('build', 'xcode-macos', Duration(milliseconds: sw.elapsedMilliseconds)); } diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 6cf67133be..2100e93913 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -609,7 +609,9 @@ abstract class FlutterCommand extends Command { FlutterOptions.kAnalyzeSize, defaultsTo: false, help: 'Whether to produce additional profile information for artifact output size. ' - 'This flag is only supported on release builds on macOS/Linux hosts.' + 'This flag is only supported on release builds. When building for Android, a single ' + 'ABI must be specified at a time with the --target-platform flag. When building for iOS, ' + 'only the symbols from the arm64 architecture are used to analyze code size.' ); } @@ -648,13 +650,14 @@ abstract class FlutterCommand extends Command { } } - String analyzeSize; - if (argParser.options.containsKey(FlutterOptions.kAnalyzeSize) - && boolArg(FlutterOptions.kAnalyzeSize) - && !globals.platform.isWindows) { - final File file = globals.fsUtils.getUniqueFile(globals.fs.currentDirectory, 'flutter_size', 'json'); - extraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${file.path}'); - analyzeSize = file.path; + String codeSizeDirectory; + if (argParser.options.containsKey(FlutterOptions.kAnalyzeSize) && boolArg(FlutterOptions.kAnalyzeSize)) { + final Directory directory = globals.fsUtils.getUniqueDirectory( + globals.fs.directory(getBuildDirectory()), + 'flutter_size', + ); + directory.createSync(recursive: true); + codeSizeDirectory = directory.path; } NullSafetyMode nullSafetyMode = NullSafetyMode.unsound; @@ -688,7 +691,7 @@ abstract class FlutterCommand extends Command { ); } final BuildMode buildMode = forcedBuildMode ?? getBuildMode(); - if (buildMode != BuildMode.release && analyzeSize != null) { + if (buildMode != BuildMode.release && codeSizeDirectory != null) { throwToolExit('--analyze-size can only be used on release builds.'); } @@ -736,7 +739,7 @@ abstract class FlutterCommand extends Command { performanceMeasurementFile: performanceMeasurementFile, packagesPath: globalResults['packages'] as String ?? '.packages', nullSafetyMode: nullSafetyMode, - analyzeSize: analyzeSize, + codeSizeDirectory: codeSizeDirectory, ); } diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart index b4b4cd36e9..9ad43cd92e 100644 --- a/packages/flutter_tools/lib/src/windows/build_windows.dart +++ b/packages/flutter_tools/lib/src/windows/build_windows.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import '../artifacts.dart'; +import '../base/analyze_size.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; @@ -11,6 +12,7 @@ import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; import '../cmake.dart'; +import '../convert.dart'; import '../globals.dart' as globals; import '../plugins.dart'; import '../project.dart'; @@ -25,6 +27,7 @@ const String _cmakeVisualStudioGeneratorIdentifier = 'Visual Studio 16 2019'; Future buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, { String target, VisualStudio visualStudioOverride, + SizeAnalyzer sizeAnalyzer, }) async { if (!windowsProject.cmakeFile.existsSync()) { throwToolExit( @@ -75,6 +78,29 @@ Future buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, { } finally { status.cancel(); } + if (buildInfo.codeSizeDirectory != null && sizeAnalyzer != null) { + final String arch = getNameForTargetPlatform(TargetPlatform.windows_x64); + final File codeSizeFile = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('snapshot.$arch.json'); + final File precompilerTrace = globals.fs.directory(buildInfo.codeSizeDirectory) + .childFile('trace.$arch.json'); + final Map output = await sizeAnalyzer.analyzeAotSnapshot( + aotSnapshot: codeSizeFile, + // This analysis is only supported for release builds. + outputDirectory: globals.fs.directory( + globals.fs.path.join(getWindowsBuildDirectory(), 'runner', 'Release'), + ), + precompilerTrace: precompilerTrace, + type: 'windows', + ); + final File outputFile = globals.fsUtils.getUniqueFile( + globals.fs.directory(getBuildDirectory()),'windows-code-size-analysis', 'json', + )..writeAsStringSync(jsonEncode(output)); + // This message is used as a sentinel in analyze_apk_size_test.dart + globals.printStatus( + 'A summary of your Windows bundle analysis can be found at: ${outputFile.path}', + ); + } } Future _runCmakeGeneration(String cmakePath, Directory buildDir, Directory sourceDir) async { diff --git a/packages/flutter_tools/test/general.shard/base/analyze_size_test.dart b/packages/flutter_tools/test/general.shard/base/analyze_size_test.dart index d11e3eeff1..029d036a2d 100644 --- a/packages/flutter_tools/test/general.shard/base/analyze_size_test.dart +++ b/packages/flutter_tools/test/general.shard/base/analyze_size_test.dart @@ -2,34 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:archive/archive.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/analyze_size.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; -import 'package:flutter_tools/src/base/process.dart'; import '../../src/common.dart'; -import '../../src/context.dart'; - -const FakeCommand unzipCommmand = FakeCommand( - command: [ - 'unzip', - '-o', - '-v', - 'test.apk', - '-d', - '/.tmp_rand0/flutter_tools.rand0' - ], - stdout: ''' -Length Method Size Cmpr Date Time CRC-32 Name --------- ------ ------- ---- ---------- ----- -------- ---- -11708 Defl:N 2592 78% 00-00-1980 00:00 07733eef AndroidManifest.xml -1399 Defl:N 1092 22% 00-00-1980 00:00 f53d952a META-INF/CERT.RSA -46298 Defl:N 14530 69% 00-00-1980 00:00 17df02b8 META-INF/CERT.SF -46298 Defl:N 14530 69% 00-00-1980 00:00 17df02b8 lib/arm64-v8a/libxyzzyapp.so -46298 Defl:N 14530 69% 00-00-1980 00:00 17df02b8 lib/arm64-v8a/libflutter.so -''', -); const String aotSizeOutput = '''[ { @@ -62,12 +41,10 @@ const String aotSizeOutput = '''[ void main() { MemoryFileSystem fileSystem; BufferLogger logger; - FakeProcessManager processManager; setUp(() { fileSystem = MemoryFileSystem.test(); logger = BufferLogger.test(); - processManager = FakeProcessManager.list([unzipCommmand]); }); test('matchesPattern matches only entire strings', () { @@ -85,45 +62,56 @@ void main() { final SizeAnalyzer sizeAnalyzer = SizeAnalyzer( fileSystem: fileSystem, logger: logger, - processUtils: ProcessUtils( - processManager: processManager, - logger: logger, - ), appFilenamePattern: RegExp(r'lib.*app\.so'), ); - final File apk = fileSystem.file('test.apk')..createSync(); + final Archive archive = Archive() + ..addFile(ArchiveFile('AndroidManifest.xml', 100, List.filled(100, 0))) + ..addFile(ArchiveFile('META-INF/CERT.RSA', 10, List.filled(10, 0))) + ..addFile(ArchiveFile('META-INF/CERT.SF', 10, List.filled(10, 0))) + ..addFile(ArchiveFile('lib/arm64-v8a/libxyzzyapp.so', 50, List.filled(50, 0))) + ..addFile(ArchiveFile('lib/arm64-v8a/libflutter.so', 50, List.filled(50, 0))); + + final File apk = fileSystem.file('test.apk') + ..writeAsBytesSync(ZipEncoder().encode(archive)); final File aotSizeJson = fileSystem.file('test.json') ..createSync() ..writeAsStringSync(aotSizeOutput); - final Map result = await sizeAnalyzer.analyzeApkSizeAndAotSnapshot(apk: apk, aotSnapshot: aotSizeJson); + final File precompilerTrace = fileSystem.file('trace.json') + ..writeAsStringSync('{}'); + final Map result = await sizeAnalyzer.analyzeZipSizeAndAotSnapshot( + zipFile: apk, + aotSnapshot: aotSizeJson, + precompilerTrace: precompilerTrace, + kind: 'apk', + ); - expect(result['type'], contains('apk')); + expect(result['type'], 'apk'); final Map androidManifestMap = result['children'][0] as Map; - expect(androidManifestMap['n'], equals('AndroidManifest.xml')); - expect(androidManifestMap['value'], equals(2592)); + expect(androidManifestMap['n'], 'AndroidManifest.xml'); + expect(androidManifestMap['value'], 6); final Map metaInfMap = result['children'][1] as Map; - expect(metaInfMap['n'], equals('META-INF')); - expect(metaInfMap['value'], equals(15622)); + expect(metaInfMap['n'], 'META-INF'); + expect(metaInfMap['value'], 10); final Map certRsaMap = metaInfMap['children'][0] as Map; - expect(certRsaMap['n'], equals('CERT.RSA')); - expect(certRsaMap['value'], equals(1092)); + expect(certRsaMap['n'], 'CERT.RSA'); + expect(certRsaMap['value'], 5); final Map certSfMap = metaInfMap['children'][1] as Map; - expect(certSfMap['n'], equals('CERT.SF')); - expect(certSfMap['value'], equals(14530)); + expect(certSfMap['n'], 'CERT.SF'); + expect(certSfMap['value'], 5); final Map libMap = result['children'][2] as Map; - expect(libMap['n'], equals('lib')); - expect(libMap['value'], equals(29060)); + expect(libMap['n'], 'lib'); + expect(libMap['value'], 12); final Map arm64Map = libMap['children'][0] as Map; - expect(arm64Map['n'], equals('arm64-v8a')); - expect(arm64Map['value'], equals(29060)); + expect(arm64Map['n'], 'arm64-v8a'); + expect(arm64Map['value'], 12); final Map libAppMap = arm64Map['children'][0] as Map; - expect(libAppMap['n'], equals('libxyzzyapp.so (Dart AOT)')); - expect(libAppMap['value'], equals(14530)); - expect(libAppMap['children'].length, equals(3)); + expect(libAppMap['n'], 'libxyzzyapp.so (Dart AOT)'); + expect(libAppMap['value'], 6); + expect(libAppMap['children'].length, 3); final Map internalMap = libAppMap['children'][0] as Map; final Map skipMap = internalMap['children'][0] as Map; expect(skipMap['n'], 'skip'); @@ -140,41 +128,92 @@ void main() { expect(allocateMap['n'], 'Allocate ArgumentError'); expect(allocateMap['value'], 4650); final Map libFlutterMap = arm64Map['children'][1] as Map; - expect(libFlutterMap['n'], equals('libflutter.so (Flutter Engine)')); - expect(libFlutterMap['value'], equals(14530)); + expect(libFlutterMap['n'], 'libflutter.so (Flutter Engine)'); + expect(libFlutterMap['value'], 6); + + expect(result['precompiler-trace'], {}); }); test('outputs summary to command line correctly', () async { final SizeAnalyzer sizeAnalyzer = SizeAnalyzer( fileSystem: fileSystem, logger: logger, - processUtils: ProcessUtils( - processManager: processManager, - logger: logger, - ), appFilenamePattern: RegExp(r'lib.*app\.so'), ); - final File apk = fileSystem.file('test.apk')..createSync(); + final Archive archive = Archive() + ..addFile(ArchiveFile('AndroidManifest.xml', 100, List.filled(100, 0))) + ..addFile(ArchiveFile('META-INF/CERT.RSA', 10, List.filled(10, 0))) + ..addFile(ArchiveFile('META-INF/CERT.SF', 10, List.filled(10, 0))) + ..addFile(ArchiveFile('lib/arm64-v8a/libxyzzyapp.so', 50, List.filled(50, 0))) + ..addFile(ArchiveFile('lib/arm64-v8a/libflutter.so', 50, List.filled(50, 0))); + + final File apk = fileSystem.file('test.apk') + ..writeAsBytesSync(ZipEncoder().encode(archive)); final File aotSizeJson = fileSystem.file('test.json') - ..createSync() ..writeAsStringSync(aotSizeOutput); - await sizeAnalyzer.analyzeApkSizeAndAotSnapshot(apk: apk, aotSnapshot: aotSizeJson); + final File precompilerTrace = fileSystem.file('trace.json') + ..writeAsStringSync('{}'); + await sizeAnalyzer.analyzeZipSizeAndAotSnapshot( + zipFile: apk, + aotSnapshot: aotSizeJson, + precompilerTrace: precompilerTrace, + kind: 'apk', + ); final List stdout = logger.statusText.split('\n'); expect( stdout, containsAll([ - ' AndroidManifest.xml 3 KB', - ' META-INF 15 KB', - ' lib 28 KB', - ' lib/arm64-v8a/libxyzzyapp.so (Dart AOT) 14 KB', - ' Dart AOT symbols accounted decompressed size 14 KB', - ' dart:_internal/SubListIterable 6 KB', - ' @stubs/allocation-stubs/dart:core/ArgumentError 5 KB', - ' dart:core/RangeError 4 KB', - ' lib/arm64-v8a/libflutter.so (Flutter Engine) 14 KB', + 'test.apk (total compressed) 644 B', + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', + ' lib 12 B', + ' Dart AOT symbols accounted decompressed size 14 KB', + ' dart:core/', + ' RangeError 4 KB', ]), ); }); + + test('can analyze contents of output directory', () async { + final SizeAnalyzer sizeAnalyzer = SizeAnalyzer( + fileSystem: fileSystem, + logger: logger, + appFilenamePattern: RegExp(r'lib.*app\.so'), + ); + + final Directory outputDirectory = fileSystem.directory('example/out/foo.app') + ..createSync(recursive: true); + outputDirectory.childFile('a.txt') + ..createSync() + ..writeAsStringSync('hello'); + outputDirectory.childFile('libapp.so') + ..createSync() + ..writeAsStringSync('goodbye'); + final File aotSizeJson = fileSystem.file('test.json') + ..writeAsStringSync(aotSizeOutput); + final File precompilerTrace = fileSystem.file('trace.json') + ..writeAsStringSync('{}'); + + final Map result = await sizeAnalyzer.analyzeAotSnapshot( + outputDirectory: outputDirectory, + aotSnapshot: aotSizeJson, + precompilerTrace: precompilerTrace, + type: 'linux', + ); + + final List stdout = logger.statusText.split('\n'); + expect( + stdout, + containsAll([ + ' foo.app 12 B', + ' foo.app 12 B', + ' Dart AOT symbols accounted decompressed size 14 KB', + ' dart:core/', + ' RangeError 4 KB', + ]), + ); + expect(result['type'], 'linux'); + expect(result['precompiler-trace'], {}); + }); } diff --git a/packages/flutter_tools/test/general.shard/base/file_system_test.dart b/packages/flutter_tools/test/general.shard/base/file_system_test.dart index 04a45b3b2d..28f8fdc83f 100644 --- a/packages/flutter_tools/test/general.shard/base/file_system_test.dart +++ b/packages/flutter_tools/test/general.shard/base/file_system_test.dart @@ -15,10 +15,8 @@ import 'package:mockito/mockito.dart'; import '../../src/common.dart'; import '../../src/context.dart'; -class MockPlatform extends Mock implements Platform {} - void main() { - group('ensureDirectoryExists', () { + group('fsUtils', () { MemoryFileSystem fs; FileSystemUtils fsUtils; @@ -26,19 +24,37 @@ void main() { fs = MemoryFileSystem(); fsUtils = FileSystemUtils( fileSystem: fs, - platform: MockPlatform(), + platform: FakePlatform(), ); }); - testWithoutContext('recursively creates a directory if it does not exist', () async { + testWithoutContext('ensureDirectoryExists recursively creates a directory if it does not exist', () async { fsUtils.ensureDirectoryExists('foo/bar/baz.flx'); expect(fs.isDirectorySync('foo/bar'), true); }); - testWithoutContext('throws tool exit on failure to create', () async { + testWithoutContext('ensureDirectoryExists throws tool exit on failure to create', () async { fs.file('foo').createSync(); expect(() => fsUtils.ensureDirectoryExists('foo/bar.flx'), throwsToolExit()); }); + + testWithoutContext('getUniqueFile creates a unique file name', () async { + final File fileA = fsUtils.getUniqueFile(fs.currentDirectory, 'foo', 'json') + ..createSync(); + final File fileB = fsUtils.getUniqueFile(fs.currentDirectory, 'foo', 'json'); + + expect(fileA.path, '/foo_01.json'); + expect(fileB.path, '/foo_02.json'); + }); + + testWithoutContext('getUniqueDirectory creates a unique directory name', () async { + final Directory directoryA = fsUtils.getUniqueDirectory(fs.currentDirectory, 'foo') + ..createSync(); + final Directory directoryB = fsUtils.getUniqueDirectory(fs.currentDirectory, 'foo'); + + expect(directoryA.path, '/foo_01'); + expect(directoryB.path, '/foo_02'); + }); }); group('copyDirectorySync', () { @@ -61,7 +77,7 @@ void main() { final FileSystemUtils fsUtils = FileSystemUtils( fileSystem: sourceMemoryFs, - platform: MockPlatform(), + platform: FakePlatform(), ); fsUtils.copyDirectorySync(sourceDirectory, targetDirectory); @@ -81,7 +97,7 @@ void main() { final MemoryFileSystem fileSystem = MemoryFileSystem(); final FileSystemUtils fsUtils = FileSystemUtils( fileSystem: fileSystem, - platform: MockPlatform(), + platform: FakePlatform(), ); final Directory origin = fileSystem.directory('/origin'); origin.createSync(); diff --git a/packages/flutter_tools/test/general.shard/build_info_test.dart b/packages/flutter_tools/test/general.shard/build_info_test.dart index 9b50cf86c2..596d1f8887 100644 --- a/packages/flutter_tools/test/general.shard/build_info_test.dart +++ b/packages/flutter_tools/test/general.shard/build_info_test.dart @@ -107,6 +107,7 @@ void main() { extraGenSnapshotOptions: ['--enable-experiment=non-nullable', 'fizz'], bundleSkSLPath: 'foo/bar/baz.sksl.json', packagesPath: 'foo/.packages', + codeSizeDirectory: 'foo/code-size', ); expect(buildInfo.toEnvironmentConfig(), { @@ -119,6 +120,7 @@ void main() { 'EXTRA_GEN_SNAPSHOT_OPTIONS': '--enable-experiment%3Dnon-nullable,fizz', 'BUNDLE_SKSL_PATH': 'foo/bar/baz.sksl.json', 'PACKAGE_CONFIG': 'foo/.packages', + 'CODE_SIZE_DIRECTORY': 'foo/code-size', }); }); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/android_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/android_test.dart index 9c6859b969..151507f0ee 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/android_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/android_test.dart @@ -209,6 +209,50 @@ void main() { ProcessManager: () => processManager, }); + testUsingContext('AndroidAot provide code size information.', () async { + processManager = FakeProcessManager.list([]); + final Environment environment = Environment.test( + fileSystem.currentDirectory, + outputDir: fileSystem.directory('out')..createSync(), + defines: { + kBuildMode: 'release', + kCodeSizeDirectory: 'code_size_1', + }, + artifacts: artifacts, + processManager: processManager, + fileSystem: fileSystem, + logger: logger, + ); + processManager.addCommand(FakeCommand(command: [ + artifacts.getArtifactPath( + Artifact.genSnapshot, + platform: TargetPlatform.android_arm64, + mode: BuildMode.release, + ), + '--deterministic', + '--write-v8-snapshot-profile-to=code_size_1/snapshot.arm64-v8a.json', + '--trace-precompiler-to=code_size_1/trace.arm64-v8a.json', + '--snapshot_kind=app-aot-elf', + '--elf=${environment.buildDir.childDirectory('arm64-v8a').childFile('app.so').path}', + '--strip', + '--no-causal-async-stacks', + '--lazy-async-stacks', + environment.buildDir.childFile('app.dill').path, + ], + )); + environment.buildDir.createSync(recursive: true); + environment.buildDir.childFile('app.dill').createSync(); + environment.projectDir.childFile('.packages').writeAsStringSync('\n'); + const AndroidAot androidAot = AndroidAot(TargetPlatform.android_arm64, BuildMode.release); + + await androidAot.build(environment); + + expect(processManager.hasRemainingExpectations, false); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); + testUsingContext('kExtraGenSnapshotOptions passes values to gen_snapshot', () async { processManager = FakeProcessManager.list([]); final Environment environment = Environment.test( diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart index 86c4132b9f..4c36f2fc0c 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/dart_test.dart @@ -12,7 +12,6 @@ import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/exceptions.dart'; import 'package:flutter_tools/src/build_system/targets/common.dart'; import 'package:flutter_tools/src/build_system/targets/ios.dart'; -import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/compile.dart'; import '../../../src/common.dart'; @@ -32,10 +31,6 @@ void main() { FileSystem fileSystem; Logger logger; - setUpAll(() { - Cache.disableLocking(); - }); - setUp(() { processManager = FakeProcessManager.list([]); logger = BufferLogger.test(); @@ -47,6 +42,7 @@ void main() { kBuildMode: getNameForBuildMode(BuildMode.profile), kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm), }, + inputs: {}, artifacts: artifacts, processManager: processManager, fileSystem: fileSystem, @@ -59,6 +55,7 @@ void main() { kBuildMode: getNameForBuildMode(BuildMode.profile), kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios), }, + inputs: {}, artifacts: artifacts, processManager: processManager, fileSystem: fileSystem, @@ -357,6 +354,36 @@ void main() { expect(processManager.hasRemainingExpectations, false); }); + testUsingContext('AotElfRelease configures gen_snapshot with code size directory', () async { + androidEnvironment.defines[kCodeSizeDirectory] = 'code_size_1'; + final String build = androidEnvironment.buildDir.path; + processManager.addCommands([ + FakeCommand(command: [ + artifacts.getArtifactPath( + Artifact.genSnapshot, + platform: TargetPlatform.android_arm, + mode: BuildMode.profile, + ), + '--deterministic', + '--write-v8-snapshot-profile-to=code_size_1/snapshot.android-arm.json', + '--trace-precompiler-to=code_size_1/trace.android-arm.json', + kElfAot, + '--elf=$build/app.so', + '--strip', + '--no-sim-use-hardfp', + '--no-use-integer-division', + '--no-causal-async-stacks', + '--lazy-async-stacks', + '$build/app.dill', + ]) + ]); + androidEnvironment.buildDir.childFile('app.dill').createSync(recursive: true); + + await const AotElfRelease(TargetPlatform.android_arm).build(androidEnvironment); + + expect(processManager.hasRemainingExpectations, false); + }); + testUsingContext('AotElfProfile throws error if missing build mode', () async { androidEnvironment.defines.remove(kBuildMode); @@ -611,6 +638,88 @@ void main() { ProcessManager: () => processManager, }); + testUsingContext('AotAssemblyRelease configures gen_snapshot with code size directory', () async { + iosEnvironment.defines[kCodeSizeDirectory] = 'code_size_1'; + iosEnvironment.defines[kIosArchs] = 'arm64'; + iosEnvironment.defines[kBitcodeFlag] = 'true'; + final String build = iosEnvironment.buildDir.path; + processManager.addCommands([ + FakeCommand(command: [ + // This path is not known by the cache due to the iOS gen_snapshot split. + 'Artifact.genSnapshot.TargetPlatform.ios.profile_arm64', + '--deterministic', + '--write-v8-snapshot-profile-to=code_size_1/snapshot.arm64.json', + '--trace-precompiler-to=code_size_1/trace.arm64.json', + kAssemblyAot, + '--assembly=$build/arm64/snapshot_assembly.S', + '--strip', + '--no-causal-async-stacks', + '--lazy-async-stacks', + '$build/app.dill', + ]), + const FakeCommand(command: [ + 'xcrun', + '--sdk', + 'iphoneos', + '--show-sdk-path', + ]), + FakeCommand(command: [ + 'xcrun', + 'cc', + '-arch', + 'arm64', + '-isysroot', + '', + // Contains bitcode flag. + '-fembed-bitcode', + '-c', + '$build/arm64/snapshot_assembly.S', + '-o', + '$build/arm64/snapshot_assembly.o', + ]), + FakeCommand(command: [ + 'xcrun', + 'clang', + '-arch', + 'arm64', + '-miphoneos-version-min=9.0', + '-dynamiclib', + '-Xlinker', + '-rpath', + '-Xlinker', + '@executable_path/Frameworks', + '-Xlinker', + '-rpath', + '-Xlinker', + '@loader_path/Frameworks', + '-install_name', + '@rpath/App.framework/App', + // Contains bitcode flag. + '-fembed-bitcode', + '-isysroot', + '', + '-o', + '$build/arm64/App.framework/App', + '$build/arm64/snapshot_assembly.o', + ]), + FakeCommand(command: [ + 'lipo', + '$build/arm64/App.framework/App', + '-create', + '-output', + '$build/App.framework/App', + ]), + ]); + + await const AotAssemblyProfile().build(iosEnvironment); + + expect(processManager.hasRemainingExpectations, false); + }, overrides: { + Platform: () => macPlatform, + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); + testUsingContext('kExtraGenSnapshotOptions passes values to gen_snapshot', () async { androidEnvironment.defines[kExtraGenSnapshotOptions] = 'foo,bar,baz=2'; androidEnvironment.defines[kBuildMode] = getNameForBuildMode(BuildMode.profile); diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index d5b093fd5c..d7046dbdca 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -2,134 +2,133 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/artifacts.dart'; -import 'package:flutter_tools/src/base/build.dart'; import 'package:flutter_tools/src/base/file_system.dart'; -import 'package:flutter_tools/src/base/io.dart'; -import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; import 'package:flutter_tools/src/build_system/targets/assets.dart'; import 'package:flutter_tools/src/build_system/targets/common.dart'; import 'package:flutter_tools/src/build_system/targets/macos.dart'; import 'package:flutter_tools/src/convert.dart'; -import 'package:flutter_tools/src/globals.dart' as globals; -import 'package:flutter_tools/src/macos/xcode.dart'; -import 'package:mockito/mockito.dart'; -import 'package:process/process.dart'; import '../../../src/common.dart'; +import '../../../src/context.dart'; import '../../../src/fake_process_manager.dart'; -import '../../../src/testbed.dart'; - -const String _kInputPrefix = 'bin/cache/artifacts/engine/darwin-x64/FlutterMacOS.framework'; -const String _kOutputPrefix = 'FlutterMacOS.framework'; - -final List inputs = [ - '$_kInputPrefix/FlutterMacOS', - // Headers - '$_kInputPrefix/Headers/FlutterDartProject.h', - '$_kInputPrefix/Headers/FlutterEngine.h', - '$_kInputPrefix/Headers/FlutterViewController.h', - '$_kInputPrefix/Headers/FlutterBinaryMessenger.h', - '$_kInputPrefix/Headers/FlutterChannels.h', - '$_kInputPrefix/Headers/FlutterCodecs.h', - '$_kInputPrefix/Headers/FlutterMacros.h', - '$_kInputPrefix/Headers/FlutterPluginMacOS.h', - '$_kInputPrefix/Headers/FlutterPluginRegistrarMacOS.h', - '$_kInputPrefix/Headers/FlutterMacOS.h', - // Modules - '$_kInputPrefix/Modules/module.modulemap', - // Resources - '$_kInputPrefix/Resources/icudtl.dat', - '$_kInputPrefix/Resources/Info.plist', - // Ignore Versions folder for now - 'packages/flutter_tools/lib/src/build_system/targets/macos.dart', -]; void main() { - Testbed testbed; Environment environment; - Platform platform; + FileSystem fileSystem; + Artifacts artifacts; + FakeProcessManager processManager; setUp(() { - platform = FakePlatform(operatingSystem: 'macos', environment: {}); - testbed = Testbed(setup: () { - environment = Environment.test( - globals.fs.currentDirectory, - defines: { - kBuildMode: 'debug', - kTargetPlatform: 'darwin-x64', - }, - inputs: {}, - artifacts: MockArtifacts(), - processManager: FakeProcessManager.any(), - logger: globals.logger, - fileSystem: globals.fs, - engineVersion: '2' - ); - environment.buildDir.createSync(recursive: true); - }, overrides: { - ProcessManager: () => MockProcessManager(), - Platform: () => platform, - }); + processManager = FakeProcessManager.any(); + artifacts = Artifacts.test(); + fileSystem = MemoryFileSystem.test(); + environment = Environment.test( + fileSystem.currentDirectory, + defines: { + kBuildMode: 'debug', + kTargetPlatform: 'darwin-x64', + }, + inputs: {}, + artifacts: artifacts, + processManager: processManager, + logger: BufferLogger.test(), + fileSystem: fileSystem, + engineVersion: '2' + ); }); - test('Copies files to correct cache directory', () => testbed.run(() async { - for (final String input in inputs) { - globals.fs.file(input).createSync(recursive: true); - } - // Create output directory so we can test that it is deleted. - environment.outputDir.childDirectory(_kOutputPrefix) - .createSync(recursive: true); + testUsingContext('Copies files to correct cache directory', () async { + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'cp', + '-R', + 'Artifact.flutterMacOSFramework.debug', + '/FlutterMacOS.framework', + ], + ), + ]); + environment = Environment.test( + fileSystem.currentDirectory, + defines: { + kBuildMode: 'debug', + kTargetPlatform: 'darwin-x64', + }, + inputs: {}, + artifacts: artifacts, + processManager: processManager, + logger: BufferLogger.test(), + fileSystem: fileSystem, + engineVersion: '2' + ); - when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async { - final List arguments = invocation.positionalArguments.first as List; - final String sourcePath = arguments[arguments.length - 2]; - final String targetPath = arguments.last; - final Directory source = globals.fs.directory(sourcePath); - final Directory target = globals.fs.directory(targetPath); + final Directory cacheDirectory = fileSystem.directory( + artifacts.getArtifactPath( + Artifact.flutterMacOSFramework, + mode: BuildMode.debug, + )) + ..createSync(); + cacheDirectory.childFile('dummy').createSync(); + environment.buildDir.createSync(recursive: true); + environment.outputDir.createSync(recursive: true); - for (final FileSystemEntity entity in source.listSync(recursive: true)) { - if (entity is File) { - final String relative = globals.fs.path.relative(entity.path, from: source.path); - final String destination = globals.fs.path.join(target.path, relative); - if (!globals.fs.file(destination).parent.existsSync()) { - globals.fs.file(destination).parent.createSync(); - } - entity.copySync(destination); - } - } - return FakeProcessResult()..exitCode = 0; - }); await const DebugUnpackMacOS().build(environment); - expect(globals.fs.directory(_kOutputPrefix).existsSync(), true); - for (final String path in inputs) { - expect(globals.fs.file(path.replaceFirst(_kInputPrefix, _kOutputPrefix)), exists); - } - })); + expect(processManager.hasRemainingExpectations, false); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); - test('debug macOS application fails if App.framework missing', () => testbed.run(() async { - final String inputKernel = globals.fs.path.join(environment.buildDir.path, 'app.dill'); - globals.fs.file(inputKernel) + testUsingContext('debug macOS application fails if App.framework missing', () async { + fileSystem.directory( + artifacts.getArtifactPath( + Artifact.flutterMacOSFramework, + mode: BuildMode.debug, + )) + .createSync(); + final String inputKernel = fileSystem.path.join(environment.buildDir.path, 'app.dill'); + fileSystem.file(inputKernel) ..createSync(recursive: true) ..writeAsStringSync('testing'); expect(() async => await const DebugMacOSBundleFlutterAssets().build(environment), throwsException); - })); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); - test('debug macOS application creates correctly structured framework', () => testbed.run(() async { + testUsingContext('debug macOS application creates correctly structured framework', () async { + fileSystem.directory( + artifacts.getArtifactPath( + Artifact.flutterMacOSFramework, + mode: BuildMode.debug, + )) + .createSync(); environment.inputs[kBundleSkSLPath] = 'bundle.sksl'; - globals.fs.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin') - .createSync(recursive: true); - globals.fs.file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') - .createSync(recursive: true); - globals.fs.file('${environment.buildDir.path}/App.framework/App') + fileSystem.file( + artifacts.getArtifactPath( + Artifact.vmSnapshotData, + platform: TargetPlatform.darwin_x64, + mode: BuildMode.debug, + )).createSync(recursive: true); + fileSystem.file( + artifacts.getArtifactPath( + Artifact.isolateSnapshotData, + platform: TargetPlatform.darwin_x64, + mode: BuildMode.debug, + )).createSync(recursive: true); + fileSystem.file('${environment.buildDir.path}/App.framework/App') .createSync(recursive: true); // sksl bundle - globals.fs.file('bundle.sksl').writeAsStringSync(json.encode( + fileSystem.file('bundle.sksl').writeAsStringSync(json.encode( { 'engineRevision': '2', 'platform': 'ios', @@ -140,70 +139,76 @@ void main() { )); final String inputKernel = '${environment.buildDir.path}/app.dill'; - globals.fs.file(inputKernel) + fileSystem.file(inputKernel) ..createSync(recursive: true) ..writeAsStringSync('testing'); await const DebugMacOSBundleFlutterAssets().build(environment); - expect(globals.fs.file( + expect(fileSystem.file( 'App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin').readAsStringSync(), 'testing', ); - expect(globals.fs.file( + expect(fileSystem.file( 'App.framework/Versions/A/Resources/Info.plist').readAsStringSync(), contains('io.flutter.flutter.app'), ); - expect(globals.fs.file( + expect(fileSystem.file( 'App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data'), exists, ); - expect(globals.fs.file( + expect(fileSystem.file( 'App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data'), exists, ); - final File skslFile = globals.fs.file('App.framework/Versions/A/Resources/flutter_assets/io.flutter.shaders.json'); + final File skslFile = fileSystem.file('App.framework/Versions/A/Resources/flutter_assets/io.flutter.shaders.json'); expect(skslFile, exists); expect(skslFile.readAsStringSync(), '{"data":{"A":"B"}}'); - })); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); - test('release/profile macOS application has no blob or precompiled runtime', () => testbed.run(() async { - globals.fs.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin') + testUsingContext('release/profile macOS application has no blob or precompiled runtime', () async { + fileSystem.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin') .createSync(recursive: true); - globals.fs.file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') + fileSystem.file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') .createSync(recursive: true); - globals.fs.file('${environment.buildDir.path}/App.framework/App') + fileSystem.file('${environment.buildDir.path}/App.framework/App') .createSync(recursive: true); await const ProfileMacOSBundleFlutterAssets().build(environment..defines[kBuildMode] = 'profile'); - expect(globals.fs.file( + expect(fileSystem.file( 'App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin'), isNot(exists), ); - expect(globals.fs.file( + expect(fileSystem.file( 'App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data'), isNot(exists), ); - expect(globals.fs.file( + expect(fileSystem.file( 'App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data'), isNot(exists), ); - })); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); - test('release/profile macOS application updates when App.framework updates', () => testbed.run(() async { - globals.fs.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin') + testUsingContext('release/profile macOS application updates when App.framework updates', () async { + fileSystem.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin') .createSync(recursive: true); - globals.fs.file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') + fileSystem.file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin') .createSync(recursive: true); - final File inputFramework = globals.fs.file(globals.fs.path.join(environment.buildDir.path, 'App.framework', 'App')) + final File inputFramework = fileSystem.file(fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App')) ..createSync(recursive: true) ..writeAsStringSync('ABC'); await const ProfileMacOSBundleFlutterAssets().build(environment..defines[kBuildMode] = 'profile'); - final File outputFramework = globals.fs.file(globals.fs.path.join(environment.outputDir.path, 'App.framework', 'App')); + final File outputFramework = fileSystem.file(fileSystem.path.join(environment.outputDir.path, 'App.framework', 'App')); expect(outputFramework.readAsStringSync(), 'ABC'); @@ -211,23 +216,8 @@ void main() { await const ProfileMacOSBundleFlutterAssets().build(environment..defines[kBuildMode] = 'profile'); expect(outputFramework.readAsStringSync(), 'DEF'); - })); -} - -class MockProcessManager extends Mock implements ProcessManager {} -class MockGenSnapshot extends Mock implements GenSnapshot {} -class MockXcode extends Mock implements Xcode {} -class MockArtifacts extends Mock implements Artifacts {} -class FakeProcessResult implements ProcessResult { - @override - int exitCode; - - @override - int pid = 0; - - @override - String stderr = ''; - - @override - String stdout = ''; + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); } diff --git a/packages/flutter_tools/test/integration.shard/analyze_size_apk_test.dart b/packages/flutter_tools/test/integration.shard/analyze_size_apk_test.dart deleted file mode 100644 index abbf6a882e..0000000000 --- a/packages/flutter_tools/test/integration.shard/analyze_size_apk_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_tools/src/base/io.dart'; -import 'package:flutter_tools/src/base/platform.dart'; -import 'package:process/process.dart'; -import 'package:flutter_tools/src/globals.dart' as globals; - -import '../src/common.dart'; - -const String debugMessage = 'A summary of your APK analysis can be found at: '; - -void main() { - test('--analyze-size flag produces expected output on hello_world', () async { - final String flutterBin = globals.fs.path.join(getFlutterRoot(), 'bin', 'flutter'); - final ProcessResult result = await const LocalProcessManager().run([ - flutterBin, - 'build', - 'apk', - '--analyze-size', - ], workingDirectory: globals.fs.path.join(getFlutterRoot(), 'examples', 'hello_world')); - - print(result.stdout); - print(result.stderr); - expect(result.stdout.toString(), contains('app-release.apk (total compressed)')); - - final String line = result.stdout.toString() - .split('\n') - .firstWhere((String line) => line.contains(debugMessage)); - - expect(globals.fs.file(globals.fs.path.join(line.split(debugMessage).last.trim())).existsSync(), true); - expect(result.exitCode, 0); - }, skip: const LocalPlatform().isWindows); // Not yet supported on Windows -} diff --git a/packages/flutter_tools/test/integration.shard/analyze_size_test.dart b/packages/flutter_tools/test/integration.shard/analyze_size_test.dart new file mode 100644 index 0000000000..1227959d5c --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/analyze_size_test.dart @@ -0,0 +1,82 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:process/process.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; + +import '../src/common.dart'; + +const String apkDebugMessage = 'A summary of your APK analysis can be found at: '; +const String iosDebugMessage = 'A summary of your iOS bundle analysis can be found at: '; + +void main() { + test('--analyze-size flag produces expected output on hello_world for Android', () async { + final String woringDirectory = globals.fs.path.join(getFlutterRoot(), 'examples', 'hello_world'); + final String flutterBin = globals.fs.path.join(getFlutterRoot(), 'bin', 'flutter'); + final ProcessResult result = await const LocalProcessManager().run([ + flutterBin, + 'build', + 'apk', + '--analyze-size', + '--target-platform=android-arm64' + ], workingDirectory: globals.fs.path.join(getFlutterRoot(), 'examples', 'hello_world')); + + print(result.stdout); + print(result.stderr); + expect(result.stdout.toString(), contains('app-release.apk (total compressed)')); + + final String line = result.stdout.toString() + .split('\n') + .firstWhere((String line) => line.contains(apkDebugMessage)); + + final String outputFilePath = line.split(apkDebugMessage).last.trim(); + expect(globals.fs.file(globals.fs.path.join(woringDirectory, outputFilePath)), exists); + expect(result.exitCode, 0); + }); + + test('--analyze-size flag produces expected output on hello_world for iOS', () async { + final String woringDirectory = globals.fs.path.join(getFlutterRoot(), 'examples', 'hello_world'); + final String flutterBin = globals.fs.path.join(getFlutterRoot(), 'bin', 'flutter'); + final ProcessResult result = await const LocalProcessManager().run([ + flutterBin, + 'build', + 'ios', + '--analyze-size', + '--no-codesign', + ], workingDirectory: woringDirectory); + + print(result.stdout); + print(result.stderr); + expect(result.stdout.toString(), contains('Dart AOT symbols accounted decompressed size')); + + final String line = result.stdout.toString() + .split('\n') + .firstWhere((String line) => line.contains(iosDebugMessage)); + + final String outputFilePath = line.split(iosDebugMessage).last.trim(); + expect(globals.fs.file(globals.fs.path.join(woringDirectory, outputFilePath)), exists); + expect(result.exitCode, 0); + }, skip: !const LocalPlatform().isMacOS); // Only supported on macOS + + test('--analyze-size is only supported in release mode', () async { + final String flutterBin = globals.fs.path.join(getFlutterRoot(), 'bin', 'flutter'); + final ProcessResult result = await const LocalProcessManager().run([ + flutterBin, + 'build', + 'apk', + '--analyze-size', + '--target-platform=android-arm64', + '--debug', + ], workingDirectory: globals.fs.path.join(getFlutterRoot(), 'examples', 'hello_world')); + + print(result.stdout); + print(result.stderr); + expect(result.stderr.toString(), contains('--analyze-size can only be used on release builds')); + + expect(result.exitCode, 1); + }); +}