458be23fa7
Fixes https://github.com/dart-lang/sdk/issues/45683 TEST=pkg-*-try bots Cq-Include-Trybots: luci.dart.try:pkg-linux-release-try,pkg-mac-release-try,pkg-win-release-try Change-Id: Ie10f313da9778d001f9c4fb618997e3b3c781dd0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/195263 Commit-Queue: Vyacheslav Egorov <vegorov@google.com> Reviewed-by: Tess Strickland <sstrickl@google.com>
132 lines
4.5 KiB
Dart
132 lines
4.5 KiB
Dart
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
/// This command allows to inspect information written into the
|
|
/// precompiler trace (`--trace-precompiler-to` output).
|
|
library vm_snapshot_analysis.explain;
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:args/command_runner.dart';
|
|
|
|
import 'package:vm_snapshot_analysis/name.dart';
|
|
import 'package:vm_snapshot_analysis/precompiler_trace.dart';
|
|
import 'package:vm_snapshot_analysis/program_info.dart';
|
|
import 'package:vm_snapshot_analysis/utils.dart';
|
|
|
|
import 'utils.dart';
|
|
|
|
class ExplainCommand extends Command<void> {
|
|
@override
|
|
final name = 'explain';
|
|
|
|
@override
|
|
final description = '''
|
|
Explain why certain methods were pulled into the binary.
|
|
''';
|
|
|
|
ExplainCommand() {
|
|
addSubcommand(ExplainDynamicCallsCommand());
|
|
}
|
|
}
|
|
|
|
/// Generates a summary report about dynamic calls sorted by approximation
|
|
/// of their retained size, i.e. the amount of bytes these calls are pulling
|
|
/// into the snapshot.
|
|
class ExplainDynamicCallsCommand extends Command<void> {
|
|
@override
|
|
final name = 'dynamic-calls';
|
|
|
|
@override
|
|
final description = '''
|
|
This command explains impact of the dynamic calls on the binary size.
|
|
|
|
It needs AOT snapshot size profile (an output of either
|
|
--write-v8-snapshot-profile-to or --print-instructions-sizes-to flags) and
|
|
precompiler trace (an output of --trace-precompiler-to flag).
|
|
''';
|
|
|
|
ExplainCommand() {}
|
|
|
|
@override
|
|
Future<void> run() async {
|
|
final args = argResults!;
|
|
|
|
final sizesJson = File(args.rest[0]);
|
|
if (!sizesJson.existsSync()) {
|
|
usageException('Size profile ${sizesJson.path} does not exist!');
|
|
}
|
|
final sizesJsonRaw = await loadJsonFromFile(sizesJson);
|
|
|
|
final traceJson = File(args.rest[1]);
|
|
if (!traceJson.existsSync()) {
|
|
usageException('Size profile ${traceJson.path} does not exist!');
|
|
}
|
|
final traceJsonRaw = await loadJsonFromFile(traceJson);
|
|
|
|
final callGraph = loadTrace(traceJsonRaw);
|
|
callGraph.computeDominators();
|
|
|
|
final programInfo = loadProgramInfoFromJson(sizesJsonRaw);
|
|
|
|
final histogram = Histogram.fromIterable<CallGraphNode>(
|
|
callGraph.dynamicCalls, sizeOf: (dynamicCall) {
|
|
// Compute approximate retained size by traversing the dominator tree
|
|
// and consulting snapshot profile.
|
|
var totalSize = 0;
|
|
dynamicCall.visitDominatorTree((retained, depth) {
|
|
if (retained.isFunctionNode) {
|
|
// Note that call graph keeps private library keys intact in the
|
|
// names (because we need to distinguish dynamic invocations
|
|
// through with the same private name in different libraries).
|
|
// So we need to scrub the path before we lookup information in the
|
|
// profile.
|
|
final path = (retained.data as ProgramInfoNode)
|
|
.path
|
|
.map((n) => Name(n).scrubbed)
|
|
.toList();
|
|
if (path.last.startsWith('[tear-off] ')) {
|
|
// Tear-off forwarder is placed into the function that is torn so
|
|
// we need to slightly tweak the path to be able to find it.
|
|
path.insert(
|
|
path.length - 1, path.last.replaceAll('[tear-off] ', ''));
|
|
}
|
|
final retainedSize = programInfo.lookup(path);
|
|
totalSize += (retainedSize?.totalSize ?? 0);
|
|
}
|
|
return true;
|
|
});
|
|
return totalSize;
|
|
}, bucketFor: (n) {
|
|
return (n.data as String).replaceAll('dyn:', '');
|
|
}, bucketInfo: BucketInfo(nameComponents: ['Selector']));
|
|
|
|
printHistogram(programInfo, histogram,
|
|
prefix: histogram.bySize.where((key) => histogram.buckets[key]! > 0));
|
|
|
|
// For top 10 dynamic selectors print the functions which contain these
|
|
// dynamic calls.
|
|
for (var selector
|
|
in histogram.bySize.take(math.min(10, histogram.length))) {
|
|
final dynSelector = 'dyn:$selector';
|
|
final callNodes = callGraph.nodes
|
|
.where((n) => n.data == selector || n.data == dynSelector);
|
|
|
|
print('\nDynamic call to ${selector}'
|
|
' (retaining ~${histogram.buckets[selector]} bytes) occurs in:');
|
|
for (var node in callNodes) {
|
|
for (var pred in node.pred) {
|
|
print(' ${pred.data.qualifiedName}');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
String get invocation =>
|
|
super.invocation.replaceAll('[arguments]', '<sizes.json> <trace.json>');
|
|
}
|