[CFE] Low-level coverage tool

This/these tools can later be integrated into other tools/tests if we
want to.

For now it is "external" tools where one can for instance see that
running our strong tests leaves quite a bit to be desired in regards
to constant transformation coverage:

package:front_end/src/fasta/kernel/constant_collection_builders.dart
Misses: [950, 956, 960, 970, 1002, 1014, 1015, 1025, 1334, 1340, 1431, 1441, 1782, 1792, 2593, 2599, 2624, 2949, 2977, 2984, 3001, 3036, 3044, 3067, 3080, 3124, 3153, 3157, 3167, 3270, 3280, 3301, 3315, 3354, 3963, 3973, 4050, 4063, 4089, 4099, 4194, 4204, 4275, 4288, 4314, 4324, 4797, 4803, 4828, 5889, 5899, 5979, 6007, 6014, 6031, 6065, 6073, 6096, 6108, 6152, 6180, 6184, 6194, 6296, 6306, 6327, 6341, 6378, 7267, 7273, 7277, 7287, 7318, 7337, 7363, 7368, 7377, 7387, 7401, 7411, 7768, 7774, 7865, 7875, 8232, 8242, 8566, 8572, 8605, 8667, 8677, 8753, 8766, 8787, 8797, 8897, 8907, 8977, 8990, 9011, 9021, 9760, 9788, 9795, 9812, 9854, 9875, 9898, 9910, 9919, 9961, 9989, 9993, 10003, 10105, 10115, 10127, 10149, 10181, 10201]
Not compiled: [[2291 - 2403], [3724 - 3834]]

package:front_end/src/fasta/kernel/constant_evaluator.dart
Misses: [4301, 4340, 4371, 4408, 7793, 7822, 7841, 7848, 7910, 7918, 7958, 8102, 8937, 8983, 9000, 9017, 9030, 9105, 9119, 9165, 9424, 9459, 9492, 9600, 9622, 9670, 9705, 12639, 12657, 19070, 19088, 23629, 23928, 23953, 27554, 27562, 27610, 27623, 28624, 28681, 28698, 29046, 29103, 29120, 29471, 30091, 30138, 30210, 30243, 30284, 30738, 30747, 35250, 37816, 37842, 39028, 39708, 40583, 40660, 40859, 40949, 41212, 41248, 41291, 41340, 41386, 41396, 41658, 41687, 41708, 41736, 41761, 41778, 41803, 41839, 41862, 41872, 41883, 41935, 41948, 41975, 42248, 42274, 42636, 42659, 42761, 42791, 42809, 42825, 43014, 43032, 43048, 43452, 43481, 43563, 43572, 43577, 48621, 48670, 48681, 48704, 48745, 48769, 48773, 49496, 50581, 50623, 50713, 50831, 50843, 50859, 50875, 50879, 51141, 51168, 51199, 51207, 51240, 51259, 51286, 51302, 51310, 51318, 51334, 51424, 51486, 51738, 51771, 51793, 51826, 51842, 51850, 51858, 51874, 51917, 51981, 52047, 52096, 52127, 52159, 52247, 52260, 52269, 52305, 52337, 52389, 52402, 52449, 52465, 52475, 52522, 52530, 52569, 52632, 52660, 52710, 52723, 52764, 52780, 52790, 52833, 52841, 52876, 53178, 53414, 53457, 53474, 53498, 53518, 53550, 53559, 53569, 53857, 53882, 53920, 53958, 53995, 54035, 54072, 54236, 54287, 54313, 54354, 54434, 54447, 54483, 54491, 54512, 54689, 54696, 54741, 54790, 54817, 54859, 54895, 54919, 54925, 54933, 54975, 55063, 55076, 55153, 55169, 55179, 55226, 55234, 55275, 55883, 55971, 55984, 56057, 56073, 56083, 56127, 56135, 56176, 56429, 56509, 56522, 56586, 56602, 56612, 56652, 56660, 56697, 56897, 56977, 56990, 57054, 57070, 57080, 57123, 57131, 57168, 57720, 57800, 57813, 57877, 57893, 57903, 57943, 57951, 57988, 58046, 58085, 58092, 58140, 58163, 58223, 58249, 58285, 58307, 58322, 58348, 58374, 58410, 58432, 58447, 58473, 58499, 58535, 58557, 58563, 58572, 58635, 58667, 58726, 58797, 58810, 58838, 62482, 62498, 62505, 62511, 62531, 62536, 62544, 62569, 62585, 62591, 62631, 62698, 62727, 62767, 62795, 62823, 62849, 62854, 62859, 62887, 62900, 62910, 62935, 62941, 62966, 63002, 63034, 63046, 63070, 63081, 63124, 63156, 63214, 63280, 63293, 63312, 63317, 63333, 63416, 63520, 63548, 64132, 64233, 64246, 64256, 64374, 65680, 65776, 65789, 65797, 65802, 65986, 66082, 66095, 66103, 66108, 66139, 66197, 66227, 68515, 68884, 68958, 69039, 69083, 69113, 69153, 69197, 69253, 69495, 69606, 69645, 69655, 69755, 69765, 69867, 70039, 70054, 70057, 70067, 70165, 70182, 70272, 70378, 70442, 70540, 70615, 71144, 71187, 71227, 71278, 71571, 71576, 71581, 71942, 72011, 72049, 72189, 72240, 72283, 72334, 73668, 73720, 73800, 73834, 73907, 73912, 73933, 73956, 73984, 74017, 74032, 74078, 74093, 74107, 74134, 74175, 74357, 74400, 74413, 74432, 74436, 74456, 74477, 74521, 74540, 74818, 74861, 74874, 74898, 74918, 74936, 74961, 74974, 75117, 75677, 75693, 75711, 75727, 75812, 75828, 75840, 75856, 76609, 76621, 76713, 76729, 76747, 76763, 76852, 76868, 76880, 76896, 77022, 77038, 77050, 77066, 77781, 77839, 77860, 77886, 77938, 77984, 77999, 78834, 78856, 78866, 79352, 79395, 79409, 79447, 79461, 79495, 79892, 79921, 80247, 82403, 82446, 82456, 82459, 82475, 82485, 82573, 82589, 82618, 82632, 82648, 82658, 82700, 82788, 82798, 82905, 82921, 82950, 82964, 82980, 82990, 83029, 85628, 87178, 87195, 87236, 87253, 87294, 87311, 87352, 87369, 87590, 87614, 87631, 87673, 87693, 87712, 87729, 87750, 87769, 87787, 87808, 87827, 87845, 87865, 87884, 87908, 87972, 89297, 90734, 91335, 91363, 91615, 91643, 95325, 95341]
Not compiled: [[2884 - 3935], [4838 - 5709], [5717 - 5764], [5717 - 5764], [6120 - 6260], [6265 - 6330], [6547 - 6616], [6621 - 6690], [6695 - 7565], [8172 - 8766], [9749 - 10256], [10261 - 10332], [10337 - 10565], [10570 - 10649], [12669 - 12775], [15251 - 15850], [22509 - 22639], [22644 - 22832], [22837 - 22965], [23163 - 23291], [32272 - 32540], [32545 - 32690], [32945 - 33199], [33706 - 33737], [33801 - 33832], [34401 - 34563], [36370 - 36615], [36620 - 36744], [37854 - 37926], [40336 - 40462], [43608 - 44490], [44495 - 46198], [46743 - 46814], [47292 - 47433], [58871 - 61188], [61193 - 62190], [66274 - 68208], [70625 - 70783], [78009 - 78569], [79465 - 79493], [80342 - 80520], [80717 - 81197], [85711 - 86339], [89503 - 89864], [91027 - 91185], [91677 - 91731], [91879 - 92361], [92928 - 93577], [93773 - 93882], [94073 - 94130], [94857 - 94883], [94959 - 95053]]

package:front_end/src/fasta/kernel/constant_int_folder.dart
Misses: [801, 1452, 1462, 1516, 1529, 1541, 1551, 2244, 2268, 2280, 2289, 2327, 2337, 2385, 2659, 2673, 2769, 2783, 2824, 2841, 2883, 2897, 2939, 2953, 2994, 3008, 3049, 3063, 3104, 3118, 3217, 3231, 3279, 3294, 3300, 3306, 3313, 3319, 3344, 3396, 3406, 3425, 3463, 3473, 3492, 3531, 3541, 3560, 3663, 3673, 3722]
Not compiled: [[4076 - 4143], [4148 - 4258], [4263 - 4318], [4323 - 4420], [4425 - 4545], [4550 - 4885], [4890 - 5320], [5325 - 7041], [7046 - 7449]]
Change-Id: Ic2334d46ebadfa062f93d7c074d7a1a1ee5f9ffc
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/162721
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/test/incremental_compiler_leak_test.dart b/pkg/front_end/test/incremental_compiler_leak_test.dart
index 326c2e2..e8927d1 100644
--- a/pkg/front_end/test/incremental_compiler_leak_test.dart
+++ b/pkg/front_end/test/incremental_compiler_leak_test.dart
@@ -1,11 +1,8 @@
 import 'dart:async';
 import 'dart:io';
 
-import "package:vm_service/vm_service.dart" as vmService;
-import "package:vm_service/vm_service_io.dart" as vmService;
-
-import "vm_service_heap_helper.dart" as helper;
 import "simple_stats.dart";
+import "vm_service_helper.dart" as vmService;
 
 const int limit = 10;
 
@@ -22,7 +19,7 @@
   ]);
 }
 
-class LeakFinder extends helper.LaunchingVMServiceHeapHelper {
+class LeakFinder extends vmService.LaunchingVMServiceHelper {
   @override
   Future<void> run() async {
     vmService.VM vm = await serviceClient.getVM();
diff --git a/pkg/front_end/test/spell_checking_list_tests.txt b/pkg/front_end/test/spell_checking_list_tests.txt
index f858baf..e985d0b 100644
--- a/pkg/front_end/test/spell_checking_list_tests.txt
+++ b/pkg/front_end/test/spell_checking_list_tests.txt
@@ -129,6 +129,8 @@
 corners
 costly
 cov
+coverage
+coverages
 cp
 csi
 ctrl
@@ -282,6 +284,7 @@
 heuristics
 hi
 hints
+hits
 home
 hoo
 hosted
@@ -403,6 +406,7 @@
 mismatched
 misnamed
 miss
+misses
 misspelled
 mistake
 mistakes
@@ -500,6 +504,7 @@
 response
 result1
 result2
+resuming
 retaining
 retainingpath
 retains
diff --git a/pkg/front_end/test/vm_service_coverage.dart b/pkg/front_end/test/vm_service_coverage.dart
new file mode 100644
index 0000000..b144e3d
--- /dev/null
+++ b/pkg/front_end/test/vm_service_coverage.dart
@@ -0,0 +1,142 @@
+import 'dart:async';
+
+import 'vm_service_helper.dart' as vmService;
+
+main(List<String> args) async {
+  CoverageHelper coverageHelper = new CoverageHelper();
+
+  List<String> allArgs = new List<String>();
+  allArgs.addAll([
+    "--disable-dart-dev",
+    "--enable-asserts",
+    "--pause_isolates_on_exit",
+  ]);
+  allArgs.addAll(args);
+
+  coverageHelper.start(allArgs);
+}
+
+class CoverageHelper extends vmService.LaunchingVMServiceHelper {
+  final bool forceCompilation;
+  final bool printHits;
+
+  CoverageHelper({this.forceCompilation: false, this.printHits: true});
+
+  @override
+  Future<void> run() async {
+    vmService.VM vm = await serviceClient.getVM();
+    if (vm.isolates.length != 1) {
+      throw "Expected 1 isolate, got ${vm.isolates.length}";
+    }
+    vmService.IsolateRef isolateRef = vm.isolates.single;
+    await waitUntilIsolateIsRunnable(isolateRef.id);
+    await serviceClient.resume(isolateRef.id);
+    Completer<String> cTimeout = new Completer();
+    Timer timer = new Timer(new Duration(minutes: 20), () {
+      cTimeout.complete("Timeout");
+      killProcess();
+    });
+
+    Completer<String> cRunDone = new Completer();
+    // ignore: unawaited_futures
+    waitUntilPaused(isolateRef.id).then((value) => cRunDone.complete("Done"));
+
+    await Future.any([cRunDone.future, cTimeout.future, cProcessExited.future]);
+
+    timer.cancel();
+
+    if (!await isPausedAtExit(isolateRef.id)) {
+      killProcess();
+      throw "Expected to be paused at exit, but is just paused!";
+    }
+
+    // Get and process coverage information.
+    Stopwatch stopwatch = new Stopwatch()..start();
+    vmService.SourceReport sourceReport = await serviceClient.getSourceReport(
+        isolateRef.id, [vmService.SourceReportKind.kCoverage],
+        forceCompile: forceCompilation);
+    print("Got source report from VM in ${stopwatch.elapsedMilliseconds} ms");
+    stopwatch.reset();
+    Map<Uri, Coverage> coverages = {};
+    for (vmService.SourceReportRange range in sourceReport.ranges) {
+      vmService.ScriptRef script = sourceReport.scripts[range.scriptIndex];
+      Uri scriptUri = Uri.parse(script.uri);
+      if (!includeCoverageFor(scriptUri)) continue;
+      Coverage coverage = coverages[scriptUri] ??= new Coverage();
+
+      vmService.SourceReportCoverage sourceReportCoverage = range.coverage;
+      if (sourceReportCoverage == null) {
+        // Range not compiled. Record the range if provided.
+        assert(!range.compiled);
+        if (range.startPos >= 0 || range.endPos >= 0) {
+          coverage.notCompiled
+              .add(new StartEndPair(range.startPos, range.endPos));
+        }
+        continue;
+      }
+      coverage.hits.addAll(sourceReportCoverage.hits);
+      coverage.misses.addAll(sourceReportCoverage.misses);
+    }
+    print("Processed source report from VM in "
+        "${stopwatch.elapsedMilliseconds} ms");
+    stopwatch.reset();
+
+    // It's paused at exit, so resuming should allow us to exit.
+    await serviceClient.resume(isolateRef.id);
+
+    for (MapEntry<Uri, Coverage> entry in coverages.entries) {
+      assert(entry.value.hits.intersection(entry.value.misses).isEmpty);
+      if (entry.value.hits.isEmpty &&
+          entry.value.misses.isEmpty &&
+          entry.value.notCompiled.isEmpty) {
+        continue;
+      }
+      print(entry.key);
+      if (printHits) {
+        print("Hits: ${entry.value.hits.toList()..sort()}");
+      }
+      print("Misses: ${entry.value.misses.toList()..sort()}");
+      print("Not compiled: ${entry.value.notCompiled.toList()..sort()}");
+      print("");
+    }
+  }
+
+  Completer<String> cProcessExited = new Completer();
+  void processExited(int exitCode) {
+    cProcessExited.complete("Exit");
+  }
+
+  bool includeCoverageFor(Uri uri) {
+    if (uri.scheme == "dart") {
+      return false;
+    }
+    if (uri.scheme == "package") {
+      return uri.pathSegments.first == "front_end" ||
+          uri.pathSegments.first == "_fe_analyzer_shared" ||
+          uri.pathSegments.first == "kernel";
+    }
+    return true;
+  }
+}
+
+class Coverage {
+  final Set<int> hits = {};
+  final Set<int> misses = {};
+  final Set<StartEndPair> notCompiled = {};
+}
+
+class StartEndPair implements Comparable {
+  final int startPos;
+  final int endPos;
+
+  StartEndPair(this.startPos, this.endPos);
+
+  String toString() => "[$startPos - $endPos]";
+
+  @override
+  int compareTo(Object other) {
+    if (other is! StartEndPair) return -1;
+    StartEndPair o = other;
+    return startPos - o.startPos;
+  }
+}
diff --git a/pkg/front_end/test/vm_service_coverage_constant_evaluator.dart b/pkg/front_end/test/vm_service_coverage_constant_evaluator.dart
new file mode 100644
index 0000000..4cab31b
--- /dev/null
+++ b/pkg/front_end/test/vm_service_coverage_constant_evaluator.dart
@@ -0,0 +1,27 @@
+import 'vm_service_coverage.dart' as helper;
+
+main(List<String> args) async {
+  CoverageHelper coverageHelper = new CoverageHelper();
+
+  List<String> allArgs = new List<String>();
+  allArgs.addAll([
+    "--disable-dart-dev",
+    "--enable-asserts",
+    "--pause_isolates_on_exit",
+  ]);
+  allArgs.addAll(args);
+
+  coverageHelper.start(allArgs);
+}
+
+class CoverageHelper extends helper.CoverageHelper {
+  CoverageHelper() : super(printHits: false);
+
+  bool includeCoverageFor(Uri uri) {
+    if (uri.scheme != "package") return false;
+    if (uri.path.startsWith("front_end/src/fasta/kernel/constant_")) {
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/pkg/front_end/test/vm_service_heap_finder.dart b/pkg/front_end/test/vm_service_heap_finder.dart
index ea3bcea..2e85184 100644
--- a/pkg/front_end/test/vm_service_heap_finder.dart
+++ b/pkg/front_end/test/vm_service_heap_finder.dart
@@ -3,7 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import "dart:io";
-import "vm_service_heap_helper.dart";
+
+import "vm_service_helper.dart" as vmService;
 
 class Foo {
   final String x;
@@ -31,7 +32,7 @@
   foos.add(new Foo("!", 44));
 
   if (connectTo == null) connectTo = ask("Connect to");
-  VMServiceHeapHelperBase vm = VMServiceHeapHelperBase();
+  VMServiceHeapHelperPrinter vm = VMServiceHeapHelperPrinter();
   await vm.connect(Uri.parse(connectTo.trim()));
   String isolateId = await vm.getIsolateId();
   if (classToFind == null) classToFind = ask("Find what class");
@@ -61,3 +62,97 @@
   stdout.write("$question: ");
   return stdin.readLineSync();
 }
+
+class VMServiceHeapHelperPrinter extends vmService.VMServiceHelper {
+  Future<void> printAllocationProfile(String isolateId, {String filter}) async {
+    await waitUntilIsolateIsRunnable(isolateId);
+    vmService.AllocationProfile allocationProfile =
+        await serviceClient.getAllocationProfile(isolateId);
+    for (vmService.ClassHeapStats member in allocationProfile.members) {
+      if (filter != null) {
+        if (member.classRef.name != filter) continue;
+      } else {
+        if (member.classRef.name == "") continue;
+        if (member.instancesCurrent == 0) continue;
+      }
+      vmService.Class c =
+          await serviceClient.getObject(isolateId, member.classRef.id);
+      if (c.location?.script?.uri == null) continue;
+      print("${member.classRef.name}: ${member.instancesCurrent}");
+    }
+  }
+
+  Future<void> filterAndPrintInstances(String isolateId, String filter,
+      String fieldName, Set<String> fieldValues) async {
+    await waitUntilIsolateIsRunnable(isolateId);
+    vmService.AllocationProfile allocationProfile =
+        await serviceClient.getAllocationProfile(isolateId);
+    for (vmService.ClassHeapStats member in allocationProfile.members) {
+      if (member.classRef.name != filter) continue;
+      vmService.Class c =
+          await serviceClient.getObject(isolateId, member.classRef.id);
+      if (c.location?.script?.uri == null) continue;
+      print("${member.classRef.name}: ${member.instancesCurrent}");
+      print(c.location.script.uri);
+
+      vmService.InstanceSet instances = await serviceClient.getInstances(
+          isolateId, member.classRef.id, 10000);
+      int instanceNum = 0;
+      for (vmService.ObjRef instance in instances.instances) {
+        instanceNum++;
+        vmService.Obj receivedObject =
+            await serviceClient.getObject(isolateId, instance.id);
+        if (receivedObject is! vmService.Instance) continue;
+        vmService.Instance object = receivedObject;
+        for (vmService.BoundField field in object.fields) {
+          if (field.decl.name == fieldName) {
+            if (field.value is vmService.Sentinel) continue;
+            vmService.Obj receivedValue =
+                await serviceClient.getObject(isolateId, field.value.id);
+            if (receivedValue is! vmService.Instance) continue;
+            String value = (receivedValue as vmService.Instance).valueAsString;
+            if (!fieldValues.contains(value)) continue;
+            print("${instanceNum}: ${field.decl.name}: "
+                "${value} --- ${instance.id}");
+          }
+        }
+      }
+    }
+    print("Done!");
+  }
+
+  Future<void> printRetainingPaths(String isolateId, String filter) async {
+    await waitUntilIsolateIsRunnable(isolateId);
+    vmService.AllocationProfile allocationProfile =
+        await serviceClient.getAllocationProfile(isolateId);
+    for (vmService.ClassHeapStats member in allocationProfile.members) {
+      if (member.classRef.name != filter) continue;
+      vmService.Class c =
+          await serviceClient.getObject(isolateId, member.classRef.id);
+      print("Found ${c.name} (location: ${c.location})");
+      print("${member.classRef.name}: "
+          "(instancesCurrent: ${member.instancesCurrent})");
+      print("");
+
+      vmService.InstanceSet instances = await serviceClient.getInstances(
+          isolateId, member.classRef.id, 10000);
+      print(" => Got ${instances.instances.length} instances");
+      print("");
+
+      for (vmService.ObjRef instance in instances.instances) {
+        vmService.Obj receivedObject =
+            await serviceClient.getObject(isolateId, instance.id);
+        print("Instance: $receivedObject");
+        vmService.RetainingPath retainingPath =
+            await serviceClient.getRetainingPath(isolateId, instance.id, 1000);
+        print("Retaining path: (length ${retainingPath.length}");
+        for (int i = 0; i < retainingPath.elements.length; i++) {
+          print("  [$i] = ${retainingPath.elements[i]}");
+        }
+
+        print("");
+      }
+    }
+    print("Done!");
+  }
+}
diff --git a/pkg/front_end/test/vm_service_heap_helper.dart b/pkg/front_end/test/vm_service_heap_helper.dart
index d801f3d..497c0b3 100644
--- a/pkg/front_end/test/vm_service_heap_helper.dart
+++ b/pkg/front_end/test/vm_service_heap_helper.dart
@@ -2,288 +2,11 @@
 // 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:convert";
-import "dart:io";
-
-import "package:vm_service/vm_service.dart" as vmService;
-import "package:vm_service/vm_service_io.dart" as vmService;
-
 import "dijkstras_sssp_algorithm.dart";
-
-class VMServiceHeapHelperBase {
-  vmService.VmService _serviceClient;
-  vmService.VmService get serviceClient => _serviceClient;
-
-  VMServiceHeapHelperBase();
-
-  Future connect(Uri observatoryUri) async {
-    String path = observatoryUri.path;
-    if (!path.endsWith("/")) path += "/";
-    String wsUriString = 'ws://${observatoryUri.authority}${path}ws';
-    _serviceClient = await vmService.vmServiceConnectUri(wsUriString,
-        log: const StdOutLog());
-  }
-
-  Future disconnect() async {
-    await _serviceClient.dispose();
-  }
-
-  Future<bool> waitUntilPaused(String isolateId) async {
-    int nulls = 0;
-    while (true) {
-      bool result = await _isPaused(isolateId);
-      if (result == null) {
-        nulls++;
-        if (nulls > 5) {
-          // We've now asked for the isolate 5 times and in all cases gotten
-          // `Sentinel`. Most likely things aren't working for whatever reason.
-          return false;
-        }
-      } else if (result) {
-        return true;
-      } else {
-        await Future.delayed(const Duration(milliseconds: 100));
-      }
-    }
-  }
-
-  Future<bool> _isPaused(String isolateId) async {
-    dynamic tmp = await _serviceClient.getIsolate(isolateId);
-    if (tmp is vmService.Isolate) {
-      vmService.Isolate isolate = tmp;
-      if (isolate.pauseEvent.kind != "Resume") return true;
-      return false;
-    }
-    return null;
-  }
-
-  Future<bool> _isPausedAtStart(String isolateId) async {
-    dynamic tmp = await _serviceClient.getIsolate(isolateId);
-    if (tmp is vmService.Isolate) {
-      vmService.Isolate isolate = tmp;
-      return isolate.pauseEvent.kind == "PauseStart";
-    }
-    return false;
-  }
-
-  Future<vmService.AllocationProfile> forceGC(String isolateId) async {
-    await waitUntilIsolateIsRunnable(isolateId);
-    int expectGcAfter = new DateTime.now().millisecondsSinceEpoch;
-    while (true) {
-      vmService.AllocationProfile allocationProfile;
-      try {
-        allocationProfile =
-            await _serviceClient.getAllocationProfile(isolateId, gc: true);
-      } catch (e) {
-        print(e.runtimeType);
-        rethrow;
-      }
-      if (allocationProfile.dateLastServiceGC != null &&
-          allocationProfile.dateLastServiceGC >= expectGcAfter) {
-        return allocationProfile;
-      }
-    }
-  }
-
-  Future<bool> isIsolateRunnable(String isolateId) async {
-    dynamic tmp = await _serviceClient.getIsolate(isolateId);
-    if (tmp is vmService.Isolate) {
-      vmService.Isolate isolate = tmp;
-      return isolate.runnable;
-    }
-    return null;
-  }
-
-  Future<void> waitUntilIsolateIsRunnable(String isolateId) async {
-    int nulls = 0;
-    while (true) {
-      bool result = await isIsolateRunnable(isolateId);
-      if (result == null) {
-        nulls++;
-        if (nulls > 5) {
-          // We've now asked for the isolate 5 times and in all cases gotten
-          // `Sentinel`. Most likely things aren't working for whatever reason.
-          return;
-        }
-      } else if (result) {
-        return;
-      } else {
-        await Future.delayed(const Duration(milliseconds: 100));
-      }
-    }
-  }
-
-  Future<void> printAllocationProfile(String isolateId, {String filter}) async {
-    await waitUntilIsolateIsRunnable(isolateId);
-    vmService.AllocationProfile allocationProfile =
-        await _serviceClient.getAllocationProfile(isolateId);
-    for (vmService.ClassHeapStats member in allocationProfile.members) {
-      if (filter != null) {
-        if (member.classRef.name != filter) continue;
-      } else {
-        if (member.classRef.name == "") continue;
-        if (member.instancesCurrent == 0) continue;
-      }
-      vmService.Class c =
-          await _serviceClient.getObject(isolateId, member.classRef.id);
-      if (c.location?.script?.uri == null) continue;
-      print("${member.classRef.name}: ${member.instancesCurrent}");
-    }
-  }
-
-  Future<void> filterAndPrintInstances(String isolateId, String filter,
-      String fieldName, Set<String> fieldValues) async {
-    await waitUntilIsolateIsRunnable(isolateId);
-    vmService.AllocationProfile allocationProfile =
-        await _serviceClient.getAllocationProfile(isolateId);
-    for (vmService.ClassHeapStats member in allocationProfile.members) {
-      if (member.classRef.name != filter) continue;
-      vmService.Class c =
-          await _serviceClient.getObject(isolateId, member.classRef.id);
-      if (c.location?.script?.uri == null) continue;
-      print("${member.classRef.name}: ${member.instancesCurrent}");
-      print(c.location.script.uri);
-
-      vmService.InstanceSet instances = await _serviceClient.getInstances(
-          isolateId, member.classRef.id, 10000);
-      int instanceNum = 0;
-      for (vmService.ObjRef instance in instances.instances) {
-        instanceNum++;
-        var receivedObject =
-            await _serviceClient.getObject(isolateId, instance.id);
-        if (receivedObject is! vmService.Instance) continue;
-        vmService.Instance object = receivedObject;
-        for (vmService.BoundField field in object.fields) {
-          if (field.decl.name == fieldName) {
-            if (field.value is vmService.Sentinel) continue;
-            var receivedValue =
-                await _serviceClient.getObject(isolateId, field.value.id);
-            if (receivedValue is! vmService.Instance) continue;
-            String value = (receivedValue as vmService.Instance).valueAsString;
-            if (!fieldValues.contains(value)) continue;
-            print("${instanceNum}: ${field.decl.name}: "
-                "${value} --- ${instance.id}");
-          }
-        }
-      }
-    }
-    print("Done!");
-  }
-
-  Future<void> printRetainingPaths(String isolateId, String filter) async {
-    await waitUntilIsolateIsRunnable(isolateId);
-    vmService.AllocationProfile allocationProfile =
-        await _serviceClient.getAllocationProfile(isolateId);
-    for (vmService.ClassHeapStats member in allocationProfile.members) {
-      if (member.classRef.name != filter) continue;
-      vmService.Class c =
-          await _serviceClient.getObject(isolateId, member.classRef.id);
-      print("Found ${c.name} (location: ${c.location})");
-      print("${member.classRef.name}: "
-          "(instancesCurrent: ${member.instancesCurrent})");
-      print("");
-
-      vmService.InstanceSet instances = await _serviceClient.getInstances(
-          isolateId, member.classRef.id, 10000);
-      print(" => Got ${instances.instances.length} instances");
-      print("");
-
-      for (vmService.ObjRef instance in instances.instances) {
-        var receivedObject =
-            await _serviceClient.getObject(isolateId, instance.id);
-        print("Instance: $receivedObject");
-        vmService.RetainingPath retainingPath =
-            await _serviceClient.getRetainingPath(isolateId, instance.id, 1000);
-        print("Retaining path: (length ${retainingPath.length}");
-        for (int i = 0; i < retainingPath.elements.length; i++) {
-          print("  [$i] = ${retainingPath.elements[i]}");
-        }
-
-        print("");
-      }
-    }
-    print("Done!");
-  }
-
-  Future<String> getIsolateId() async {
-    vmService.VM vm = await _serviceClient.getVM();
-    if (vm.isolates.length != 1) {
-      throw "Expected 1 isolate, got ${vm.isolates.length}";
-    }
-    vmService.IsolateRef isolateRef = vm.isolates.single;
-    return isolateRef.id;
-  }
-}
-
-abstract class LaunchingVMServiceHeapHelper extends VMServiceHeapHelperBase {
-  Process _process;
-  Process get process => _process;
-
-  bool _started = false;
-
-  void start(List<String> scriptAndArgs,
-      {void stdinReceiver(String line),
-      void stderrReceiver(String line)}) async {
-    if (_started) throw "Already started";
-    _started = true;
-    _process = await Process.start(
-        Platform.resolvedExecutable,
-        ["--pause_isolates_on_start", "--enable-vm-service=0"]
-          ..addAll(scriptAndArgs));
-    _process.stdout
-        .transform(utf8.decoder)
-        .transform(new LineSplitter())
-        .listen((line) {
-      const kObservatoryListening = 'Observatory listening on ';
-      if (line.startsWith(kObservatoryListening)) {
-        Uri observatoryUri =
-            Uri.parse(line.substring(kObservatoryListening.length));
-        _setupAndRun(observatoryUri).catchError((e, st) {
-          // Manually kill the process or it will leak,
-          // see http://dartbug.com/42918
-          killProcess();
-          // This seems to rethrow.
-          throw e;
-        });
-      }
-      if (stdinReceiver != null) {
-        stdinReceiver(line);
-      } else {
-        stdout.writeln("> $line");
-      }
-    });
-    _process.stderr
-        .transform(utf8.decoder)
-        .transform(new LineSplitter())
-        .listen((line) {
-      if (stderrReceiver != null) {
-        stderrReceiver(line);
-      } else {
-        stderr.writeln("> $line");
-      }
-    });
-    // ignore: unawaited_futures
-    _process.exitCode.then((value) {
-      processExited(value);
-    });
-  }
-
-  void processExited(int exitCode) {}
-
-  void killProcess() {
-    _process.kill();
-  }
-
-  Future _setupAndRun(Uri observatoryUri) async {
-    await connect(observatoryUri);
-    await run();
-  }
-
-  Future<void> run();
-}
+import "vm_service_helper.dart" as vmService;
 
 class VMServiceHeapHelperSpecificExactLeakFinder
-    extends LaunchingVMServiceHeapHelper {
+    extends vmService.LaunchingVMServiceHelper {
   final Map<Uri, Map<String, List<String>>> _interests =
       new Map<Uri, Map<String, List<String>>>();
   final Map<Uri, Map<String, List<String>>> _prettyPrints =
@@ -326,7 +49,7 @@
   }
 
   void pause() async {
-    await _serviceClient.pause(_isolateRef.id);
+    await serviceClient.pause(_isolateRef.id);
   }
 
   vmService.VM _vm;
@@ -336,7 +59,7 @@
 
   /// Best effort check if the isolate is idle.
   Future<bool> isIdle() async {
-    dynamic tmp = await _serviceClient.getIsolate(_isolateRef.id);
+    dynamic tmp = await serviceClient.getIsolate(_isolateRef.id);
     if (tmp is vmService.Isolate) {
       vmService.Isolate isolate = tmp;
       return isolate.pauseEvent.topFrame == null;
@@ -346,15 +69,15 @@
 
   @override
   Future<void> run() async {
-    _vm = await _serviceClient.getVM();
+    _vm = await serviceClient.getVM();
     if (_vm.isolates.length != 1) {
       throw "Expected 1 isolate, got ${_vm.isolates.length}";
     }
     _isolateRef = _vm.isolates.single;
     await forceGC(_isolateRef.id);
 
-    assert(await _isPausedAtStart(_isolateRef.id));
-    await _serviceClient.resume(_isolateRef.id);
+    assert(await isPausedAtStart(_isolateRef.id));
+    await serviceClient.resume(_isolateRef.id);
 
     _iterationNumber = 1;
     while (true) {
@@ -364,7 +87,7 @@
 
       vmService.HeapSnapshotGraph heapSnapshotGraph =
           await vmService.HeapSnapshotGraph.getSnapshot(
-              _serviceClient, _isolateRef);
+              serviceClient, _isolateRef);
 
       Set<String> duplicatePrints = {};
       Map<String, List<vmService.HeapSnapshotObject>> groupedByToString = {};
@@ -396,7 +119,7 @@
         }
       }
 
-      await _serviceClient.resume(_isolateRef.id);
+      await serviceClient.resume(_isolateRef.id);
       _iterationNumber++;
     }
   }
@@ -695,20 +418,6 @@
   Interest(this.uri, this.className, this.fieldNames);
 }
 
-class StdOutLog implements vmService.Log {
-  const StdOutLog();
-
-  @override
-  void severe(String message) {
-    print("> SEVERE: $message");
-  }
-
-  @override
-  void warning(String message) {
-    print("> WARNING: $message");
-  }
-}
-
 HeapGraph convertHeapGraph(vmService.HeapSnapshotGraph graph) {
   HeapGraphClassSentinel classSentinel = new HeapGraphClassSentinel();
   List<HeapGraphClassActual> classes =
diff --git a/pkg/front_end/test/vm_service_helper.dart b/pkg/front_end/test/vm_service_helper.dart
new file mode 100644
index 0000000..a80eb5a
--- /dev/null
+++ b/pkg/front_end/test/vm_service_helper.dart
@@ -0,0 +1,215 @@
+// 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:convert";
+import "dart:io";
+
+import "package:vm_service/vm_service.dart" as vmService;
+import "package:vm_service/vm_service_io.dart" as vmService;
+
+export "package:vm_service/vm_service.dart";
+export "package:vm_service/vm_service_io.dart";
+
+class VMServiceHelper {
+  vmService.VmService _serviceClient;
+  vmService.VmService get serviceClient => _serviceClient;
+
+  VMServiceHelper();
+
+  Future connect(Uri observatoryUri) async {
+    String path = observatoryUri.path;
+    if (!path.endsWith("/")) path += "/";
+    String wsUriString = 'ws://${observatoryUri.authority}${path}ws';
+    _serviceClient = await vmService.vmServiceConnectUri(wsUriString,
+        log: const StdOutLog());
+  }
+
+  Future disconnect() async {
+    await _serviceClient.dispose();
+  }
+
+  Future<bool> waitUntilPaused(String isolateId) async {
+    int nulls = 0;
+    while (true) {
+      bool result = await isPaused(isolateId);
+      if (result == null) {
+        nulls++;
+        if (nulls > 5) {
+          // We've now asked for the isolate 5 times and in all cases gotten
+          // `Sentinel`. Most likely things aren't working for whatever reason.
+          return false;
+        }
+      } else if (result) {
+        return true;
+      } else {
+        await Future.delayed(const Duration(milliseconds: 100));
+      }
+    }
+  }
+
+  Future<bool> isPaused(String isolateId) async {
+    dynamic tmp = await _serviceClient.getIsolate(isolateId);
+    if (tmp is vmService.Isolate) {
+      vmService.Isolate isolate = tmp;
+      if (isolate.pauseEvent.kind != "Resume") return true;
+      return false;
+    }
+    return null;
+  }
+
+  Future<bool> isPausedAtStart(String isolateId) async {
+    dynamic tmp = await _serviceClient.getIsolate(isolateId);
+    if (tmp is vmService.Isolate) {
+      vmService.Isolate isolate = tmp;
+      return isolate.pauseEvent.kind == "PauseStart";
+    }
+    return false;
+  }
+
+  Future<bool> isPausedAtExit(String isolateId) async {
+    dynamic tmp = await _serviceClient.getIsolate(isolateId);
+    if (tmp is vmService.Isolate) {
+      vmService.Isolate isolate = tmp;
+      return isolate.pauseEvent.kind == "PauseExit";
+    }
+    return false;
+  }
+
+  Future<vmService.AllocationProfile> forceGC(String isolateId) async {
+    await waitUntilIsolateIsRunnable(isolateId);
+    int expectGcAfter = new DateTime.now().millisecondsSinceEpoch;
+    while (true) {
+      vmService.AllocationProfile allocationProfile;
+      try {
+        allocationProfile =
+            await _serviceClient.getAllocationProfile(isolateId, gc: true);
+      } catch (e) {
+        print(e.runtimeType);
+        rethrow;
+      }
+      if (allocationProfile.dateLastServiceGC != null &&
+          allocationProfile.dateLastServiceGC >= expectGcAfter) {
+        return allocationProfile;
+      }
+    }
+  }
+
+  Future<bool> isIsolateRunnable(String isolateId) async {
+    dynamic tmp = await _serviceClient.getIsolate(isolateId);
+    if (tmp is vmService.Isolate) {
+      vmService.Isolate isolate = tmp;
+      return isolate.runnable;
+    }
+    return null;
+  }
+
+  Future<void> waitUntilIsolateIsRunnable(String isolateId) async {
+    int nulls = 0;
+    while (true) {
+      bool result = await isIsolateRunnable(isolateId);
+      if (result == null) {
+        nulls++;
+        if (nulls > 5) {
+          // We've now asked for the isolate 5 times and in all cases gotten
+          // `Sentinel`. Most likely things aren't working for whatever reason.
+          return;
+        }
+      } else if (result) {
+        return;
+      } else {
+        await Future.delayed(const Duration(milliseconds: 100));
+      }
+    }
+  }
+
+  Future<String> getIsolateId() async {
+    vmService.VM vm = await _serviceClient.getVM();
+    if (vm.isolates.length != 1) {
+      throw "Expected 1 isolate, got ${vm.isolates.length}";
+    }
+    vmService.IsolateRef isolateRef = vm.isolates.single;
+    return isolateRef.id;
+  }
+}
+
+class StdOutLog implements vmService.Log {
+  const StdOutLog();
+
+  @override
+  void severe(String message) {
+    print("> SEVERE: $message");
+  }
+
+  @override
+  void warning(String message) {
+    print("> WARNING: $message");
+  }
+}
+
+abstract class LaunchingVMServiceHelper extends VMServiceHelper {
+  Process _process;
+  Process get process => _process;
+
+  bool _started = false;
+
+  void start(List<String> scriptAndArgs,
+      {void stdinReceiver(String line),
+      void stderrReceiver(String line)}) async {
+    if (_started) throw "Already started";
+    _started = true;
+    _process = await Process.start(
+        Platform.resolvedExecutable,
+        ["--pause_isolates_on_start", "--enable-vm-service=0"]
+          ..addAll(scriptAndArgs));
+    _process.stdout
+        .transform(utf8.decoder)
+        .transform(new LineSplitter())
+        .listen((line) {
+      const kObservatoryListening = 'Observatory listening on ';
+      if (line.startsWith(kObservatoryListening)) {
+        Uri observatoryUri =
+            Uri.parse(line.substring(kObservatoryListening.length));
+        _setupAndRun(observatoryUri).catchError((e, st) {
+          // Manually kill the process or it will leak,
+          // see http://dartbug.com/42918
+          killProcess();
+          // This seems to rethrow.
+          throw e;
+        });
+      }
+      if (stdinReceiver != null) {
+        stdinReceiver(line);
+      } else {
+        stdout.writeln("> $line");
+      }
+    });
+    _process.stderr
+        .transform(utf8.decoder)
+        .transform(new LineSplitter())
+        .listen((line) {
+      if (stderrReceiver != null) {
+        stderrReceiver(line);
+      } else {
+        stderr.writeln("> $line");
+      }
+    });
+    // ignore: unawaited_futures
+    _process.exitCode.then((value) {
+      processExited(value);
+    });
+  }
+
+  void processExited(int exitCode) {}
+
+  void killProcess() {
+    _process.kill();
+  }
+
+  Future _setupAndRun(Uri observatoryUri) async {
+    await connect(observatoryUri);
+    await run();
+  }
+
+  Future<void> run();
+}