77/// and which symbols decreased in size.
88library vm.snapshot.compare;
99
10- import 'dart:convert' ;
1110import 'dart:io' ;
1211import 'dart:math' as math;
1312
1413import 'package:args/command_runner.dart' ;
1514
15+ import 'package:vm/snapshot/instruction_sizes.dart' ;
16+ import 'package:vm/snapshot/program_info.dart' ;
17+
1618class CompareCommand extends Command <void > {
1719 @override
1820 final String name = 'compare' ;
@@ -32,10 +34,26 @@ Use --narrow flag to limit column widths.''';
3234 super .invocation.replaceAll ('[arguments]' , '<old.json> <new.json>' );
3335
3436 CompareCommand () {
35- argParser.addOption ('column-width' ,
36- help: 'Truncate column content to the given width'
37- ' (${AsciiTable .unlimitedWidth } means do not truncate).' ,
38- defaultsTo: AsciiTable .unlimitedWidth.toString ());
37+ argParser
38+ ..addOption ('column-width' ,
39+ help: 'Truncate column content to the given width'
40+ ' (${AsciiTable .unlimitedWidth } means do not truncate).' ,
41+ defaultsTo: AsciiTable .unlimitedWidth.toString ())
42+ ..addOption ('granularity' ,
43+ help: 'Choose the granularity of the output.' ,
44+ allowed: ['method' , 'class' , 'library' , 'package' ],
45+ defaultsTo: 'method' )
46+ ..addFlag ('collapse-anonymous-closures' , help: '''
47+ Collapse all anonymous closures from the same scope into a single entry.
48+ When comparing size of AOT snapshots for two different versions of a
49+ program there is no reliable way to precisely establish which two anonymous
50+ closures are the same and should be compared in size - so
51+ comparison might produce a noisy output. This option reduces confusion
52+ by collapsing different anonymous closures within the same scope into a
53+ single entry. Note that when comparing the same application compiled
54+ with two different versions of an AOT compiler closures can be distinguished
55+ precisely based on their source position (which is included in their name).
56+ ''' );
3957 }
4058
4159 @override
@@ -53,110 +71,103 @@ Use --narrow flag to limit column widths.''';
5371
5472 final oldJsonPath = _checkExists (argResults.rest[0 ]);
5573 final newJsonPath = _checkExists (argResults.rest[1 ]);
56- printComparison (oldJsonPath, newJsonPath, maxWidth: maxWidth);
74+ printComparison (oldJsonPath, newJsonPath,
75+ maxWidth: maxWidth,
76+ granularity: _parseHistogramType (argResults['granularity' ]),
77+ collapseAnonymousClosures: argResults['collapse-anonymous-closures' ]);
78+ }
79+
80+ HistogramType _parseHistogramType (String value) {
81+ switch (value) {
82+ case 'method' :
83+ return HistogramType .bySymbol;
84+ case 'class' :
85+ return HistogramType .byClass;
86+ case 'library' :
87+ return HistogramType .byLibrary;
88+ case 'package' :
89+ return HistogramType .byPackage;
90+ }
5791 }
5892
59- String _checkExists (String path) {
60- if (! File (path).existsSync ()) {
93+ File _checkExists (String path) {
94+ final file = File (path);
95+ if (! file.existsSync ()) {
6196 usageException ('File $path does not exist!' );
6297 }
63- return path ;
98+ return file ;
6499 }
65100}
66101
67- void printComparison (String oldJsonPath, String newJsonPath,
68- {int maxWidth: 0 }) {
69- final oldSizes = loadSymbolSizes (oldJsonPath);
70- final newSizes = loadSymbolSizes (newJsonPath);
71-
102+ void printComparison (File oldJson, File newJson,
103+ {int maxWidth: 0 ,
104+ bool collapseAnonymousClosures = false ,
105+ HistogramType granularity = HistogramType .bySymbol}) async {
106+ final oldSizes = await loadProgramInfo (oldJson,
107+ collapseAnonymousClosures: collapseAnonymousClosures);
108+ final newSizes = await loadProgramInfo (newJson,
109+ collapseAnonymousClosures: collapseAnonymousClosures);
110+ final diff = computeDiff (oldSizes, newSizes);
111+
112+ // Compute total sizes.
72113 var totalOld = 0 ;
114+ oldSizes.visit ((_, __, ___, size) {
115+ totalOld += size;
116+ });
117+
73118 var totalNew = 0 ;
119+ newSizes.visit ((_, __, ___, size) {
120+ totalNew += size;
121+ });
122+
74123 var totalDiff = 0 ;
75- final diffBySymbol = < String , int > {};
76-
77- // Process all symbols (from both old and new results) and compute the change
78- // in size. If symbol is not present in the compilation assume its size to be
79- // zero.
80- for (var key in Set <String >()..addAll (newSizes.keys)..addAll (oldSizes.keys)) {
81- final oldSize = oldSizes[key] ?? 0 ;
82- final newSize = newSizes[key] ?? 0 ;
83- final diff = newSize - oldSize;
84- if (diff != 0 ) diffBySymbol[key] = diff;
85- totalOld += oldSize;
86- totalNew += newSize;
87- totalDiff += diff;
88- }
124+ diff.visit ((_, __, ___, size) {
125+ totalDiff += size.inBytes;
126+ });
89127
90- // Compute the list of changed symbols sorted by difference (descending) .
91- final changedSymbolsBySize = diffBySymbol.keys. toList ();
92- changedSymbolsBySize. sort ((a, b ) => diffBySymbol[b] - diffBySymbol[a] );
128+ // Compute histogram .
129+ final histogram = SizesHistogram . from < SymbolDiff >(
130+ diff, (diff ) => diff.inBytes, granularity );
93131
94132 // Now produce the report table.
95133 const numLargerSymbolsToReport = 30 ;
96134 const numSmallerSymbolsToReport = 10 ;
97135 final table = AsciiTable (header: [
98- Text .left ('Library' ),
99- Text .left ('Method' ),
136+ for (var col in histogram.bucketing.nameComponents) Text .left (col),
100137 Text .right ('Diff (Bytes)' )
101138 ], maxWidth: maxWidth);
102139
103140 // Report [numLargerSymbolsToReport] symbols that increased in size most.
104- for (var key in changedSymbolsBySize
105- .where ((k) => diffBySymbol [k] > 0 )
141+ for (var key in histogram.bySize
142+ .where ((k) => histogram.buckets [k] > 0 )
106143 .take (numLargerSymbolsToReport)) {
107- final name = key.split (librarySeparator);
108- table.addRow ([name[0 ], name[1 ], '+${diffBySymbol [key ]}' ]);
144+ table.addRow ([
145+ ...histogram.bucketing.namesFromBucket (key),
146+ '+${histogram .buckets [key ]}'
147+ ]);
109148 }
110149 table.addSeparator (Separator .Wave );
111150
112151 // Report [numSmallerSymbolsToReport] symbols that decreased in size most.
113- for (var key in changedSymbolsBySize .reversed
114- .where ((k) => diffBySymbol [k] < 0 )
152+ for (var key in histogram.bySize .reversed
153+ .where ((k) => histogram.buckets [k] < 0 )
115154 .take (numSmallerSymbolsToReport)
116155 .toList ()
117156 .reversed) {
118- final name = key.split (librarySeparator);
119- table.addRow ([name[0 ], name[1 ], '${diffBySymbol [key ]}' ]);
157+ table.addRow ([
158+ ...histogram.bucketing.namesFromBucket (key),
159+ '${histogram .buckets [key ]}'
160+ ]);
120161 }
121162 table.addSeparator ();
122163
123164 table.render ();
124- print ('Comparing ${oldJsonPath } (old) to ${newJsonPath } (new)' );
165+ print ('Comparing ${oldJson . path } (old) to ${newJson . path } (new)' );
125166 print ('Old : ${totalOld } bytes.' );
126167 print ('New : ${totalNew } bytes.' );
127168 print ('Change: ${totalDiff > 0 ? '+' : '' }${totalDiff } bytes.' );
128169}
129170
130- /// A combination of characters that is unlikely to occur in the symbol name.
131- const String librarySeparator = ',' ;
132-
133- /// Load --print-instructions-sizes-to output as a mapping from symbol names
134- /// to their sizes.
135- ///
136- /// Note: we produce a single symbol name from function name and library name
137- /// by concatenating them with [librarySeparator] .
138- Map <String , int > loadSymbolSizes (String name) {
139- final symbols = jsonDecode (File (name).readAsStringSync ());
140- final result = new Map <String , int >();
141- final regexp = new RegExp (r"0x[a-fA-F0-9]+" );
142- for (int i = 0 , n = symbols.length; i < n; i++ ) {
143- final e = symbols[i];
144- // Obtain a key by combining library and method name. Strip anything
145- // after the library separator to make sure we can easily decode later.
146- // For method names, also remove non-deterministic parts to avoid
147- // reporting non-existing differences against the same layout.
148- String lib = ((e['l' ] ?? '' ).split (librarySeparator))[0 ];
149- String name = (e['n' ].split (librarySeparator))[0 ]
150- .replaceAll ('[Optimized] ' , '' )
151- .replaceAll (regexp, '' );
152- String key = lib + librarySeparator + name;
153- int val = e['s' ];
154- result[key] =
155- (result[key] ?? 0 ) + val; // add (key,val), accumulate if exists
156- }
157- return result;
158- }
159-
160171/// A row in the [AsciiTable] .
161172abstract class Row {
162173 String render (List <int > widths, List <AlignmentDirection > alignments);
0 commit comments