blob: 130e34b476665d5b8b0a86cf906012d3c7543102 [file] [log] [blame]
// 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> {
final name = 'explain';
final description = '''
Explain why certain methods were pulled into the binary.
ExplainCommand() {
/// 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> {
final name = 'dynamic-calls';
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).
Future<void> run() async {
final args = argResults!;
final sizesJson = File([0]);
if (!sizesJson.existsSync()) {
usageException('Size profile ${sizesJson.path} does not exist!');
final sizesJsonRaw = await loadJsonFromFile(sizesJson);
final traceJson = File([1]);
if (!traceJson.existsSync()) {
usageException('Size profile ${traceJson.path} does not exist!');
final traceJsonRaw = await loadJsonFromFile(traceJson);
final callGraph = loadTrace(traceJsonRaw);
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 = ( as ProgramInfoNode)
.map((n) => Name(n).scrubbed)
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.length - 1, path.last.replaceAll('[tear-off] ', ''));
final retainedSize = programInfo.lookup(path);
totalSize += (retainedSize?.totalSize ?? 0);
return true;
return totalSize;
}, bucketFor: (n) {
return ( 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) => == selector || == 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(' ${}');
String get invocation =>
super.invocation.replaceAll('[arguments]', '<sizes.json> <trace.json>');