From d727165aba204b2854c9c795ebb3d8a74bf42452 Mon Sep 17 00:00:00 2001 From: Mark Zhou Date: Fri, 21 Jan 2022 18:53:51 +0000 Subject: [PATCH] [dart2js] Adding initial tooling support for runtime code analysis. Change-Id: I6138b4480a1f35fbb8ca4b7a145e7df6ac043d64 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/229063 Reviewed-by: Joshua Litt Commit-Queue: Mark Zhou --- .../bin/src/runtime_coverage_analysis.dart | 130 ++++++++++++++++++ pkg/dart2js_info/bin/tools.dart | 2 + pkg/dart2js_info/lib/src/util.dart | 20 +++ 3 files changed, 152 insertions(+) create mode 100644 pkg/dart2js_info/bin/src/runtime_coverage_analysis.dart diff --git a/pkg/dart2js_info/bin/src/runtime_coverage_analysis.dart b/pkg/dart2js_info/bin/src/runtime_coverage_analysis.dart new file mode 100644 index 00000000000..577aee612f3 --- /dev/null +++ b/pkg/dart2js_info/bin/src/runtime_coverage_analysis.dart @@ -0,0 +1,130 @@ +// 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 + 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 _runtimeCoverageAnalysis(infoFile, coverageFile) async { + var info = await infoFromFile(infoFile); + var coverageRaw = jsonDecode(File(coverageFile).readAsStringSync()); + var coverage = {}; + 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((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)}%'); +} diff --git a/pkg/dart2js_info/bin/tools.dart b/pkg/dart2js_info/bin/tools.dart index 9e2cda87169..7dc64bd67a0 100644 --- a/pkg/dart2js_info/bin/tools.dart +++ b/pkg/dart2js_info/bin/tools.dart @@ -16,6 +16,7 @@ import 'src/convert.dart'; import 'src/function_size_analysis.dart'; import 'src/library_size_split.dart'; import 'src/live_code_size_analysis.dart'; +import 'src/runtime_coverage_analysis.dart'; import 'src/show_inferred_types.dart'; import 'src/text_print.dart'; @@ -35,6 +36,7 @@ void main(args) { ..addCommand(FunctionSizeCommand()) ..addCommand(LibrarySizeCommand()) ..addCommand(LiveCodeAnalysisCommand()) + ..addCommand(RuntimeCoverageAnalysisCommand()) ..addCommand(ShowInferredTypesCommand()) ..addCommand(ShowCommand()); commandRunner.run(args); diff --git a/pkg/dart2js_info/lib/src/util.dart b/pkg/dart2js_info/lib/src/util.dart index eebb1c74f50..9ee83cbcff2 100644 --- a/pkg/dart2js_info/lib/src/util.dart +++ b/pkg/dart2js_info/lib/src/util.dart @@ -46,6 +46,26 @@ Graph graphFromInfo(AllInfo info) { return graph; } +/// Provide a qualified name associated with [info]. Qualified names consist of +/// the library's canonical URI concatenated with a library-unique kernel name. +// See: https://github.com/dart-lang/sdk/blob/47eff41cdbfea4a178208dfc3137ba2b6bea0e36/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart#L978 +// TODO(sigmund): guarantee that the name is actually unique. +String qualifiedName(Info f) { + assert(f is ClosureInfo || f is ClassInfo); + var element = f; + String name; + while (element != null) { + if (element is LibraryInfo) { + name = '${element.uri}:$name'; + return name; + } else { + name = name ?? element.name; + element = element.parent; + } + } + return ''; +} + /// Provide a unique long name associated with [info]. // TODO(sigmund): guarantee that the name is actually unique. String longName(Info info, {bool useLibraryUri = false, bool forId = false}) {