-
Notifications
You must be signed in to change notification settings - Fork 28.9k
Implement size analyzer to unzip & parse APK and AOT size snapshot to generate analysis json #62495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
json.decode(aotSizeJson.readAsStringSync()) as List<dynamic>, | ||
); | ||
|
||
// Rearrange children to ensure the type key shows at the top. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does the order of the keys in the output map matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's to make sure that the type field isn't just hidden at the bottom of the json because of the giant AOT data. I'm not sure when the user would need to check the type manually by opening up the json, but I was thinking it'd be better to make the type field more visible.
|
||
static const int tableWidth = 80; | ||
|
||
Future<Map<String, dynamic>> analyzeApkSize({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add dart doc describing what this method does. Add a note saying that aotSizeJson
can be from instruction sizes or v8 snapshot (if that is correct)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From other PR: if this function now does 2 things, reflect it in the name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
packages/flutter_tools/pubspec.yaml
Outdated
@@ -34,6 +34,7 @@ dependencies: | |||
native_stack_traces: 0.3.7 | |||
build_daemon: 2.1.4 | |||
shelf: 0.7.5 | |||
vm_snapshot_analysis: ^0.5.0+1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be an exact version, then run flutter update-packages --force-upgrade
to update the checksums
BufferLogger logger; | ||
FakeProcessManager processManager; | ||
|
||
const FakeCommand unzipCommmand = FakeCommand( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for the consts here, I would hoist them out of main at put them at the top level
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
currentParent = currentParent.childByName(pathPart) ?? | ||
currentParent.addChild(SymbolNode(pathPart)); | ||
} | ||
// TODO: this shouldn't be necessary, https://github.com/dart-lang/sdk/issues/41137 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this issue is marked as fixed, is this still necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
return child; | ||
} | ||
|
||
List<SymbolNode> get ancestors { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neither ancestors nor siblings are used in the test, so they currently have 0 coverage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed since those are not used (the entire SymbolNode
code was copied from here: #51594 (comment))
return rootNode; | ||
} | ||
|
||
/// Prints the paths from currentNode all leaf nodes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make this documentation more specific. This method seems like it is specific to the lib/ directory in an APK. note that here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
if (currentNode.children.isNotEmpty && !currentNode.name.contains('libapp.so')) { | ||
for (final SymbolNode child in currentNode.children) { | ||
_printLibDetails(child, totalPath + '/', aotSizeJson); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use string concatenation '$totalPath/'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
int numBytes, | ||
int level, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make these two named and required parameters
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
_printLibDetails(child, totalPath + '/', aotSizeJson); | ||
} | ||
} else { | ||
// Print total path and size if currentPath does not have any chilren. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if currentNode does not have any children
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
if (libraryUri != null) ...libraryUri.split('/') else '@stubs', | ||
if (className?.isNotEmpty ?? false) className, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm surprised that we are checking that className.isNotEmpty
and checking that libraryUri != null
. I would expect these two checks to be the same. Why are we checking for null on one and empty on the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
} else if (path.endsWith('libflutter.so')) { | ||
childWithPathAsName.name += ' (Flutter Engine)'; | ||
} | ||
currentNode.addChild(childWithPathAsName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does childWithPathAsName.addValue(pathsToSize[paths]);
need to be called before currentNode.addChild(childWithPathAsName);
so that the childWithPathAsName's
value is summed into currentNode's
value when calling addChild
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
childWithPathAsName
's value should already have been added to currentNode on the previous iteration of the loop.
|
||
final List<SymbolNode> sortedSymbols = root.children.toList() | ||
..sort((SymbolNode a, SymbolNode b) => b.value.compareTo(a.value)); | ||
for (final SymbolNode node in sortedSymbols.take(10)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why 10? put in a const with a descriptive name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
final List<SymbolNode> sortedSymbols = root.children.toList() | ||
..sort((SymbolNode a, SymbolNode b) => b.value.compareTo(a.value)); | ||
for (final SymbolNode node in sortedSymbols.take(10)) { | ||
_printEntitySize(node.name, node.value, 4); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment here. why 4?
Map<String, dynamic> apkAnalysisJson, | ||
List<String> path, | ||
List<dynamic> aotSizeJson, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make these required named parameters
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
/// Adds breakdown of aot size data as the children of the node at the given path. | ||
Map<String, dynamic> addAotSizeDataToJson( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to addAotSizeDataToApkAnalysis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Note the analysis test https://github.com/flutter/flutter/pull/62495/checks?check_run_id=923824683 Also, high level UX suggestion: I think we should list all the files in devtools, but for CLI where space is limited, the various small files aren't that useful to breakdown. I suggest bundling every single file at the root that's <1kB into just one line that says |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great start!
import 'logger.dart'; | ||
|
||
class SizeAnalyzer { | ||
SizeAnalyzer(this._fileSystem, this._logger, this._processUtils); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using required named parameters will be clearer than positional parameters here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
static const int tableWidth = 80; | ||
|
||
Future<Map<String, dynamic>> analyzeApkSize({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From other PR: if this function now does 2 things, reflect it in the name.
// TODO: implement Windows. | ||
String unzipOut; | ||
try { | ||
unzipOut = (await _processUtils.run(<String>[ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From other PR: using zipinfo rather than unzip could be an optimization (feel free to do it in a separate PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a TODO
// TODO(peterdjlee): Add aot size for all platforms. | ||
apkAnalysisJson = addAotSizeDataToJson( | ||
apkAnalysisJson, | ||
'lib/arm64-v8a/libapp.so (Dart AOT)'.split('/'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add code comments here for what this is doing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
} | ||
|
||
class SymbolNode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can probably be private. Also add comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
'''; | ||
|
||
setUp(() { | ||
fileSystem = MemoryFileSystem.test(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah is this why those parameters were in the constructor? There is already a dependency injection mechanism. See other tests using testUsingContext
. Then you can remove those construction params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jonahwilliams helped me put in those constructors explaining we might not want to use the global
keyword. Would removing the construction params force us to use the global
keyword?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, new code should not use the global zone injection - it makes testing the code very brittle as we've found that new contributors frequently do not understand which interfaces need to be overriden.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the new consensus is to do constructor injection through the entire graph? Is there a tracking bug to migrate existing code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#47161 , but I haven't done a good job of keeping it updated
Analysis test is failing with |
@@ -0,0 +1,166 @@ | |||
// Copyright 2014 The Flutter Authors. All rights reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2020
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
confusingly it should be 2014
} | ||
return json; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new line at end of file
); | ||
logger.printStatus('━' * tableWidth); | ||
final Directory tempApkContent = fileSystem.systemTempDirectory.createTempSync('flutter_tools.'); | ||
// TODO: implement Windows. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a bug assigned to it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tempApkContent.path | ||
])).stdout; | ||
} on Exception catch (e) { | ||
print(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be a printError and not a regular print
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
// 'path/to/file' where file = 1500 => pathsToSize[['path', 'to', 'file']] = 1500 | ||
for (final String line in const LineSplitter().convert(unzipOut)) { | ||
// 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+.* (.+)$'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer hoisting this into a private static, to avoid recompiling the regexp every time it is created
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/// A pretty printer for an entity with a size. | ||
void _printEntitySize( | ||
String entityName, { | ||
@required int byteSize, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatting is off a bit here, indent the required params by 2 spaces
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
// 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:file/memory.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you need a newline here to make analysis pass
You should be able to sync to the latest master to get a correctly formatted pubspec |
3046f07
to
ee9e677
Compare
import 'package:flutter_tools/src/base/process.dart'; | ||
import 'package:flutter_tools/src/base/terminal.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these need to be relative imports to fix the analyze bot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like other files have relative imports! Changed to relative imports.
// TODO(peterdjlee): Implement a way to unzip the APK for Windows. See issue #62603. | ||
String unzipOut; | ||
try { | ||
// TODO(peterdjlee): Use zipinfo instead of unzip. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, as far as I know we do not document requiring zipinfo to be installed, so switching may not be straightforward
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both zipinfo and unzip are built-in on macOS and neither are built-in on Ubuntu so we're more or less in the same position either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we're not in the same state - we require unzip and warn if it is not installed and do no such thing for zipinfo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah I see, SG, let's leave it then. This makes it simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
As soon as https://cirrus-ci.com/task/5692958575951872 finishes this should be good to land. The g3 breakage is expected because we've added a new dependency (not in this PR but a prior one) |
… generate analysis json (flutter#62495) * Implement size analyzer to unzip & parse APK and AOT size snapshot to generate analysis json
… generate analysis json (flutter#62495) * Implement size analyzer to unzip & parse APK and AOT size snapshot to generate analysis json
Description
Implemented

SizeAnalyzer
where an APK file and an AOT size snapshot can be passed in throughanalyzeApkSize(apk, aotSizeJson)
which returns a Map that contains the breakdown of APK data with AOT snapshot data included under libapp.so. `analyzeApkSize(apk, aotSizeJson) also prints out a top level summary to the terminal as such:Related Issues
#51594
Tests
I added a test file
analyze_size_test.dart
to test that this toolChecklist
Before you create this PR, confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]
). This will ensure a smooth and quick review process.///
).flutter analyze --flutter-repo
) does not report any problems on my PR.Breaking Change
Did any tests fail when you ran them? Please read Handling breaking changes.