Adding support for common element comparisons to dart2js_info

Common elements are determined structurally, not by size.
Additionally, elements can be restricted to the package-level or
sorted by name/size via flags.

Change-Id: I8ee4bf70f4ccbf022cdb121f2e61f9416bb9affe
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/222000
Reviewed-by: Joshua Litt <joshualitt@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Mark Zhou <markzipan@google.com>
This commit is contained in:
Mark Zhou 2021-12-08 21:23:38 +00:00 committed by Commit Bot
parent 3d56526ba1
commit 5bff102009
6 changed files with 330 additions and 1 deletions

View file

@ -1,3 +1,7 @@
## 0.6.6
* Added the `comm` tool for finding commonalities between info files.
## 0.6.5
* Drop unused dependencies.

View file

@ -92,7 +92,11 @@ The following tools are a available today:
dependency between functions and fields in your program. Currently it only
supports the `some_path` query, which shows a dependency path from one
function to another.
* [`common`][common]: a tool that reports the common elements of two info
files. Commonality is determined by the element's name, file path, and URI
but not code size.
* [`diff`][diff]: a tool that diffs two info files and reports which
program elements have been added, removed, or changed size. This also
tells which elements are no longer deferred or have become deferred.
@ -160,6 +164,44 @@ a fully qualified element name, which includes the library and class name
If the name of a function your are looking for is unique enough, it might be
sufficient to just write that name as your regular expression.
### Common tool
This command-line tool shows common elements between two info files. It can be
run as follows:
```console
$ pub global activate dart2js_info # only needed once
$ dart2js_info common old.js.info.data new.js.info.data
```
The tool gives a breakdown of the common elements between the two info files,
reporting code size discrepancies if they exist.
Here's an example output snippet:
```
COMMON ELEMENTS (455 common elements, 70334 bytes -> 70460 bytes)
========================================================================
dart:_foreign_helper::: 141 bytes
dart:_foreign_helper::JS_CONST: 141 bytes
dart:_foreign_helper::JS_CONST.code: 0 bytes
dart:_interceptors::: 4052 -> 7968 bytes
dart:_interceptors::ArrayIterator: 805 bytes
dart:_interceptors::ArrayIterator.ArrayIterator: 0 bytes
dart:_interceptors::ArrayIterator._current: 91 bytes
dart:_interceptors::ArrayIterator._index: 0 bytes
dart:_interceptors::ArrayIterator._iterable: 0 bytes
dart:_interceptors::ArrayIterator._length: 0 bytes
dart:_interceptors::ArrayIterator.current: 0 bytes
dart:_interceptors::ArrayIterator.moveNext: 406 bytes
dart:_interceptors::Interceptor: 198 bytes
dart:_interceptors::Interceptor.toString: 104 -> 182 bytes
```
Common elements are sorted by name by default but can be sorted by size with
the `--order-by-size` flag. Additionally, the tool can be restricted to
just packages with the `--packages-only` flag.
### Diff tool
This command-line tool shows a diff between two info files. It can be run

View file

@ -0,0 +1,152 @@
// Copyright (c) 2021, 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.
import 'package:args/command_runner.dart';
import 'package:dart2js_info/info.dart';
import 'package:dart2js_info/src/common_element.dart';
import 'package:dart2js_info/src/io.dart';
import 'package:dart2js_info/src/util.dart';
import 'usage_exception.dart';
/// A command that computes the commonalities between two info files.
class CommonCommand extends Command<void> with PrintUsageException {
@override
final String name = "common";
@override
final String description =
"See code element commonalities between two dump-info files.";
CommonCommand() {
argParser.addFlag('packages-only',
defaultsTo: false, help: "Show only packages in common");
argParser.addFlag('order-by-size',
defaultsTo: false,
help: "Show output ordered by size in bytes (decreasing). "
"If there are size discrepancies, orders by the first "
"dump-info file's reported size.");
}
@override
void run() async {
var args = argResults.rest;
if (args.length < 2) {
usageException(
'Missing arguments, expected two dump-info files to compare');
return;
}
var oldInfo = await infoFromFile(args[0]);
var newInfo = await infoFromFile(args[1]);
var packagesOnly = argResults['packages-only'];
var orderBySize = argResults['order-by-size'];
var commonElements = findCommonalities(oldInfo, newInfo);
if (packagesOnly) {
reportPackages(commonElements, orderBySize: orderBySize);
} else {
report(commonElements, orderBySize: orderBySize);
}
}
}
void report(List<CommonElement> commonElements, {orderBySize = false}) {
var oldSizeTotal = 0, newSizeTotal = 0;
for (var element in commonElements) {
// Only sum sizes from leaf elements so we don't double count.
if (element.oldInfo.kind == InfoKind.field ||
element.oldInfo.kind == InfoKind.function ||
element.oldInfo.kind == InfoKind.closure ||
element.oldInfo.kind == InfoKind.typedef) {
oldSizeTotal += element.oldInfo.size;
newSizeTotal += element.newInfo.size;
}
}
_section('COMMON ELEMENTS',
elementCount: commonElements.length,
oldSizeTotal: oldSizeTotal,
newSizeTotal: newSizeTotal);
if (orderBySize) {
commonElements.sort((a, b) => b.oldInfo.size.compareTo(a.oldInfo.size));
} else {
commonElements.sort((a, b) => a.name.compareTo(b.name));
}
for (var element in commonElements) {
var oldSize = element.oldInfo.size;
var newSize = element.newInfo.size;
if (oldSize == newSize) {
print('${element.name}: ${element.oldInfo.size} bytes');
} else {
print('${element.name}: ${element.oldInfo.size} -> '
'${element.newInfo.size} bytes');
}
}
}
void reportPackages(List<CommonElement> commonElements, {orderBySize = false}) {
// Maps package names to their cumulative size.
var oldPackageInfo = <String, int>{};
var newPackageInfo = <String, int>{};
for (int i = 0; i < commonElements.length; i++) {
var element = commonElements[i];
// Skip non-libraries to avoid double counting elements when accumulating
// package-level information.
if (element.oldInfo.kind != InfoKind.library) continue;
var package = packageName(element.oldInfo);
if (package == null) continue;
var oldSize = element.oldInfo.size;
var newSize = element.newInfo.size;
oldPackageInfo[package] = (oldPackageInfo[package] ?? 0) + oldSize;
newPackageInfo[package] = (newPackageInfo[package] ?? 0) + newSize;
}
var oldSizeTotal = 0, newSizeTotal = 0;
oldPackageInfo.forEach((oldPackageName, oldPackageSize) {
var newPackageSize = newPackageInfo[oldPackageName];
oldSizeTotal += oldPackageSize;
newSizeTotal += newPackageSize;
});
_section('COMMON ELEMENTS (PACKAGES)',
elementCount: oldPackageInfo.keys.length,
oldSizeTotal: oldSizeTotal,
newSizeTotal: newSizeTotal);
var packageInfoEntries = oldPackageInfo.entries.toList();
if (orderBySize) {
packageInfoEntries.sort((a, b) => b.value.compareTo(a.value));
} else {
packageInfoEntries.sort((a, b) => a.key.compareTo(b.key));
}
for (var entry in packageInfoEntries) {
var oldSize = entry.value;
var newSize = newPackageInfo[entry.key];
if (oldSize == newSize) {
print('${entry.key}: $oldSize bytes');
} else {
print('${entry.key}: $oldSize bytes -> $newSize bytes');
}
}
}
void _section(String title,
{int elementCount, int oldSizeTotal, int newSizeTotal}) {
if (oldSizeTotal == newSizeTotal) {
print('$title ($elementCount common elements, $oldSizeTotal bytes)');
} else {
print('$title ($elementCount common elements, '
'$oldSizeTotal bytes -> $newSizeTotal bytes)');
}
print('=' * 72);
}

View file

@ -5,6 +5,7 @@
import 'package:args/command_runner.dart';
import 'src/code_deps.dart';
import 'src/common_command.dart';
import 'src/coverage_log_server.dart';
import 'src/debug_info.dart';
import 'src/diff.dart';
@ -23,6 +24,7 @@ void main(args) {
var commandRunner = CommandRunner("dart2js_info",
"collection of tools to digest the output of dart2js's --dump-info")
..addCommand(CodeDepsCommand())
..addCommand(CommonCommand())
..addCommand(CoverageLogServerCommand())
..addCommand(DebugCommand())
..addCommand(DiffCommand())

View file

@ -0,0 +1,116 @@
import 'package:dart2js_info/info.dart';
import 'package:dart2js_info/src/util.dart';
class CommonElement {
final BasicInfo oldInfo;
final BasicInfo newInfo;
CommonElement(this.oldInfo, this.newInfo);
get name => longName(oldInfo, useLibraryUri: true);
}
List<CommonElement> findCommonalities(AllInfo oldInfo, AllInfo newInfo) {
var finder = _InfoCommonElementFinder(oldInfo, newInfo);
finder.run();
return finder.commonElements;
}
class _InfoCommonElementFinder extends InfoVisitor<void> {
final AllInfo _old;
final AllInfo _new;
BasicInfo _other;
List<CommonElement> commonElements = <CommonElement>[];
_InfoCommonElementFinder(this._old, this._new);
void run() {
_commonList(_old.libraries, _new.libraries);
}
@override
visitAll(AllInfo info) {
throw StateError('should not run common on AllInfo');
}
@override
visitProgram(ProgramInfo info) {
throw StateError('should not run common on ProgramInfo');
}
@override
visitOutput(OutputUnitInfo info) {
throw StateError('should not run common on OutputUnitInfo');
}
@override
visitConstant(ConstantInfo info) {
throw StateError('should not run common on ConstantInfo');
}
@override
visitLibrary(LibraryInfo info) {
var other = _other as LibraryInfo;
commonElements.add(CommonElement(info, other));
_commonList(info.topLevelVariables, other.topLevelVariables);
_commonList(info.topLevelFunctions, other.topLevelFunctions);
_commonList(info.classes, other.classes);
}
@override
visitClass(ClassInfo info) {
var other = _other as ClassInfo;
commonElements.add(CommonElement(info, other));
_commonList(info.fields, other.fields);
_commonList(info.functions, other.functions);
}
@override
visitClassType(ClassTypeInfo info) {
var other = _other as ClassInfo;
commonElements.add(CommonElement(info, other));
}
@override
visitClosure(ClosureInfo info) {
var other = _other as ClosureInfo;
commonElements.add(CommonElement(info, other));
_commonList([info.function], [other.function]);
}
@override
visitField(FieldInfo info) {
var other = _other as FieldInfo;
commonElements.add(CommonElement(info, other));
_commonList(info.closures, other.closures);
}
@override
visitFunction(FunctionInfo info) {
var other = _other as FunctionInfo;
commonElements.add(CommonElement(info, other));
_commonList(info.closures, other.closures);
}
@override
visitTypedef(TypedefInfo info) {
var other = _other as ClassInfo;
commonElements.add(CommonElement(info, other));
}
void _commonList(List<BasicInfo> oldInfos, List<BasicInfo> newInfos) {
var newNames = <String, BasicInfo>{};
for (var newInfo in newInfos) {
newNames[longName(newInfo, useLibraryUri: true)] = newInfo;
}
for (var oldInfo in oldInfos) {
var oldName = longName(oldInfo, useLibraryUri: true);
if (newNames.containsKey(oldName)) {
_other = newNames[oldName];
oldInfo.accept(this);
}
}
}
}

View file

@ -90,6 +90,19 @@ String longName(Info info, {bool useLibraryUri = false, bool forId = false}) {
return sb.toString();
}
/// Provides the package name associated with [info] or null otherwise.
String packageName(Info info) {
while (info.parent != null) {
info = info.parent;
}
if (info is LibraryInfo) {
if (info.uri.scheme == 'package') {
return '${info.uri}'.split('/').first;
}
}
return null;
}
/// Produce a string containing [value] padded with white space up to [n] chars.
pad(value, n, {bool right = false}) {
var s = '$value';