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