// 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:collection/collection.dart';
import 'package:test/test.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) =>
      (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', '[tear-off] tornOff']) {
          expect(inputLib.children['C']!.children, contains(name));
          expect(inputLib.children['C']!.children[name]!.children, isEmpty);
        }

        for (var name in ['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',
                '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 ['[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 ['[tear-off] tornOff']) {
          expect(classD.children['tornOff']!.children, contains(name));
        }

        // 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',
                        '#size': lessThan(0),
                        '<anonymous closure>': {
                          '#type': 'function',
                          '#size': lessThan(0),
                        },
                      },
                    },
                  }
                }
              }));
        });
      });
    });

    test('treemap', () async {
      await withV8Profile(testSource, (profileJson) async {
        final infoJson = await loadJson(File(profileJson));
        final info =
            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.
        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);
}
