[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)
This commit is contained in:
Jonah Williams 2020-08-25 10:00:24 -07:00 committed by GitHub
parent dd0c881275
commit 059de1537e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1013 additions and 398 deletions

View File

@ -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}" \

View File

@ -19,6 +19,7 @@ Future<void> main(List<String> arguments) async {
final String flutterRoot = Platform.environment['FLUTTER_ROOT'];
final String flutterTarget = Platform.environment['FLUTTER_TARGET']
?? pathJoin(<String>['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)

View File

@ -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}" \

View File

@ -628,6 +628,10 @@ class FlutterPlugin implements Plugin<Project> {
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<Project> {
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}"
}

View File

@ -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'

View File

@ -357,6 +357,9 @@ Future<void> 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<void> 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<void> 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<String, Object> 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<void> _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<String, Object> 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()].

View File

@ -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<Map<String, dynamic>> analyzeApkSizeAndAotSnapshot({
@required File apk,
static const int _kAotSizeMaxDepth = 2;
static const int _kZipSizeMaxDepth = 1;
/// Analyze the [aotSnapshot] in an uncompressed output directory.
Future<Map<String, dynamic>> 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(<String>[
'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<String, dynamic> 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<String, dynamic> 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<String, Object>,
);
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<Map<String, dynamic>> 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<String, dynamic> 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<String, dynamic> 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<String, Object>,
);
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<List<String>, int> pathsToSize = <List<String>, 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<List<String>, int> pathsToSize = <List<String>, int>{};
for (final File file in directory.listSync(recursive: true).whereType<File>()) {
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<String> path = fileSystem.path.split(
fileSystem.path.relative(file.path, from: relativeTo));
pathsToSize[path] = file.lengthSync();
}
return _buildSymbolTree(pathsToSize);
}
final _SymbolNode rootNode = _SymbolNode('Root');
List<String> _locatedAotFilePath;
List<String> _buildNodeName(_SymbolNode start, _SymbolNode parent) {
final List<String> results = <String>[start.name];
while (parent != null && parent.name != 'Root') {
results.insert(0, parent.name);
parent = parent.parent;
}
return results;
}
_SymbolNode _buildSymbolTree(Map<List<String>, int> pathsToSize) {
final _SymbolNode rootNode = _SymbolNode('Root');
_SymbolNode currentNode = rootNode;
for (final List<String> 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<String> 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<String, dynamic> _addAotSnapshotDataToApkAnalysis({
Map<String, dynamic> _addAotSnapshotDataToAnalysis({
@required Map<String, dynamic> apkAnalysisJson,
@required List<String> path,
@required Map<String, dynamic> aotSnapshotJson,
@required Map<String, dynamic> precompilerTrace,
}) {
Map<String, dynamic> currentLevel = apkAnalysisJson;
currentLevel['precompiler-trace'] = precompilerTrace;
while (path.isNotEmpty) {
final List<Map<String, dynamic>> children = currentLevel['children'] as List<Map<String, dynamic>>;
final Map<String, dynamic> childWithPathAsName = children.firstWhere(
@ -223,14 +285,16 @@ class SizeAnalyzer {
return apkAnalysisJson;
}
List<String> _leadingPaths = <String>[];
/// 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<String> 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);
}

View File

@ -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;
}
}

View File

@ -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<String> defines) {
/// Dart defines are encoded inside [environmentDefines] as a comma-separated list.
List<String> decodeDartDefines(Map<String, String> environmentDefines, String key) {
if (!environmentDefines.containsKey(key) || environmentDefines[key].isEmpty) {
return const <String>[];
return <String>[];
}
return environmentDefines[key]
.split(',')

View File

@ -232,6 +232,19 @@ class AndroidAot extends AotElfBase {
final List<String> 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,

View File

@ -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,

View File

@ -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<DarwinArch> iosArchs = environment.defines[kIosArchs]
final List<DarwinArch> 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<Future<int>> pending = <Future<int>>[];
for (final DarwinArch iosArch in iosArchs) {
for (final DarwinArch darwinArch in darwinArchs) {
final List<String> archExtraGenSnapshotOptions = List<String>.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<int> 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(<String>[
'lipo',
...iosArchs.map((DarwinArch iosArch) =>
...darwinArchs.map((DarwinArch iosArch) =>
environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')),
'-create',
'-output',

View File

@ -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<File> inputs = globals.fs.directory(basePath)
final List<File> inputs = environment.fileSystem.directory(basePath)
.listSync(recursive: true)
.whereType<File>()
.toList();
final List<File> 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(<String>['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<void> 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<String> 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);
}

View File

@ -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: <String>['android-arm', 'android-arm64', 'android-x64'],

View File

@ -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<Directory>()
.firstWhere((Directory directory) {
return globals.fs.path.extension(directory.path) == '.app';
});
final Map<String, Object> 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}.');
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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<void> 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<void> 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<String, Object> 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<void> _runCmake(String buildModeName, Directory sourceDir, Directory buildDir) async {

View File

@ -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<void> 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<void> 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<Directory>()
.firstWhere((Directory directory) {
return globals.fs.path.extension(directory.path) == '.app';
});
final Map<String, Object> 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));
}

View File

@ -609,7 +609,9 @@ abstract class FlutterCommand extends Command<void> {
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<void> {
}
}
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<void> {
);
}
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<void> {
performanceMeasurementFile: performanceMeasurementFile,
packagesPath: globalResults['packages'] as String ?? '.packages',
nullSafetyMode: nullSafetyMode,
analyzeSize: analyzeSize,
codeSizeDirectory: codeSizeDirectory,
);
}

View File

@ -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<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {
String target,
VisualStudio visualStudioOverride,
SizeAnalyzer sizeAnalyzer,
}) async {
if (!windowsProject.cmakeFile.existsSync()) {
throwToolExit(
@ -75,6 +78,29 @@ Future<void> 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<String, Object> 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<void> _runCmakeGeneration(String cmakePath, Directory buildDir, Directory sourceDir) async {

View File

@ -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: <String>[
'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(<FakeCommand>[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<int>.filled(100, 0)))
..addFile(ArchiveFile('META-INF/CERT.RSA', 10, List<int>.filled(10, 0)))
..addFile(ArchiveFile('META-INF/CERT.SF', 10, List<int>.filled(10, 0)))
..addFile(ArchiveFile('lib/arm64-v8a/libxyzzyapp.so', 50, List<int>.filled(50, 0)))
..addFile(ArchiveFile('lib/arm64-v8a/libflutter.so', 50, List<int>.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<String, dynamic> result = await sizeAnalyzer.analyzeApkSizeAndAotSnapshot(apk: apk, aotSnapshot: aotSizeJson);
final File precompilerTrace = fileSystem.file('trace.json')
..writeAsStringSync('{}');
final Map<String, dynamic> result = await sizeAnalyzer.analyzeZipSizeAndAotSnapshot(
zipFile: apk,
aotSnapshot: aotSizeJson,
precompilerTrace: precompilerTrace,
kind: 'apk',
);
expect(result['type'], contains('apk'));
expect(result['type'], 'apk');
final Map<String, dynamic> androidManifestMap = result['children'][0] as Map<String, dynamic>;
expect(androidManifestMap['n'], equals('AndroidManifest.xml'));
expect(androidManifestMap['value'], equals(2592));
expect(androidManifestMap['n'], 'AndroidManifest.xml');
expect(androidManifestMap['value'], 6);
final Map<String, dynamic> metaInfMap = result['children'][1] as Map<String, dynamic>;
expect(metaInfMap['n'], equals('META-INF'));
expect(metaInfMap['value'], equals(15622));
expect(metaInfMap['n'], 'META-INF');
expect(metaInfMap['value'], 10);
final Map<String, dynamic> certRsaMap = metaInfMap['children'][0] as Map<String, dynamic>;
expect(certRsaMap['n'], equals('CERT.RSA'));
expect(certRsaMap['value'], equals(1092));
expect(certRsaMap['n'], 'CERT.RSA');
expect(certRsaMap['value'], 5);
final Map<String, dynamic> certSfMap = metaInfMap['children'][1] as Map<String, dynamic>;
expect(certSfMap['n'], equals('CERT.SF'));
expect(certSfMap['value'], equals(14530));
expect(certSfMap['n'], 'CERT.SF');
expect(certSfMap['value'], 5);
final Map<String, dynamic> libMap = result['children'][2] as Map<String, dynamic>;
expect(libMap['n'], equals('lib'));
expect(libMap['value'], equals(29060));
expect(libMap['n'], 'lib');
expect(libMap['value'], 12);
final Map<String, dynamic> arm64Map = libMap['children'][0] as Map<String, dynamic>;
expect(arm64Map['n'], equals('arm64-v8a'));
expect(arm64Map['value'], equals(29060));
expect(arm64Map['n'], 'arm64-v8a');
expect(arm64Map['value'], 12);
final Map<String, dynamic> libAppMap = arm64Map['children'][0] as Map<String, dynamic>;
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<String, dynamic> internalMap = libAppMap['children'][0] as Map<String, dynamic>;
final Map<String, dynamic> skipMap = internalMap['children'][0] as Map<String, dynamic>;
expect(skipMap['n'], 'skip');
@ -140,41 +128,92 @@ void main() {
expect(allocateMap['n'], 'Allocate ArgumentError');
expect(allocateMap['value'], 4650);
final Map<String, dynamic> libFlutterMap = arm64Map['children'][1] as Map<String, dynamic>;
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'], <String, Object>{});
});
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<int>.filled(100, 0)))
..addFile(ArchiveFile('META-INF/CERT.RSA', 10, List<int>.filled(10, 0)))
..addFile(ArchiveFile('META-INF/CERT.SF', 10, List<int>.filled(10, 0)))
..addFile(ArchiveFile('lib/arm64-v8a/libxyzzyapp.so', 50, List<int>.filled(50, 0)))
..addFile(ArchiveFile('lib/arm64-v8a/libflutter.so', 50, List<int>.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<String> stdout = logger.statusText.split('\n');
expect(
stdout,
containsAll(<String>[
' 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<String, Object> result = await sizeAnalyzer.analyzeAotSnapshot(
outputDirectory: outputDirectory,
aotSnapshot: aotSizeJson,
precompilerTrace: precompilerTrace,
type: 'linux',
);
final List<String> stdout = logger.statusText.split('\n');
expect(
stdout,
containsAll(<String>[
' 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'], <String, Object>{});
});
}

View File

@ -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();

View File

@ -107,6 +107,7 @@ void main() {
extraGenSnapshotOptions: <String>['--enable-experiment=non-nullable', 'fizz'],
bundleSkSLPath: 'foo/bar/baz.sksl.json',
packagesPath: 'foo/.packages',
codeSizeDirectory: 'foo/code-size',
);
expect(buildInfo.toEnvironmentConfig(), <String, String>{
@ -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',
});
});

View File

@ -209,6 +209,50 @@ void main() {
ProcessManager: () => processManager,
});
testUsingContext('AndroidAot provide code size information.', () async {
processManager = FakeProcessManager.list(<FakeCommand>[]);
final Environment environment = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('out')..createSync(),
defines: <String, String>{
kBuildMode: 'release',
kCodeSizeDirectory: 'code_size_1',
},
artifacts: artifacts,
processManager: processManager,
fileSystem: fileSystem,
logger: logger,
);
processManager.addCommand(FakeCommand(command: <String>[
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: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('kExtraGenSnapshotOptions passes values to gen_snapshot', () async {
processManager = FakeProcessManager.list(<FakeCommand>[]);
final Environment environment = Environment.test(

View File

@ -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(<FakeCommand>[]);
logger = BufferLogger.test();
@ -47,6 +42,7 @@ void main() {
kBuildMode: getNameForBuildMode(BuildMode.profile),
kTargetPlatform: getNameForTargetPlatform(TargetPlatform.android_arm),
},
inputs: <String, String>{},
artifacts: artifacts,
processManager: processManager,
fileSystem: fileSystem,
@ -59,6 +55,7 @@ void main() {
kBuildMode: getNameForBuildMode(BuildMode.profile),
kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios),
},
inputs: <String, String>{},
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>[
FakeCommand(command: <String>[
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>[
FakeCommand(command: <String>[
// 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: <String>[
'xcrun',
'--sdk',
'iphoneos',
'--show-sdk-path',
]),
FakeCommand(command: <String>[
'xcrun',
'cc',
'-arch',
'arm64',
'-isysroot',
'',
// Contains bitcode flag.
'-fembed-bitcode',
'-c',
'$build/arm64/snapshot_assembly.S',
'-o',
'$build/arm64/snapshot_assembly.o',
]),
FakeCommand(command: <String>[
'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: <String>[
'lipo',
'$build/arm64/App.framework/App',
'-create',
'-output',
'$build/App.framework/App',
]),
]);
await const AotAssemblyProfile().build(iosEnvironment);
expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{
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);

View File

@ -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<String> inputs = <String>[
'$_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: <String, String>{});
testbed = Testbed(setup: () {
environment = Environment.test(
globals.fs.currentDirectory,
defines: <String, String>{
kBuildMode: 'debug',
kTargetPlatform: 'darwin-x64',
},
inputs: <String, String>{},
artifacts: MockArtifacts(),
processManager: FakeProcessManager.any(),
logger: globals.logger,
fileSystem: globals.fs,
engineVersion: '2'
);
environment.buildDir.createSync(recursive: true);
}, overrides: <Type, Generator>{
ProcessManager: () => MockProcessManager(),
Platform: () => platform,
});
processManager = FakeProcessManager.any();
artifacts = Artifacts.test();
fileSystem = MemoryFileSystem.test();
environment = Environment.test(
fileSystem.currentDirectory,
defines: <String, String>{
kBuildMode: 'debug',
kTargetPlatform: 'darwin-x64',
},
inputs: <String, String>{},
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(<FakeCommand>[
const FakeCommand(
command: <String>[
'cp',
'-R',
'Artifact.flutterMacOSFramework.debug',
'/FlutterMacOS.framework',
],
),
]);
environment = Environment.test(
fileSystem.currentDirectory,
defines: <String, String>{
kBuildMode: 'debug',
kTargetPlatform: 'darwin-x64',
},
inputs: <String, String>{},
artifacts: artifacts,
processManager: processManager,
logger: BufferLogger.test(),
fileSystem: fileSystem,
engineVersion: '2'
);
when(globals.processManager.run(any)).thenAnswer((Invocation invocation) async {
final List<String> arguments = invocation.positionalArguments.first as List<String>;
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: <Type, Generator>{
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: <Type, Generator>{
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(
<String, Object>{
'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: <Type, Generator>{
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: <Type, Generator>{
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: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
}

View File

@ -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(<String>[
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
}

View File

@ -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(<String>[
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(<String>[
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(<String>[
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);
});
}