Skip to content

Commit 20d2917

Browse files
mralephcommit-bot@chromium.org
authored andcommitted
[vm/tool] Improvements to snapshot_analysis 'compare' command.
Introduce helper classes and methods for constructing program hierarchy from a flat list of symbols as well as methods for diffing and bucketing the program hierarchies. Issue #41249 Cq-Include-Trybots: luci.dart.try:pkg-linux-debug-try,pkg-linux-release-try,pkg-win-release-try,pkg-mac-release-try Change-Id: Ieb6ad1ccd8bc7fa1181d31e78ac351369ede5f81 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/150101 Commit-Queue: Vyacheslav Egorov <[email protected]> Reviewed-by: Alexander Markov <[email protected]>
1 parent e35ca30 commit 20d2917

File tree

5 files changed

+794
-115
lines changed

5 files changed

+794
-115
lines changed

pkg/vm/lib/snapshot/compare.dart

Lines changed: 83 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
/// and which symbols decreased in size.
88
library vm.snapshot.compare;
99

10-
import 'dart:convert';
1110
import 'dart:io';
1211
import 'dart:math' as math;
1312

1413
import 'package:args/command_runner.dart';
1514

15+
import 'package:vm/snapshot/instruction_sizes.dart';
16+
import 'package:vm/snapshot/program_info.dart';
17+
1618
class 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].
161172
abstract class Row {
162173
String render(List<int> widths, List<AlignmentDirection> alignments);

pkg/vm/lib/snapshot/instruction_sizes.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:convert';
99
import 'dart:io';
1010

1111
import 'package:vm/snapshot/name.dart';
12+
import 'package:vm/snapshot/program_info.dart';
1213

1314
/// Parse the output of `--print-instructions-sizes-to` saved in the given
1415
/// file [input].
@@ -24,6 +25,22 @@ Future<List<SymbolInfo>> load(File input) async {
2425
.toList(growable: false);
2526
}
2627

28+
/// Parse the output of `--print-instructions-sizes-to` saved in the given
29+
/// file [input] into [ProgramInfo<int>] structure representing the sizes
30+
/// of individual functions.
31+
///
32+
/// If [collapseAnonymousClosures] is set to [true] then all anonymous closures
33+
/// within the same scopes are collapsed together. Collapsing closures is
34+
/// helpful when comparing symbol sizes between two versions of the same
35+
/// program because in general there is no reliable way to recognize the same
36+
/// anonymous closures into two independent compilations.
37+
Future<ProgramInfo<int>> loadProgramInfo(File input,
38+
{bool collapseAnonymousClosures = false}) async {
39+
final symbols = await load(input);
40+
return toProgramInfo(symbols,
41+
collapseAnonymousClosures: collapseAnonymousClosures);
42+
}
43+
2744
/// Information about the size of the instruction object.
2845
class SymbolInfo {
2946
/// Name of the code object (`Code::QualifiedName`) owning these instructions.
@@ -51,3 +68,43 @@ class SymbolInfo {
5168
size: map['s']);
5269
}
5370
}
71+
72+
/// Restore hierarchical [ProgramInfo<int>] representation from the list of
73+
/// symbols by parsing function names.
74+
///
75+
/// If [collapseAnonymousClosures] is set to [true] then all anonymous closures
76+
/// within the same scopes are collapsed together. Collapsing closures is
77+
/// helpful when comparing symbol sizes between two versions of the same
78+
/// program because in general there is no reliable way to recognize the same
79+
/// anonymous closures into two independent compilations.
80+
ProgramInfo<int> toProgramInfo(List<SymbolInfo> symbols,
81+
{bool collapseAnonymousClosures = false}) {
82+
final program = ProgramInfo<int>();
83+
for (var sym in symbols) {
84+
final scrubbed = sym.name.scrubbed;
85+
if (sym.libraryUri == null) {
86+
assert(sym.name.isStub);
87+
assert(
88+
!program.stubs.containsKey(scrubbed) || sym.name.isTypeTestingStub);
89+
program.stubs[scrubbed] = (program.stubs[scrubbed] ?? 0) + sym.size;
90+
} else {
91+
// Split the name into components (names of individual functions).
92+
final path = sym.name.components;
93+
94+
final lib =
95+
program.libraries.putIfAbsent(sym.libraryUri, () => LibraryInfo());
96+
final cls = lib.classes.putIfAbsent(sym.className, () => ClassInfo());
97+
var fun = cls.functions.putIfAbsent(path.first, () => FunctionInfo());
98+
for (var name in path.skip(1)) {
99+
if (collapseAnonymousClosures &&
100+
name.startsWith('<anonymous closure @')) {
101+
name = '<anonymous closure>';
102+
}
103+
fun = fun.closures.putIfAbsent(name, () => FunctionInfo());
104+
}
105+
fun.info = (fun.info ?? 0) + sym.size;
106+
}
107+
}
108+
109+
return program;
110+
}

pkg/vm/lib/snapshot/name.dart

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,45 @@ class Name {
1515
/// Raw textual representation of the name as it occurred in the output
1616
/// of the AOT compiler.
1717
final String raw;
18+
String _scrubbed;
1819

1920
Name(this.raw);
2021

2122
/// Pretty version of the name, with some of the irrelevant information
2223
/// removed from it.
24+
///
2325
/// Note: we still expect this name to be unique within compilation,
2426
/// so we are not removing any details that are used for disambiguation.
25-
String get scrubbed => raw.replaceAll(_scrubbingRe, '');
27+
/// The only exception are type testing stubs, these refer to type names
28+
/// and types names are not bound to be unique between compilations.
29+
String get scrubbed => _scrubbed ??=
30+
raw.replaceAll(isStub ? _stubScrubbingRe : _scrubbingRe, '');
2631

2732
/// Returns true if this name refers to a stub.
2833
bool get isStub => raw.startsWith('[Stub] ');
2934

3035
/// Returns true if this name refers to an allocation stub.
3136
bool get isAllocationStub => raw.startsWith('[Stub] Allocate ');
37+
38+
/// Returns ture if this name refers to a type testing stub.
39+
bool get isTypeTestingStub => raw.startsWith('[Stub] Type Test ');
40+
41+
/// Split this name into individual '.' separated components (e.g. names of
42+
/// its parent functions).
43+
List<String> get components {
44+
// Break the rest of the name into components.
45+
final result = scrubbed.split('.');
46+
47+
// Constructor names look like this 'new <ClassName>.<CtorName>' so
48+
// we need to concatenate the first two components back to form
49+
// the constructor name.
50+
if (result.first.startsWith('new ')) {
51+
result[0] = '${result[0]}${result[1]}';
52+
result.removeAt(1);
53+
}
54+
55+
return result;
56+
}
3257
}
3358

3459
// Remove useless prefixes and private library suffixes from the raw name.
@@ -37,3 +62,7 @@ class Name {
3762
// still, these names are formatted as '<anonymous closure @\d+>'.
3863
final _scrubbingRe =
3964
RegExp(r'\[(Optimized|Unoptimized|Stub)\]\s*|@\d+(?![>\d])');
65+
66+
// Remove useless prefixes and private library suffixes from the raw name
67+
// for stubs.
68+
final _stubScrubbingRe = RegExp(r'\[Stub\]\s*|@\d+|\(H[a-f\d]+\) ');

0 commit comments

Comments
 (0)