blob: 577aee612f320866a38e010ee80082e7946cf215 [file] [log] [blame]
// Copyright (c) 2022, 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.
/// Command-line tool presenting combined information from dump-info and
/// runtime coverage data.
///
/// This tool requires two input files an `.info.data` and a
/// `.coverage.json` file. To produce these files you need to follow these
/// steps:
///
/// * Compile an app with dart2js using --dump-info and save the .info.data
/// file:
///
/// dart2js --dump-info main.dart
///
/// * Build the same app with dart2js using --experimental-track-allocations:
///
/// dart2js --experimental-track-allocations main.dart
///
/// This can be combined with the --dump-info step above.
///
/// * Load your app, exercise your code, then extract the runtime code
/// coverage JSON blob by querying `$__dart_deferred_initializers__.allocations` in the page.
///
/// * Finally, run this tool.
library compiler.tool.runtime_coverage_analysis;
import 'dart:convert';
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:collection/collection.dart';
import 'package:dart2js_info/info.dart';
import 'package:dart2js_info/src/io.dart';
import 'package:dart2js_info/src/util.dart';
import 'usage_exception.dart';
class RuntimeCoverageAnalysisCommand extends Command<void>
with PrintUsageException {
@override
final String name = "runtime_coverage";
@override
final String description = "Analyze runtime coverage data";
RuntimeCoverageAnalysisCommand();
@override
void run() async {
var args = argResults.rest;
if (args.length < 2) {
usageException('Missing arguments, expected: info.data coverage.json');
}
await _runtimeCoverageAnalysis(args[0], args[1]);
}
}
Future<void> _runtimeCoverageAnalysis(infoFile, coverageFile) async {
var info = await infoFromFile(infoFile);
var coverageRaw = jsonDecode(File(coverageFile).readAsStringSync());
var coverage = <String, bool>{};
coverageRaw
.forEach((k, v) => coverage[k] = coverage[k] ?? false || v as bool);
int totalProgramSize = info.program.size;
int totalLibSize = info.libraries.fold(0, (n, lib) => n + lib.size);
int totalCode = 0;
int usedCode = 0;
var unused = PriorityQueue<Info>((a, b) => b.size.compareTo(a.size));
void tallyCode(Info i) {
totalCode += i.size;
var name = qualifiedName(i);
var used = coverage[name];
if (used != null) {
usedCode += i.size;
} else {
unused.add(i);
}
}
info.classes.forEach(tallyCode);
info.closures.forEach(tallyCode);
_section('Runtime Coverage Summary');
_showHeader('', 'bytes', '%');
_show('Program size', totalProgramSize, totalProgramSize);
_show('Libraries (excluding statics)', totalLibSize, totalProgramSize);
_show('Code (classes + closures)', totalCode, totalProgramSize);
_show('Used', usedCode, totalProgramSize);
print('');
_showHeader('', 'count', '%');
var total = info.classes.length + info.closures.length;
_show('Classes + closures', total, total);
_show('Used', total - unused.length, total);
print('');
var unusedTotal = totalCode - usedCode;
_section('Runtime Coverage Breakdown', size: unusedTotal);
// TODO(markzipan): support grouping results by package/library.
for (int i = 0; i < unused.length; i++) {
var item = unused.removeFirst();
var percent = (item.size * 100 / unusedTotal).toStringAsFixed(2);
print('${qualifiedName(item)}: ${item.size} bytes, $percent%');
}
}
void _section(String title, {int size}) {
if (size == null) {
print(title);
} else {
print('$title ($size bytes)');
}
print('=' * 72);
}
_showHeader(String msg, String header1, String header2) {
print(' ${pad(msg, 30, right: true)} ${pad(header1, 8)} ${pad(header2, 6)}');
}
_show(String msg, int size, int total) {
var percent = (size * 100 / total).toStringAsFixed(2);
print(' ${pad(msg, 30, right: true)} ${pad(size, 8)} ${pad(percent, 6)}%');
}