blob: 4f9898ccc545b2cd6a904879edf11272f594e6da [file] [log] [blame]
// 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;
}