blob: 36fe57ce4ea6a87831c65bab9995b6fae1a2588b [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:archive/archive.dart';
import 'package:archive/archive_io.dart';
import 'package:meta/meta.dart';
import 'package:vm_snapshot_analysis/treemap.dart';
import '../convert.dart';
import '../reporting/reporting.dart';
import 'common.dart';
import 'file_system.dart';
import 'logger.dart';
import 'terminal.dart';
/// A class to analyze APK and AOT snapshot and generate a breakdown of the data.
class SizeAnalyzer {
SizeAnalyzer({
required FileSystem fileSystem,
required Logger logger,
required Usage flutterUsage,
Pattern appFilenamePattern = 'libapp.so',
}) : _flutterUsage = flutterUsage,
_fileSystem = fileSystem,
_logger = logger,
_appFilenamePattern = appFilenamePattern;
final FileSystem _fileSystem;
final Logger _logger;
final Pattern _appFilenamePattern;
final Usage _flutterUsage;
String? _appFilename;
static const String aotSnapshotFileName = 'aot-snapshot.json';
static const int tableWidth = 80;
static const int _kAotSizeMaxDepth = 2;
static const int _kZipSizeMaxDepth = 1;
/// Analyze the [aotSnapshot] in an uncompressed output directory.
Future<Map<String, Object?>> analyzeAotSnapshot({
required Directory outputDirectory,
required File aotSnapshot,
required File precompilerTrace,
required String type,
String? excludePath,
}) async {
_logger.printStatus('▒' * tableWidth);
_logger.printStatus('━' * tableWidth);
final _SymbolNode aotAnalysisJson = _parseDirectory(
outputDirectory,
outputDirectory.parent.path,
excludePath,
);
// Convert an AOT snapshot file into a map.
final Object? decodedAotSnapshot = json.decode(aotSnapshot.readAsStringSync());
if (decodedAotSnapshot == null) {
throwToolExit('AOT snapshot is invalid for analysis');
}
final Map<String, Object?> processedAotSnapshotJson = treemapFromJson(decodedAotSnapshot);
final _SymbolNode? aotSnapshotJsonRoot = _parseAotSnapshot(processedAotSnapshotJson);
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 == _fileSystem.path.basename(outputDirectory.path) && aotSnapshotJsonRoot != null) {
_printLibChildrenPaths(firstLevelPath, '', aotSnapshotJsonRoot, _kAotSizeMaxDepth, 0);
}
}
_logger.printStatus('▒' * tableWidth);
Map<String, Object?> 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?>? ?? <String, Object?>{},
);
assert(_appFilename != null);
CodeSizeEvent(type, flutterUsage: _flutterUsage).send();
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, Object?>> 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 Object? decodedAotSnapshot = json.decode(aotSnapshot.readAsStringSync());
if (decodedAotSnapshot == null) {
throwToolExit('AOT snapshot is invalid for analysis');
}
final Map<String, Object?> processedAotSnapshotJson = treemapFromJson(decodedAotSnapshot);
final _SymbolNode? aotSnapshotJsonRoot = _parseAotSnapshot(processedAotSnapshotJson);
if (aotSnapshotJsonRoot != null) {
for (final _SymbolNode firstLevelPath in apkAnalysisRoot.children) {
_printLibChildrenPaths(firstLevelPath, '', aotSnapshotJsonRoot, _kZipSizeMaxDepth, 0);
}
}
_logger.printStatus('▒' * tableWidth);
Map<String, Object?> apkAnalysisJson = apkAnalysisRoot.toJson();
apkAnalysisJson['type'] = kind;
assert(_appFilename != null);
apkAnalysisJson = _addAotSnapshotDataToAnalysis(
apkAnalysisJson: apkAnalysisJson,
path: _locatedAotFilePath,
aotSnapshotJson: processedAotSnapshotJson,
precompilerTrace: json.decode(precompilerTrace.readAsStringSync()) as Map<String, Object?>? ?? <String, Object?>{},
);
CodeSizeEvent(kind, flutterUsage: _flutterUsage).send();
return apkAnalysisJson;
}
_SymbolNode _parseUnzipFile(File zipFile) {
final Archive archive = ZipDecoder().decodeBytes(zipFile.readAsBytesSync());
final Map<List<String>, int> pathsToSize = <List<String>, int>{};
for (final ArchiveFile archiveFile in archive.files) {
final InputStreamBase? rawContent = archiveFile.rawContent;
if (rawContent != null) {
pathsToSize[_fileSystem.path.split(archiveFile.name)] = 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;
}
final List<String> path = _fileSystem.path.split(
_fileSystem.path.relative(file.path, from: relativeTo));
pathsToSize[path] = file.lengthSync();
}
return _buildSymbolTree(pathsToSize);
}
List<String> _locatedAotFilePath = <String>[];
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);
if (childWithPathAsName == null) {
childWithPathAsName = _SymbolNode(path);
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)';
}
currentNode.addChild(childWithPathAsName);
}
childWithPathAsName.addSize(pathsToSize[paths] ?? 0);
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,
int maxDepth,
int currentDepth,
) {
totalPath += currentNode.name;
assert(_appFilename != null);
if (currentNode.children.isNotEmpty
&& currentNode.name != '$_appFilename (Dart AOT)'
&& currentDepth < maxDepth
&& currentNode.byteSize >= 1000) {
for (final _SymbolNode child in currentNode.children) {
_printLibChildrenPaths(child, '$totalPath/', aotSnapshotJsonRoot, maxDepth, currentDepth + 1);
}
_leadingPaths = totalPath.split('/')
..removeLast();
} else {
// 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 = 20, required int level}) {
_printEntitySize(
'Dart AOT symbols accounted decompressed size',
byteSize: aotSnapshotRoot.byteSize,
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)) {
// 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, Object?> _addAotSnapshotDataToAnalysis({
required Map<String, Object?> apkAnalysisJson,
required List<String> path,
required Map<String, Object?> aotSnapshotJson,
required Map<String, Object?> precompilerTrace,
}) {
Map<String, Object?> currentLevel = apkAnalysisJson;
currentLevel['precompiler-trace'] = precompilerTrace;
while (path.isNotEmpty) {
final List<Map<String, Object?>>? children = currentLevel['children'] as List<Map<String, Object?>>?;
final Map<String, Object?> childWithPathAsName = children?.firstWhere(
(Map<String, Object?> child) => (child['n'] as String?) == path.first,
) ?? <String, Object?>{};
path.removeAt(0);
currentLevel = childWithPathAsName;
}
currentLevel['children'] = aotSnapshotJson['children'];
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 String formattedSize = _prettyPrintBytes(byteSize);
TerminalColor color = TerminalColor.green;
if (formattedSize.endsWith('MB')) {
color = TerminalColor.cyan;
} else if (formattedSize.endsWith('KB')) {
color = TerminalColor.yellow;
}
// Compute any preceding directories, and compare this to the stored
// directories (in _leadingPaths) for the last entity that was printed. The
// similarly 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(
baseName + ' ' * spaceInBetween,
newline: false,
emphasis: emphasis,
indent: (level + i) * 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, Object?> 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, Object?> 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, Object?> aotSnapshotJson) {
final List<Object?> rawChildren = aotSnapshotJson['children'] as List<Object?>? ?? <Object?>[];
final List<_SymbolNode> symbolNodeChildren = <_SymbolNode>[];
int totalByteSize = 0;
// Given a child, build its subtree.
for (final Object? child in rawChildren) {
if (child == null) {
continue;
}
final _SymbolNode? childTreemapNode = _parseAotSnapshot(child as Map<String, Object?>);
if (childTreemapNode != null) {
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, Object?> toJson() {
final Map<String, Object?> json = <String, Object?>{
'n': name,
'value': byteSize,
};
final List<Map<String, Object?>> childrenAsJson = <Map<String, Object?>>[];
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;
}