// 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:meta/meta.dart'; import 'package:vm_snapshot_analysis/treemap.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. 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; static const String aotSnapshotFileName = 'aot-snapshot.json'; 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, @required File aotSnapshot, }) 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); // 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) { _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); } } logger.printStatus('▒' * tableWidth); Map<String, dynamic> apkAnalysisJson = apkAnalysisRoot.toJson(); apkAnalysisJson['type'] = 'apk'; // TODO(peterdjlee): Add aot snapshot for all platforms. assert(_appFilename != null); apkAnalysisJson = _addAotSnapshotDataToApkAnalysis( apkAnalysisJson: apkAnalysisJson, path: 'lib/arm64-v8a/$_appFilename (Dart AOT)'.split('/'), // Pass in a list of paths by splitting with '/'. aotSnapshotJson: processedAotSnapshotJson, ); 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) { 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) { continue; } const int sizeGroupIndex = 1; const int nameGroupIndex = 2; pathsToSize[match.group(nameGroupIndex).split('/')] = int.parse(match.group(sizeGroupIndex)); } 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); if (childWithPathAsName == null) { childWithPathAsName = _SymbolNode(path); if (matchesPattern(path, pattern: appFilenamePattern) != null) { _appFilename = path; childWithPathAsName.name += ' (Dart AOT)'; } else if (path == 'libflutter.so') { childWithPathAsName.name += ' (Flutter Engine)'; } currentNode.addChild(childWithPathAsName); } childWithPathAsName.addSize(pathsToSize[paths]); currentNode = childWithPathAsName; } currentNode = rootNode; } return rootNode; } /// Prints all children paths for the lib/ directory in an APK. /// /// A brief summary of aot snapshot is printed under 'lib/arm64-v8a/$_appFilename'. void _printLibChildrenPaths( _SymbolNode currentNode, String totalPath, _SymbolNode aotSnapshotJsonRoot, ) { totalPath += currentNode.name; assert(_appFilename != null); if (currentNode.children.isNotEmpty && currentNode.name != '$_appFilename (Dart AOT)') { for (final _SymbolNode child in currentNode.children) { _printLibChildrenPaths(child, '$totalPath/', aotSnapshotJsonRoot); } } 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); } } } /// 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}) { _printEntitySize( 'Dart AOT symbols accounted decompressed size', byteSize: aotSnapshotRoot.byteSize, level: 3, ); final List<_SymbolNode> sortedSymbols = aotSnapshotRoot.children.toList() ..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); } } /// Adds breakdown of aot snapshot data as the children of the node at the given path. Map<String, dynamic> _addAotSnapshotDataToApkAnalysis({ @required Map<String, dynamic> apkAnalysisJson, @required List<String> path, @required Map<String, dynamic> aotSnapshotJson, }) { Map<String, dynamic> currentLevel = apkAnalysisJson; while (path.isNotEmpty) { final List<Map<String, dynamic>> children = currentLevel['children'] as List<Map<String, dynamic>>; final Map<String, dynamic> childWithPathAsName = children.firstWhere( (Map<String, dynamic> child) => child['n'] as String == path.first, ); path.removeAt(0); currentLevel = childWithPathAsName; } currentLevel['children'] = aotSnapshotJson['children']; return apkAnalysisJson; } /// 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, }) { final bool emphasis = level <= 1; final String formattedSize = _prettyPrintBytes(byteSize); TerminalColor color = TerminalColor.green; if (formattedSize.endsWith('MB')) { color = TerminalColor.cyan; } else if (formattedSize.endsWith('KB')) { color = TerminalColor.yellow; } final int spaceInBetween = tableWidth - level * 2 - entityName.length - formattedSize.length; logger.printStatus( entityName + ' ' * spaceInBetween, newline: false, emphasis: emphasis, indent: level * 2, ); logger.printStatus(formattedSize, color: showColor ? color : null); } String _prettyPrintBytes(int numBytes) { const int kB = 1024; const int mB = kB * 1024; if (numBytes < kB) { return '$numBytes B'; } else if (numBytes < mB) { return '${(numBytes / kB).round()} KB'; } else { return '${(numBytes / mB).round()} MB'; } } _SymbolNode _parseAotSnapshot(Map<String, dynamic> aotSnapshotJson) { final bool isLeafNode = aotSnapshotJson['children'] == null; if (!isLeafNode) { return _buildNodeWithChildren(aotSnapshotJson); } else { // TODO(peterdjlee): Investigate why there are leaf nodes with size of null. final int byteSize = aotSnapshotJson['value'] as int; if (byteSize == null) { return null; } return _buildNode(aotSnapshotJson, byteSize); } } _SymbolNode _buildNode( Map<String, dynamic> aotSnapshotJson, int byteSize, { List<_SymbolNode> children = const <_SymbolNode>[], }) { final String name = aotSnapshotJson['n'] as String; final Map<String, _SymbolNode> childrenMap = <String, _SymbolNode>{}; for (final _SymbolNode child in children) { childrenMap[child.name] = child; } return _SymbolNode( name, byteSize: byteSize, )..addAllChildren(children); } /// Builds a node by recursively building all of its children first /// in order to calculate the sum of its children's sizes. _SymbolNode _buildNodeWithChildren(Map<String, dynamic> aotSnapshotJson) { final List<dynamic> rawChildren = aotSnapshotJson['children'] as List<dynamic>; final List<_SymbolNode> symbolNodeChildren = <_SymbolNode>[]; int totalByteSize = 0; // Given a child, build its subtree. for (final dynamic child in rawChildren) { final _SymbolNode childTreemapNode = _parseAotSnapshot(child as Map<String, dynamic>); symbolNodeChildren.add(childTreemapNode); totalByteSize += childTreemapNode.byteSize; } // If none of the children matched the diff tree type if (totalByteSize == 0) { return null; } else { return _buildNode( aotSnapshotJson, totalByteSize, children: symbolNodeChildren, ); } } } /// A node class that represents a single symbol for AOT size snapshots. class _SymbolNode { _SymbolNode( this.name, { this.byteSize = 0, }) : assert(name != null), assert(byteSize != null), _children = <String, _SymbolNode>{}; /// The human friendly identifier for this node. String name; int byteSize; void addSize(int sizeToBeAdded) { byteSize += sizeToBeAdded; } _SymbolNode get parent => _parent; _SymbolNode _parent; Iterable<_SymbolNode> get children => _children.values; final Map<String, _SymbolNode> _children; _SymbolNode childByName(String name) => _children[name]; _SymbolNode addChild(_SymbolNode child) { assert(child.parent == null); assert(!_children.containsKey(child.name), 'Cannot add duplicate child key ${child.name}'); child._parent = this; _children[child.name] = child; return child; } void addAllChildren(List<_SymbolNode> children) { children.forEach(addChild); } Map<String, dynamic> toJson() { final Map<String, dynamic> json = <String, dynamic>{ 'n': name, 'value': byteSize }; final List<Map<String, dynamic>> childrenAsJson = <Map<String, dynamic>>[]; for (final _SymbolNode child in children) { childrenAsJson.add(child.toJson()); } if (childrenAsJson.isNotEmpty) { json['children'] = childrenAsJson; } return json; } } /// Matches `pattern` against the entirety of `string`. @visibleForTesting Match matchesPattern(String string, {@required Pattern pattern}) { final Match match = pattern.matchAsPrefix(string); return (match != null && match.end == string.length) ? match : null; }