blob: 16b250416420f8cf069b9e166dd2a852b85075e2 [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.
import 'dart:io';
import 'package:test/test.dart';
import 'package:collection/collection.dart';
import 'package:vm_snapshot_analysis/instruction_sizes.dart'
as instruction_sizes;
import 'package:vm_snapshot_analysis/program_info.dart';
import 'package:vm_snapshot_analysis/treemap.dart';
import 'package:vm_snapshot_analysis/utils.dart';
import 'package:vm_snapshot_analysis/v8_profile.dart';
import 'utils.dart';
final testSource = {
'input.dart': """
class K {
final value;
const K(this.value);
}
@pragma('vm:never-inline')
dynamic makeSomeClosures() {
return [
() => [const K(0)],
() => [const K(1)],
() => [11],
];
}
class A {
@pragma('vm:never-inline')
dynamic tornOff() {
return const K(2);
}
}
class B {
@pragma('vm:never-inline')
dynamic tornOff() {
return const K(3);
}
}
class C {
static dynamic tornOff() async {
return const K(4);
}
}
class D {
static dynamic tornOff() sync* {
yield const K(5);
}
}
@pragma('vm:never-inline')
Function tearOff(dynamic o) {
return o.tornOff;
}
void main(List<String> args) {
for (var cl in makeSomeClosures()) {
print(cl());
}
print(tearOff(args.isEmpty ? A() : B()));
print(C.tornOff);
print(D.tornOff);
}
"""
};
// Almost exactly the same source as above, but with few modifications
// marked with a 'modified' comment.
final testSourceModified = {
'input.dart': """
class K {
final value;
const K(this.value);
}
@pragma('vm:never-inline')
dynamic makeSomeClosures() {
return [
() => [const K(0)],
() => [const K(1)],
() => [11],
() => [const K(2)], // modified
];
}
class A {
@pragma('vm:never-inline')
dynamic tornOff() {
for (var cl in makeSomeClosures()) { // modified
print(cl()); // modified
} // modified
return const K(2);
}
}
class B {
@pragma('vm:never-inline')
dynamic tornOff() {
return const K(3);
}
}
class C {
static dynamic tornOff() async {
return const K(4);
}
}
class D {
static dynamic tornOff() sync* {
yield const K(5);
}
}
@pragma('vm:never-inline')
Function tearOff(dynamic o) {
return o.tornOff;
}
void main(List<String> args) {
// modified
print(tearOff(args.isEmpty ? A() : B()));
print(C.tornOff);
print(D.tornOff);
}
"""
};
final testSourceModified2 = {
'input.dart': """
class K {
final value;
const K(this.value);
}
@pragma('vm:never-inline')
dynamic makeSomeClosures() {
return [
() => [const K(0)],
];
}
class A {
@pragma('vm:never-inline')
dynamic tornOff() {
return const K(2);
}
}
class B {
@pragma('vm:never-inline')
dynamic tornOff() {
return const K(3);
}
}
class C {
static dynamic tornOff() async {
return const K(4);
}
}
class D {
static dynamic tornOff() sync* {
yield const K(5);
}
}
@pragma('vm:never-inline')
Function tearOff(dynamic o) {
return o.tornOff;
}
void main(List<String> args) {
for (var cl in makeSomeClosures()) {
print(cl());
}
print(tearOff(args.isEmpty ? A() : B()));
print(C.tornOff);
print(D.tornOff);
}
"""
};
final chainOfStaticCalls = {
'input.dart': """
@pragma('vm:never-inline')
String _private3(dynamic o) {
return "";
}
@pragma('vm:never-inline')
String _private2(dynamic o) {
return _private3(o);
}
@pragma('vm:never-inline')
String _private1(dynamic o) {
return _private2(o);
}
void main(List<String> args) {
_private1(null);
}
"""
};
extension on Histogram {
String bucketFor(String pkg, String lib, String cls, String fun) =>
(this.bucketInfo as Bucketing).bucketFor(pkg, lib, cls, fun);
}
void main() async {
if (!Platform.executable.contains('dart-sdk')) {
// If we are not running from the prebuilt SDK then this test does nothing.
return;
}
group('no-size-impact', () {
for (var flag in [printInstructionSizesFlag, writeV8ProfileFlag]) {
test(flag, () async {
await withFlagImpl(testSource, null, (baseline) async {
await withFlagImpl(testSource, flag, (snapshotWithFlag) async {
final sizeBaseline = File(baseline.outputBinary).statSync().size;
final sizeWithFlag =
File(snapshotWithFlag.outputBinary).statSync().size;
expect(sizeWithFlag - sizeBaseline, equals(0));
});
});
});
}
});
group('instruction-sizes', () {
test('basic-parsing', () async {
await withSymbolSizes(testSource, (sizesJson) async {
final json = await loadJson(File(sizesJson));
final symbols = instruction_sizes.fromJson(json as List<dynamic>);
expect(symbols, isNotNull,
reason: 'Sizes file was successfully parsed');
expect(symbols.length, greaterThan(0),
reason: 'Sizes file is non-empty');
// Check for duplicated symbols (using both raw and scrubbed names).
// Maps below contain mappings library-uri -> class-name -> names.
final symbolRawNames = <String, Map<String, Set<String>>>{};
final symbolScrubbedNames = <String, Map<String, Set<String>>>{};
Set<String> getSetOfNames(Map<String, Map<String, Set<String>>> map,
String? libraryUri, String? className) {
return map
.putIfAbsent(libraryUri ?? '', () => {})
.putIfAbsent(className ?? '', () => {});
}
for (var sym in symbols) {
expect(
getSetOfNames(symbolRawNames, sym.libraryUri, sym.className)
.add(sym.name.raw),
isTrue,
reason:
'All symbols should have unique names (within libraries): ${sym.name.raw}');
expect(
getSetOfNames(symbolScrubbedNames, sym.libraryUri, sym.className)
.add(sym.name.scrubbed),
isTrue,
reason: 'Scrubbing the name should not make it non-unique');
}
// Check for expected names which should appear in the output.
final inputDartSymbolNames =
symbolScrubbedNames['package:input/input.dart'];
expect(inputDartSymbolNames, isNotNull,
reason: 'Symbols from input.dart are included into sizes output');
inputDartSymbolNames!; // Checked above. Promote the variable type.
expect(inputDartSymbolNames[''], isNotNull,
reason: 'Should include top-level members from input.dart');
expect(inputDartSymbolNames[''], contains('makeSomeClosures'));
final closures = inputDartSymbolNames['']!.where(
(name) => name.startsWith('makeSomeClosures.<anonymous closure'));
expect(closures.length, 3,
reason: 'There are three closures inside makeSomeClosure');
expect(inputDartSymbolNames['A'], isNotNull,
reason: 'Should include class A members from input.dart');
expect(inputDartSymbolNames['A'], contains('tornOff'));
expect(inputDartSymbolNames['A'], contains('[tear-off] tornOff'));
expect(inputDartSymbolNames['A'],
contains('[tear-off-extractor] get:tornOff'));
expect(inputDartSymbolNames['B'], isNotNull,
reason: 'Should include class B members from input.dart');
expect(inputDartSymbolNames['B'], contains('tornOff'));
expect(inputDartSymbolNames['B'], contains('[tear-off] tornOff'));
expect(inputDartSymbolNames['B'],
contains('[tear-off-extractor] get:tornOff'));
// Presence of async modifier should not cause tear-off name to end
// with {body}.
expect(inputDartSymbolNames['C'], contains('[tear-off] tornOff'));
// Presence of sync* modifier should not cause tear-off name to end
// with {body}.
expect(inputDartSymbolNames['D'], contains('[tear-off] tornOff'));
// Check that output does not contain '[unknown stub]'
expect(symbolRawNames['']![''], isNot(contains('[unknown stub]')),
reason: 'All stubs must be named');
});
});
test('program-info-from-sizes', () async {
await withSymbolSizes(testSource, (sizesJson) async {
final json = await loadJson(File(sizesJson));
final info = loadProgramInfoFromJson(json);
expect(info.root.children, contains('dart:core'));
expect(info.root.children, contains('dart:typed_data'));
expect(info.root.children, contains('package:input'));
final inputLib = info.root.children['package:input']!
.children['package:input/input.dart']!;
expect(inputLib.children, contains('')); // Top-level class.
expect(inputLib.children, contains('A'));
expect(inputLib.children, contains('B'));
expect(inputLib.children, contains('C'));
expect(inputLib.children, contains('D'));
final topLevel = inputLib.children['']!;
expect(topLevel.children, contains('makeSomeClosures'));
expect(
topLevel.children['makeSomeClosures']!.children.length, equals(3));
for (var name in [
'[tear-off] tornOff',
'tornOff',
'Allocate A',
'[tear-off-extractor] get:tornOff'
]) {
expect(inputLib.children['A']!.children, contains(name));
expect(inputLib.children['A']!.children[name]!.children, isEmpty);
}
for (var name in [
'[tear-off] tornOff',
'tornOff',
'Allocate B',
'[tear-off-extractor] get:tornOff'
]) {
expect(inputLib.children['B']!.children, contains(name));
expect(inputLib.children['B']!.children[name]!.children, isEmpty);
}
for (var name in ['tornOff{body}', 'tornOff', '[tear-off] tornOff']) {
expect(inputLib.children['C']!.children, contains(name));
expect(inputLib.children['C']!.children[name]!.children, isEmpty);
}
for (var name in [
'tornOff{body}',
'tornOff{body depth 2}',
'tornOff',
'[tear-off] tornOff'
]) {
expect(inputLib.children['D']!.children, contains(name));
expect(inputLib.children['D']!.children[name]!.children, isEmpty);
}
});
});
test('histograms', () async {
await withSymbolSizes(testSource, (sizesJson) async {
final json = await loadJson(File(sizesJson));
final info = loadProgramInfoFromJson(json);
final bySymbol = computeHistogram(info, HistogramType.bySymbol);
expect(
bySymbol.buckets,
contains(bySymbol.bucketFor(
'package:input', 'package:input/input.dart', 'A', 'tornOff')));
expect(
bySymbol.buckets,
contains(bySymbol.bucketFor(
'package:input', 'package:input/input.dart', 'B', 'tornOff')));
expect(
bySymbol.buckets,
contains(bySymbol.bucketFor(
'package:input', 'package:input/input.dart', 'C', 'tornOff')));
expect(
bySymbol.buckets,
contains(bySymbol.bucketFor(
'package:input', 'package:input/input.dart', 'D', 'tornOff')));
final byClass = computeHistogram(info, HistogramType.byClass);
expect(
byClass.buckets,
contains(byClass.bucketFor('package:input',
'package:input/input.dart', 'A', 'does-not-matter')));
expect(
byClass.buckets,
contains(byClass.bucketFor('package:input',
'package:input/input.dart', 'B', 'does-not-matter')));
expect(
byClass.buckets,
contains(byClass.bucketFor('package:input',
'package:input/input.dart', 'C', 'does-not-matter')));
expect(
byClass.buckets,
contains(byClass.bucketFor('package:input',
'package:input/input.dart', 'D', 'does-not-matter')));
final byLibrary = computeHistogram(info, HistogramType.byLibrary);
expect(
byLibrary.buckets,
contains(byLibrary.bucketFor(
'package:input',
'package:input/input.dart',
'does-not-matter',
'does-not-matter')));
final byPackage = computeHistogram(info, HistogramType.byPackage);
expect(
byPackage.buckets,
contains(byPackage.bucketFor(
'package:input',
'package:input/does-not-matter.dart',
'does-not-matter',
'does-not-matter')));
});
});
test('histograms-filter', () async {
await withSymbolSizes(testSource, (sizesJson) async {
final json = await loadJson(File(sizesJson));
final info = loadProgramInfoFromJson(json);
{
final h = computeHistogram(info, HistogramType.bySymbol,
filter: 'A.tornOff');
expect(
h.buckets,
contains(h.bucketFor('package:input', 'package:input/input.dart',
'A', 'tornOff')));
expect(
h.buckets,
isNot(contains(h.bucketFor('package:input',
'package:input/input.dart', 'B', 'tornOff'))));
}
{
final h = computeHistogram(info, HistogramType.bySymbol,
filter: '::A*tornOff');
expect(
h.buckets,
contains(h.bucketFor('package:input', 'package:input/input.dart',
'A', 'tornOff')));
expect(
h.buckets,
isNot(contains(h.bucketFor('package:input',
'package:input/input.dart', 'B', 'tornOff'))));
}
{
final h = computeHistogram(info, HistogramType.bySymbol,
filter: 'input.dart*tornOff');
expect(
h.buckets,
contains(h.bucketFor('package:input', 'package:input/input.dart',
'A', 'tornOff')));
expect(
h.buckets,
contains(h.bucketFor('package:input', 'package:input/input.dart',
'B', 'tornOff')));
}
{
final h =
computeHistogram(info, HistogramType.byPackage, filter: 'A');
expect(
h.buckets,
contains(h.bucketFor(
'package:input',
'package:input/does-not-matter.dart',
'does-not-matter',
'does-not-matter')));
}
});
});
test('diff', () async {
await withSymbolSizes(testSource, (sizesJson) async {
await withSymbolSizes(testSourceModified, (modifiedSizesJson) async {
final infoJson = await loadJson(File(sizesJson));
final info = loadProgramInfoFromJson(infoJson);
final modifiedJson = await loadJson(File(modifiedSizesJson));
final modifiedInfo = loadProgramInfoFromJson(modifiedJson);
final diff = computeDiff(info, modifiedInfo);
expect(
diffToJson(diff),
equals({
'#type': 'library',
'package:input': {
'#type': 'package',
'package:input/input.dart': {
'#type': 'library',
'': {
'#type': 'class',
'makeSomeClosures': {
'#type': 'function',
'#size': greaterThan(0), // We added code here.
'<anonymous closure @186>': {
'#type': 'function',
'#size': greaterThan(0),
},
},
'main': {
'#type': 'function',
'#size': lessThan(0), // We removed code from main.
},
},
'A': {
'#type': 'class',
'tornOff': {
'#type': 'function',
'#size': greaterThan(0),
},
}
}
}
}));
});
});
});
test('diff-collapsed', () async {
await withSymbolSizes(testSource, (sizesJson) async {
await withSymbolSizes(testSourceModified2, (modifiedSizesJson) async {
final json = await loadJson(File(sizesJson));
final info =
loadProgramInfoFromJson(json, collapseAnonymousClosures: true);
final modifiedJson = await loadJson(File(modifiedSizesJson));
final modifiedInfo = loadProgramInfoFromJson(modifiedJson,
collapseAnonymousClosures: true);
final diff = computeDiff(info, modifiedInfo);
expect(
diffToJson(diff),
equals({
'#type': 'library',
'@stubs': {
'#type': 'library',
'Type Test Type: int*': {
'#type': 'function',
'#size': lessThan(0),
},
},
'package:input': {
'#type': 'package',
'package:input/input.dart': {
'#type': 'library',
'': {
'#type': 'class',
'makeSomeClosures': {
'#size': lessThan(0),
'#type': 'function',
'<anonymous closure>': {
'#size': lessThan(0),
'#type': 'function'
}
}
}
}
}
}));
});
});
});
});
group('v8-profile', () {
test('program-info-from-profile', () async {
await withV8Profile(testSource, (profileJson) async {
final infoJson = await loadJson(File(profileJson));
final info = loadProgramInfoFromJson(infoJson);
expect(info.root.children, contains('dart:core'));
expect(info.root.children, contains('dart:typed_data'));
expect(info.root.children, contains('package:input'));
final inputLib = info.root.children['package:input']!
.children['package:input/input.dart']!;
expect(inputLib.children, contains('::')); // Top-level class.
expect(inputLib.children, contains('A'));
expect(inputLib.children, contains('B'));
expect(inputLib.children, contains('C'));
final topLevel = inputLib.children['::']!;
expect(topLevel.children, contains('makeSomeClosures'));
expect(
topLevel.children['makeSomeClosures']!.children.values
.where((child) => child.type == NodeType.functionNode)
.length,
equals(3));
for (var name in [
'tornOff',
'Allocate A',
'[tear-off-extractor] get:tornOff'
]) {
expect(inputLib.children['A']!.children, contains(name));
}
expect(inputLib.children['A']!.children['tornOff']!.children,
contains('[tear-off] tornOff'));
for (var name in [
'tornOff',
'Allocate B',
'[tear-off-extractor] get:tornOff'
]) {
expect(inputLib.children['B']!.children, contains(name));
}
expect(inputLib.children['B']!.children['tornOff']!.children,
contains('[tear-off] tornOff'));
final classC = inputLib.children['C']!;
expect(classC.children, contains('tornOff'));
for (var name in ['tornOff{body}', '[tear-off] tornOff']) {
expect(classC.children['tornOff']!.children, contains(name));
}
// Verify that [ProgramInfoNode] owns its corresponding snapshot [Node].
final classesOwnedByC = info.snapshotInfo!.snapshot.nodes
.where((n) => info.snapshotInfo!.ownerOf(n) == classC)
.where((n) => n.type == 'Class')
.map((n) => n.name);
expect(classesOwnedByC, equals(['C']));
final classD = inputLib.children['D']!;
expect(classD.children, contains('tornOff'));
for (var name in ['tornOff{body}', '[tear-off] tornOff']) {
expect(classD.children['tornOff']!.children, contains(name));
}
expect(classD.children['tornOff']!.children['tornOff{body}']!.children,
contains('tornOff{body depth 2}'));
// Verify that [ProgramInfoNode] owns its corresponding snapshot [Node].
final classesOwnedByD = info.snapshotInfo!.snapshot.nodes
.where((n) => info.snapshotInfo!.ownerOf(n) == classD)
.where((n) => n.type == 'Class')
.map((n) => n.name);
expect(classesOwnedByD, equals(['D']));
});
});
// This test verifies that we are able to attribute instructions to the
// function they originated from.
test('instruction ownership is preserved', () async {
await withV8Profile(testSource, (profileJson) async {
await withSymbolSizes(testSource, (sizesJson) async {
final fromProfile =
loadProgramInfoFromJson(await loadJson(File(profileJson)));
final fromSymbolSizes = loadProgramInfoFromJson(
await loadJson(File(sizesJson)),
collapseAnonymousClosures: true);
final functionNode = fromProfile.lookup([
'package:input',
'package:input/input.dart',
'::',
'makeSomeClosures'
]);
final codeNode = fromProfile.snapshotInfo!.snapshot.nodes.firstWhere(
(n) =>
n.type == 'Code' &&
fromProfile.snapshotInfo!.ownerOf(n) == functionNode &&
n.name.contains('makeSomeClosures'));
expect(codeNode['<instructions>'], isNotNull);
final instructionsSize = codeNode['<instructions>']!.selfSize;
final symbolSize = fromSymbolSizes.lookup([
'package:input',
'package:input/input.dart',
'',
'makeSomeClosures'
])!.size;
expect(instructionsSize - symbolSize!, equals(0));
});
});
});
test('histograms', () async {
await withV8Profile(testSource, (sizesJson) async {
final infoJson = await loadJson(File(sizesJson));
final info = loadProgramInfoFromJson(infoJson);
final bySymbol = computeHistogram(info, HistogramType.bySymbol);
expect(
bySymbol.buckets,
contains(bySymbol.bucketFor(
'package:input', 'package:input/input.dart', 'A', 'tornOff')));
expect(
bySymbol.buckets,
contains(bySymbol.bucketFor(
'package:input', 'package:input/input.dart', 'B', 'tornOff')));
expect(
bySymbol.buckets,
contains(bySymbol.bucketFor(
'package:input', 'package:input/input.dart', 'C', 'tornOff')));
expect(
bySymbol.buckets,
contains(bySymbol.bucketFor(
'package:input', 'package:input/input.dart', 'D', 'tornOff')));
final byClass = computeHistogram(info, HistogramType.byClass);
expect(
byClass.buckets,
contains(byClass.bucketFor('package:input',
'package:input/input.dart', 'A', 'does-not-matter')));
expect(
byClass.buckets,
contains(byClass.bucketFor('package:input',
'package:input/input.dart', 'B', 'does-not-matter')));
expect(
byClass.buckets,
contains(byClass.bucketFor('package:input',
'package:input/input.dart', 'C', 'does-not-matter')));
expect(
byClass.buckets,
contains(byClass.bucketFor('package:input',
'package:input/input.dart', 'D', 'does-not-matter')));
final byLibrary = computeHistogram(info, HistogramType.byLibrary);
expect(
byLibrary.buckets,
contains(byLibrary.bucketFor(
'package:input',
'package:input/input.dart',
'does-not-matter',
'does-not-matter')));
final byPackage = computeHistogram(info, HistogramType.byPackage);
expect(
byPackage.buckets,
contains(byPackage.bucketFor(
'package:input',
'package:input/does-not-matter.dart',
'does-not-matter',
'does-not-matter')));
});
});
test('diff', () async {
await withV8Profile(testSource, (profileJson) async {
await withV8Profile(testSourceModified, (modifiedProfileJson) async {
final infoJson = await loadJson(File(profileJson));
final info = loadProgramInfoFromJson(infoJson);
final modifiedJson = await loadJson(File(modifiedProfileJson));
final modifiedInfo = loadProgramInfoFromJson(modifiedJson);
final diff = computeDiff(info, modifiedInfo);
expect(
diffToJson(diff, keepOnlyInputPackage: true),
equals({
'package:input': {
'#type': 'package',
'package:input/input.dart': {
'#type': 'library',
'::': {
'#type': 'class',
'makeSomeClosures': {
'#type': 'function',
'#size': greaterThan(0),
'<anonymous closure @186>': {
'#type': 'function',
'#size': greaterThan(0),
},
},
'main': {
'#type': 'function',
'#size': lessThan(0),
},
},
'A': {
'#type': 'class',
'tornOff': {
'#type': 'function',
'#size': greaterThan(0),
},
}
}
}
}));
});
});
});
test('diff-collapsed', () async {
await withV8Profile(testSource, (profileJson) async {
await withV8Profile(testSourceModified2, (modifiedProfileJson) async {
final infoJson = await loadJson(File(profileJson));
final info = loadProgramInfoFromJson(infoJson,
collapseAnonymousClosures: true);
final modifiedJson = await loadJson(File(modifiedProfileJson));
final modifiedInfo = loadProgramInfoFromJson(modifiedJson,
collapseAnonymousClosures: true);
final diff = computeDiff(info, modifiedInfo);
expect(
diffToJson(diff, keepOnlyInputPackage: true),
equals({
'package:input': {
'#type': 'package',
'package:input/input.dart': {
'#type': 'library',
'::': {
'#type': 'class',
'makeSomeClosures': {
'#type': 'function',
'<anonymous closure>': {
'#type': 'function',
'#size': lessThan(0),
},
},
},
}
}
}));
});
});
});
test('treemap', () async {
await withV8Profile(testSource, (profileJson) async {
final infoJson = await loadJson(File(profileJson));
final info = await loadProgramInfoFromJson(infoJson,
collapseAnonymousClosures: true);
final treemap = treemapFromInfo(info);
List<Map<String, dynamic>> childrenOf(Map<String, dynamic> node) =>
(node['children'] as List).cast();
String nameOf(Map<String, dynamic> node) => node['n'];
Map<String, dynamic>? findChild(
Map<String, dynamic> node, String name) {
return childrenOf(node)
.firstWhereOrNull((child) => nameOf(child) == name);
}
Set<String> childrenNames(Map<String, dynamic> node) {
return childrenOf(node).map(nameOf).toSet();
}
// Verify that we don't include package names twice into paths
// while building the treemap.
if (Platform.isWindows) {
// Note: in Windows we don't consider main.dart part of package:input
// for some reason.
expect(findChild(treemap, 'package:input/input.dart'), isNotNull);
} else {
expect(childrenNames(findChild(treemap, 'package:input')!),
equals({'main.dart', 'input.dart'}));
}
});
});
test('dominators', () async {
await withV8Profile(chainOfStaticCalls, (profileJson) async {
// Note: computing dominators also verifies that we don't have
// unreachable nodes in the snapshot.
final infoJson = await loadJson(File(profileJson));
final snapshot = Snapshot.fromJson(infoJson as Map<String, dynamic>);
for (var n in snapshot.nodes.skip(1)) {
expect(snapshot.dominatorOf(n), isNotNull);
}
});
});
});
}
final printInstructionSizesFlag = '--print_instructions_sizes_to';
Future withSymbolSizes(
Map<String, String> source, Future Function(String sizesJson) f) =>
withFlag(source, printInstructionSizesFlag, f);
final writeV8ProfileFlag = '--write_v8_snapshot_profile_to';
Future withV8Profile(
Map<String, String> source, Future Function(String sizesJson) f) =>
withFlag(source, writeV8ProfileFlag, f);
// On Windows there is some issue with interpreting entry point URI as a package URI
// it instead gets interpreted as a file URI - which breaks comparison. So we
// simply ignore entry point library (main.dart).
// Additionally this function removes all nodes with the size below
// the given threshold.
Map<String, dynamic>? diffToJson(ProgramInfo diff,
{bool keepOnlyInputPackage = false}) {
final diffJson = diff.toJson();
diffJson.removeWhere((key, _) =>
keepOnlyInputPackage ? key != 'package:input' : key.startsWith('file:'));
// Rebuild the diff JSON discarding all nodes with size below threshold.
const smallChangeThreshold = 16;
Map<String, dynamic>? discardSmallChanges(Map<String, dynamic> map) {
final result = <String, dynamic>{};
// First recursively process all children (skipping #type and #size keys).
for (var key in map.keys) {
if (key == '#type' || key == '#size') continue;
final value = discardSmallChanges(map[key]);
if (value != null) {
result[key] = value;
}
}
// Check if this node own #size is above the threshold and copy it
// into the result if it is.
final size = map['#size'] ?? 0;
if (size.abs() > smallChangeThreshold) {
result['#size'] = size;
}
// If the node has no children and its own size does not pass the threshold
// drop it.
if (result.isEmpty) {
return null;
}
// We decided that this node is meaningful - preserve its type.
if (map.containsKey('#type')) {
result['#type'] = map['#type'];
}
return result;
}
return discardSmallChanges(diffJson);
}