Skip to content

Commit ca5d40c

Browse files
peterdjleemingwandroid
authored andcommitted
Implement size analyzer to unzip & parse APK and AOT size snapshot to generate analysis json (flutter#62495)
* Implement size analyzer to unzip & parse APK and AOT size snapshot to generate analysis json
1 parent 06f03d1 commit ca5d40c

File tree

2 files changed

+537
-0
lines changed

2 files changed

+537
-0
lines changed
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:meta/meta.dart';
6+
import 'package:vm_snapshot_analysis/treemap.dart';
7+
8+
import '../base/file_system.dart';
9+
import '../convert.dart';
10+
import 'logger.dart';
11+
import 'process.dart';
12+
import 'terminal.dart';
13+
14+
/// A class to analyze APK and AOT snapshot and generate a breakdown of the data.
15+
class SizeAnalyzer {
16+
SizeAnalyzer({
17+
@required this.fileSystem,
18+
@required this.logger,
19+
@required this.processUtils,
20+
});
21+
22+
final FileSystem fileSystem;
23+
final Logger logger;
24+
final ProcessUtils processUtils;
25+
26+
static const String aotSnapshotFileName = 'aot-snapshot.json';
27+
28+
static const int tableWidth = 80;
29+
30+
/// Analyzes [apk] and [aotSnapshot] to output a [Map] object that includes
31+
/// the breakdown of the both files, where the breakdown of [aotSnapshot] is placed
32+
/// under 'lib/arm64-v8a/libapp.so'.
33+
///
34+
/// The [aotSnapshot] can be either instruction sizes snapshot or v8 snapshot.
35+
Future<Map<String, dynamic>> analyzeApkSizeAndAotSnapshot({
36+
@required File apk,
37+
@required File aotSnapshot,
38+
}) async {
39+
logger.printStatus('▒' * tableWidth);
40+
_printEntitySize(
41+
'${apk.basename} (total compressed)',
42+
byteSize: apk.lengthSync(),
43+
level: 0,
44+
showColor: false,
45+
);
46+
logger.printStatus('━' * tableWidth);
47+
final Directory tempApkContent = fileSystem.systemTempDirectory.createTempSync('flutter_tools.');
48+
// TODO(peterdjlee): Implement a way to unzip the APK for Windows. See issue #62603.
49+
String unzipOut;
50+
try {
51+
// TODO(peterdjlee): Use zipinfo instead of unzip.
52+
unzipOut = (await processUtils.run(<String>[
53+
'unzip',
54+
'-o',
55+
'-v',
56+
apk.path,
57+
'-d',
58+
tempApkContent.path
59+
])).stdout;
60+
} on Exception catch (e) {
61+
logger.printError(e.toString());
62+
} finally {
63+
// We just want the the stdout printout. We don't need the files.
64+
tempApkContent.deleteSync(recursive: true);
65+
}
66+
67+
final _SymbolNode apkAnalysisRoot = _parseUnzipFile(unzipOut);
68+
69+
// Convert an AOT snapshot file into a map.
70+
final Map<String, dynamic> processedAotSnapshotJson = treemapFromJson(
71+
json.decode(aotSnapshot.readAsStringSync()),
72+
);
73+
final _SymbolNode aotSnapshotJsonRoot = _parseAotSnapshot(processedAotSnapshotJson);
74+
75+
for (final _SymbolNode firstLevelPath in apkAnalysisRoot.children) {
76+
_printEntitySize(
77+
firstLevelPath.name,
78+
byteSize: firstLevelPath.byteSize,
79+
level: 1,
80+
);
81+
// Print the expansion of lib directory to show more info for libapp.so.
82+
if (firstLevelPath.name == 'lib') {
83+
_printLibChildrenPaths(firstLevelPath, '', aotSnapshotJsonRoot);
84+
}
85+
}
86+
87+
logger.printStatus('▒' * tableWidth);
88+
89+
Map<String, dynamic> apkAnalysisJson = apkAnalysisRoot.toJson();
90+
91+
apkAnalysisJson['type'] = 'apk';
92+
93+
// TODO(peterdjlee): Add aot snapshot for all platforms.
94+
apkAnalysisJson = _addAotSnapshotDataToApkAnalysis(
95+
apkAnalysisJson: apkAnalysisJson,
96+
path: 'lib/arm64-v8a/libapp.so (Dart AOT)'.split('/'), // Pass in a list of paths by splitting with '/'.
97+
aotSnapshotJson: processedAotSnapshotJson,
98+
);
99+
100+
return apkAnalysisJson;
101+
}
102+
103+
104+
// Expression to match 'Size' column to group 1 and 'Name' column to group 2.
105+
final RegExp _parseUnzipOutput = RegExp(r'^\s*\d+\s+[\w|:]+\s+(\d+)\s+.* (.+)$');
106+
107+
// Parse the output of unzip -v which shows the zip's contents' compressed sizes.
108+
// Example output of unzip -v:
109+
// Length Method Size Cmpr Date Time CRC-32 Name
110+
// -------- ------ ------- ---- ---------- ----- -------- ----
111+
// 11708 Defl:N 2592 78% 00-00-1980 00:00 07733eef AndroidManifest.xml
112+
// 1399 Defl:N 1092 22% 00-00-1980 00:00 f53d952a META-INF/CERT.RSA
113+
// 46298 Defl:N 14530 69% 00-00-1980 00:00 17df02b8 META-INF/CERT.SF
114+
_SymbolNode _parseUnzipFile(String unzipOut) {
115+
final Map<List<String>, int> pathsToSize = <List<String>, int>{};
116+
117+
// Parse each path into pathsToSize so that the key is a list of
118+
// path parts and the value is the size.
119+
// For example:
120+
// 'path/to/file' where file = 1500 => pathsToSize[['path', 'to', 'file']] = 1500
121+
for (final String line in const LineSplitter().convert(unzipOut)) {
122+
final RegExpMatch match = _parseUnzipOutput.firstMatch(line);
123+
if (match == null) {
124+
continue;
125+
}
126+
const int sizeGroupIndex = 1;
127+
const int nameGroupIndex = 2;
128+
pathsToSize[match.group(nameGroupIndex).split('/')] = int.parse(match.group(sizeGroupIndex));
129+
}
130+
131+
final _SymbolNode rootNode = _SymbolNode('Root');
132+
133+
_SymbolNode currentNode = rootNode;
134+
for (final List<String> paths in pathsToSize.keys) {
135+
for (final String path in paths) {
136+
_SymbolNode childWithPathAsName = currentNode.childByName(path);
137+
138+
if (childWithPathAsName == null) {
139+
childWithPathAsName = _SymbolNode(path);
140+
if (path.endsWith('libapp.so')) {
141+
childWithPathAsName.name += ' (Dart AOT)';
142+
} else if (path.endsWith('libflutter.so')) {
143+
childWithPathAsName.name += ' (Flutter Engine)';
144+
}
145+
currentNode.addChild(childWithPathAsName);
146+
}
147+
childWithPathAsName.addSize(pathsToSize[paths]);
148+
currentNode = childWithPathAsName;
149+
}
150+
currentNode = rootNode;
151+
}
152+
153+
return rootNode;
154+
}
155+
156+
/// Prints all children paths for the lib/ directory in an APK.
157+
///
158+
/// A brief summary of aot snapshot is printed under 'lib/arm64-v8a/libapp.so'.
159+
void _printLibChildrenPaths(
160+
_SymbolNode currentNode,
161+
String totalPath,
162+
_SymbolNode aotSnapshotJsonRoot,
163+
) {
164+
totalPath += currentNode.name;
165+
166+
if (currentNode.children.isNotEmpty && !currentNode.name.contains('libapp.so')) {
167+
for (final _SymbolNode child in currentNode.children) {
168+
_printLibChildrenPaths(child, '$totalPath/', aotSnapshotJsonRoot);
169+
}
170+
} else {
171+
// Print total path and size if currentNode does not have any chilren.
172+
_printEntitySize(totalPath, byteSize: currentNode.byteSize, level: 2);
173+
174+
// We picked this file because arm64-v8a is likely the most popular
175+
// architecture. ther architecture sizes should be similar.
176+
const String libappPath = 'lib/arm64-v8a/libapp.so';
177+
// TODO(peterdjlee): Analyze aot size for all platforms.
178+
if (totalPath.contains(libappPath)) {
179+
_printAotSnapshotSummary(aotSnapshotJsonRoot);
180+
}
181+
}
182+
}
183+
184+
/// Go through the AOT gen snapshot size JSON and print out a collapsed summary
185+
/// for the first package level.
186+
void _printAotSnapshotSummary(_SymbolNode aotSnapshotRoot, {int maxDirectoriesShown = 10}) {
187+
_printEntitySize(
188+
'Dart AOT symbols accounted decompressed size',
189+
byteSize: aotSnapshotRoot.byteSize,
190+
level: 3,
191+
);
192+
193+
final List<_SymbolNode> sortedSymbols = aotSnapshotRoot.children.toList()
194+
..sort((_SymbolNode a, _SymbolNode b) => b.byteSize.compareTo(a.byteSize));
195+
for (final _SymbolNode node in sortedSymbols.take(maxDirectoriesShown)) {
196+
_printEntitySize(node.name, byteSize: node.byteSize, level: 4);
197+
}
198+
}
199+
200+
/// Adds breakdown of aot snapshot data as the children of the node at the given path.
201+
Map<String, dynamic> _addAotSnapshotDataToApkAnalysis({
202+
@required Map<String, dynamic> apkAnalysisJson,
203+
@required List<String> path,
204+
@required Map<String, dynamic> aotSnapshotJson,
205+
}) {
206+
Map<String, dynamic> currentLevel = apkAnalysisJson;
207+
while (path.isNotEmpty) {
208+
final List<Map<String, dynamic>> children = currentLevel['children'] as List<Map<String, dynamic>>;
209+
final Map<String, dynamic> childWithPathAsName = children.firstWhere(
210+
(Map<String, dynamic> child) => child['n'] as String == path.first,
211+
);
212+
path.removeAt(0);
213+
currentLevel = childWithPathAsName;
214+
}
215+
currentLevel['children'] = aotSnapshotJson['children'];
216+
return apkAnalysisJson;
217+
}
218+
219+
/// Print an entity's name with its size on the same line.
220+
void _printEntitySize(
221+
String entityName, {
222+
@required int byteSize,
223+
@required int level,
224+
bool showColor = true,
225+
}) {
226+
final bool emphasis = level <= 1;
227+
final String formattedSize = _prettyPrintBytes(byteSize);
228+
229+
TerminalColor color = TerminalColor.green;
230+
if (formattedSize.endsWith('MB')) {
231+
color = TerminalColor.cyan;
232+
} else if (formattedSize.endsWith('KB')) {
233+
color = TerminalColor.yellow;
234+
}
235+
236+
final int spaceInBetween = tableWidth - level * 2 - entityName.length - formattedSize.length;
237+
logger.printStatus(
238+
entityName + ' ' * spaceInBetween,
239+
newline: false,
240+
emphasis: emphasis,
241+
indent: level * 2,
242+
);
243+
logger.printStatus(formattedSize, color: showColor ? color : null);
244+
}
245+
246+
String _prettyPrintBytes(int numBytes) {
247+
const int kB = 1024;
248+
const int mB = kB * 1024;
249+
if (numBytes < kB) {
250+
return '$numBytes B';
251+
} else if (numBytes < mB) {
252+
return '${(numBytes / kB).round()} KB';
253+
} else {
254+
return '${(numBytes / mB).round()} MB';
255+
}
256+
}
257+
258+
_SymbolNode _parseAotSnapshot(Map<String, dynamic> aotSnapshotJson) {
259+
final bool isLeafNode = aotSnapshotJson['children'] == null;
260+
if (!isLeafNode) {
261+
return _buildNodeWithChildren(aotSnapshotJson);
262+
} else {
263+
// TODO(peterdjlee): Investigate why there are leaf nodes with size of null.
264+
final int byteSize = aotSnapshotJson['value'] as int;
265+
if (byteSize == null) {
266+
return null;
267+
}
268+
return _buildNode(aotSnapshotJson, byteSize);
269+
}
270+
}
271+
272+
_SymbolNode _buildNode(
273+
Map<String, dynamic> aotSnapshotJson,
274+
int byteSize, {
275+
List<_SymbolNode> children = const <_SymbolNode>[],
276+
}) {
277+
final String name = aotSnapshotJson['n'] as String;
278+
final Map<String, _SymbolNode> childrenMap = <String, _SymbolNode>{};
279+
280+
for (final _SymbolNode child in children) {
281+
childrenMap[child.name] = child;
282+
}
283+
284+
return _SymbolNode(
285+
name,
286+
byteSize: byteSize,
287+
)..addAllChildren(children);
288+
}
289+
290+
/// Builds a node by recursively building all of its children first
291+
/// in order to calculate the sum of its children's sizes.
292+
_SymbolNode _buildNodeWithChildren(Map<String, dynamic> aotSnapshotJson) {
293+
final List<dynamic> rawChildren = aotSnapshotJson['children'] as List<dynamic>;
294+
final List<_SymbolNode> symbolNodeChildren = <_SymbolNode>[];
295+
int totalByteSize = 0;
296+
297+
// Given a child, build its subtree.
298+
for (final dynamic child in rawChildren) {
299+
final _SymbolNode childTreemapNode = _parseAotSnapshot(child as Map<String, dynamic>);
300+
symbolNodeChildren.add(childTreemapNode);
301+
totalByteSize += childTreemapNode.byteSize;
302+
}
303+
304+
// If none of the children matched the diff tree type
305+
if (totalByteSize == 0) {
306+
return null;
307+
} else {
308+
return _buildNode(
309+
aotSnapshotJson,
310+
totalByteSize,
311+
children: symbolNodeChildren,
312+
);
313+
}
314+
}
315+
}
316+
317+
/// A node class that represents a single symbol for AOT size snapshots.
318+
class _SymbolNode {
319+
_SymbolNode(
320+
this.name, {
321+
this.byteSize = 0,
322+
}) : assert(name != null),
323+
assert(byteSize != null),
324+
_children = <String, _SymbolNode>{};
325+
326+
/// The human friendly identifier for this node.
327+
String name;
328+
329+
int byteSize;
330+
void addSize(int sizeToBeAdded) {
331+
byteSize += sizeToBeAdded;
332+
}
333+
334+
_SymbolNode get parent => _parent;
335+
_SymbolNode _parent;
336+
337+
Iterable<_SymbolNode> get children => _children.values;
338+
final Map<String, _SymbolNode> _children;
339+
340+
_SymbolNode childByName(String name) => _children[name];
341+
342+
_SymbolNode addChild(_SymbolNode child) {
343+
assert(child.parent == null);
344+
assert(!_children.containsKey(child.name),
345+
'Cannot add duplicate child key ${child.name}');
346+
347+
child._parent = this;
348+
_children[child.name] = child;
349+
return child;
350+
}
351+
352+
void addAllChildren(List<_SymbolNode> children) {
353+
children.forEach(addChild);
354+
}
355+
356+
Map<String, dynamic> toJson() {
357+
final Map<String, dynamic> json = <String, dynamic>{
358+
'n': name,
359+
'value': byteSize
360+
};
361+
final List<Map<String, dynamic>> childrenAsJson = <Map<String, dynamic>>[];
362+
for (final _SymbolNode child in children) {
363+
childrenAsJson.add(child.toJson());
364+
}
365+
if (childrenAsJson.isNotEmpty) {
366+
json['children'] = childrenAsJson;
367+
}
368+
return json;
369+
}
370+
}

0 commit comments

Comments
 (0)