Make coverage, like, really fast (#30811)


diff --git a/packages/flutter_tools/lib/src/base/io.dart b/packages/flutter_tools/lib/src/base/io.dart
index 862f43b..1c9761e 100644
--- a/packages/flutter_tools/lib/src/base/io.dart
+++ b/packages/flutter_tools/lib/src/base/io.dart
@@ -37,6 +37,7 @@
 export 'dart:io'
     show
         BytesBuilder,
+        CompressionOptions,
         // Directory         NO! Use `file_system.dart`
         exitCode,
         // File              NO! Use `file_system.dart`
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 2dc9d6f..9a0924d 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -149,7 +149,7 @@
 
     CoverageCollector collector;
     if (argResults['coverage'] || argResults['merge-coverage']) {
-      collector = CoverageCollector();
+      collector = CoverageCollector(await FlutterProject.current());
     }
 
     final bool machine = argResults['machine'];
diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart
index 4220c85..320bc04 100644
--- a/packages/flutter_tools/lib/src/test/coverage_collector.dart
+++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart
@@ -14,12 +14,18 @@
 import '../base/process_manager.dart';
 import '../dart/package_map.dart';
 import '../globals.dart';
+import '../project.dart';
+import '../vmservice.dart';
 
 import 'watcher.dart';
 
 /// A class that's used to collect coverage data during tests.
 class CoverageCollector extends TestWatcher {
+  CoverageCollector(this.flutterProject, {this.coverageDirectory});
+
   Map<String, dynamic> _globalHitmap;
+  final Directory coverageDirectory;
+  final FlutterProject flutterProject;
 
   @override
   Future<void> handleFinishedTest(ProcessEvent event) async {
@@ -43,7 +49,6 @@
   Future<void> collectCoverage(Process process, Uri observatoryUri) async {
     assert(process != null);
     assert(observatoryUri != null);
-
     final int pid = process.pid;
     printTrace('pid $pid: collecting coverage data from $observatoryUri...');
 
@@ -52,7 +57,14 @@
       .then<void>((int code) {
         throw Exception('Failed to collect coverage, process terminated prematurely with exit code $code.');
       });
-    final Future<void> collectionComplete = coverage.collect(observatoryUri, false, false)
+    final Future<void> collectionComplete = collect(observatoryUri, (String libraryName) {
+      // If we have a specified coverage directory or could not find the package name, then
+      // accept all libraries.
+      if (coverageDirectory != null) {
+        return true;
+      }
+      return libraryName.contains(flutterProject.manifest.appName);
+    })
       .then<void>((Map<String, dynamic> result) {
         if (result == null)
           throw Exception('Failed to collect coverage.');
@@ -141,3 +153,136 @@
     return true;
   }
 }
+
+Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libraryPredicate) async {
+  final VMService vmService = await VMService.connect(serviceUri, compression: CompressionOptions.compressionOff);
+  await vmService.getVM();
+  return _getAllCoverage(vmService, libraryPredicate);
+}
+
+
+Future<Map<String, dynamic>> _getAllCoverage(VMService service, bool Function(String) libraryPredicate) async {
+  await service.getVM();
+  final List<Map<String, dynamic>> coverage = <Map<String, dynamic>>[];
+  for (Isolate isolateRef in service.vm.isolates) {
+    await isolateRef.load();
+    final Map<String, dynamic> scriptList = await isolateRef.invokeRpcRaw('getScripts', params: <String, dynamic>{'isolateId': isolateRef.id});
+    final List<Future<void>> futures = <Future<void>>[];
+
+    final Map<String, Map<String, dynamic>> scripts = <String, Map<String, dynamic>>{};
+    final Map<String, Map<String, dynamic>> sourceReports = <String, Map<String, dynamic>>{};
+    // For each ScriptRef loaded into the VM, load the corresponding Script and
+    // SourceReport object.
+    for (Map<String, dynamic> script in scriptList['scripts']) {
+      if (!libraryPredicate(script['uri'])) {
+        continue;
+      }
+      final String scriptId = script['id'];
+      futures.add(
+        isolateRef.invokeRpcRaw('getSourceReport', params: <String, dynamic>{
+          'forceCompile': true,
+          'scriptId': scriptId,
+          'isolateId': isolateRef.id,
+          'reports': <String>['Coverage'],
+        })
+        .then((Map<String, dynamic> report) {
+          sourceReports[scriptId] = report;
+        })
+      );
+      futures.add(
+        isolateRef.invokeRpcRaw('getObject', params: <String, dynamic>{
+          'isolateId': isolateRef.id,
+          'objectId': scriptId,
+        })
+        .then((Map<String, dynamic> script) {
+          scripts[scriptId] = script;
+        })
+      );
+    }
+    await Future.wait(futures);
+    _buildCoverageMap(scripts, sourceReports, coverage);
+  }
+  return <String, dynamic>{'type': 'CodeCoverage', 'coverage': coverage};
+}
+
+// Build a hitmap of Uri -> Line -> Hit Count for each script object.
+void _buildCoverageMap(
+  Map<String, Map<String, dynamic>> scripts,
+  Map<String, Map<String, dynamic>> sourceReports,
+  List<Map<String, dynamic>> coverage,
+) {
+  final Map<String, Map<int, int>> hitMaps = <String, Map<int, int>>{};
+  for (String scriptId in scripts.keys) {
+    final Map<String, dynamic> sourceReport = sourceReports[scriptId];
+    for (Map<String, dynamic> range in sourceReport['ranges']) {
+      final Map<String, dynamic> coverage = range['coverage'];
+      final Map<String, dynamic> scriptRef = sourceReport['scripts'][range['scriptIndex']];
+      final String uri = scriptRef['uri'];
+
+      hitMaps[uri] ??= <int, int>{};
+      final Map<int, int> hitMap = hitMaps[uri];
+      final List<dynamic> hits = coverage['hits'];
+      final List<dynamic> misses = coverage['misses'];
+      final List<dynamic> tokenPositions = scripts[scriptRef['id']]['tokenPosTable'];
+      if (hits != null) {
+        for (int hit in hits) {
+          final int line = _lineAndColumn(hit, tokenPositions)[0];
+          final int current = hitMap[line] ?? 0;
+          hitMap[line] = current + 1;
+        }
+      }
+      if (misses != null) {
+        for (int miss in misses) {
+          final int line = _lineAndColumn(miss, tokenPositions)[0];
+          hitMap[line] ??= 0;
+        }
+      }
+    }
+  }
+  hitMaps.forEach((String uri, Map<int, int> hitMap) {
+    coverage.add(_toScriptCoverageJson(uri, hitMap));
+  });
+}
+
+// Binary search the token position table for the line and column which
+// corresponds to each token position.
+// The format of this table is described in https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script
+List<int> _lineAndColumn(int position, List<dynamic> tokenPositions) {
+  int min = 0;
+  int max = tokenPositions.length;
+  while (min < max) {
+    final int mid = min + ((max - min) >> 1);
+    final List<dynamic> row = tokenPositions[mid];
+    if (row[1] > position) {
+      max = mid;
+    } else {
+      for (int i = 1; i < row.length; i += 2) {
+        if (row[i] == position) {
+          return <int>[row.first, row[i + 1]];
+        }
+      }
+      min = mid + 1;
+    }
+  }
+  throw StateError('Unreachable');
+}
+
+// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
+Map<String, dynamic> _toScriptCoverageJson(String scriptUri, Map<int, int> hitMap) {
+  final Map<String, dynamic> json = <String, dynamic>{};
+  final List<int> hits = <int>[];
+  hitMap.forEach((int line, int hitCount) {
+    hits.add(line);
+    hits.add(hitCount);
+  });
+  json['source'] = scriptUri;
+  json['script'] = <String, dynamic>{
+    'type': '@Script',
+    'fixedId': true,
+    'id': 'libraries/1/scripts/${Uri.encodeComponent(scriptUri)}',
+    'uri': scriptUri,
+    '_kind': 'library',
+  };
+  json['hits'] = hits;
+  return json;
+}
diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart
index 08ccfc6..14ed2d4 100644
--- a/packages/flutter_tools/lib/src/test/runner.dart
+++ b/packages/flutter_tools/lib/src/test/runner.dart
@@ -37,6 +37,7 @@
   @required int concurrency,
   FlutterProject flutterProject,
   String icudtlPath,
+  Directory coverageDirectory,
 }) async {
   // Compute the command-line arguments for package:test.
   final List<String> testArgs = <String>[];
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index 6d7e4bc..05a642c 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -24,10 +24,10 @@
 
 /// Override `WebSocketConnector` in [context] to use a different constructor
 /// for [WebSocket]s (used by tests).
-typedef WebSocketConnector = Future<io.WebSocket> Function(String url);
+typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression});
 
 /// A function that opens a two-way communication channel to the specified [uri].
-typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri);
+typedef _OpenChannel = Future<StreamChannel<String>> Function(Uri uri, {io.CompressionOptions compression});
 
 _OpenChannel _openChannel = _defaultOpenChannel;
 
@@ -62,7 +62,7 @@
 
 const String _kRecordingType = 'vmservice';
 
-Future<StreamChannel<String>> _defaultOpenChannel(Uri uri) async {
+Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOptions compression = io.CompressionOptions.compressionDefault}) async {
   Duration delay = const Duration(milliseconds: 100);
   int attempts = 0;
   io.WebSocket socket;
@@ -86,7 +86,7 @@
   while (socket == null) {
     attempts += 1;
     try {
-      socket = await constructor(uri.toString());
+      socket = await constructor(uri.toString(), compression: compression);
     } on io.WebSocketException catch (e) {
       await handleError(e);
     } on io.SocketException catch (e) {
@@ -220,7 +220,7 @@
   /// `"vmservice"` subdirectory.
   static void enableRecordingConnection(String location) {
     final Directory dir = getRecordingSink(location, _kRecordingType);
-    _openChannel = (Uri uri) async {
+    _openChannel = (Uri uri, {io.CompressionOptions compression}) async {
       final StreamChannel<String> delegate = await _defaultOpenChannel(uri);
       return RecordingVMServiceChannel(delegate, dir);
     };
@@ -233,7 +233,7 @@
   /// passed to [enableRecordingConnection]), or a [ToolExit] will be thrown.
   static void enableReplayConnection(String location) {
     final Directory dir = getReplaySource(location, _kRecordingType);
-    _openChannel = (Uri uri) async => ReplayVMServiceChannel(dir);
+    _openChannel = (Uri uri, {io.CompressionOptions compression}) async => ReplayVMServiceChannel(dir);
   }
 
   /// Connect to a Dart VM Service at [httpUri].
@@ -249,9 +249,10 @@
     ReloadSources reloadSources,
     Restart restart,
     CompileExpression compileExpression,
+    io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
   }) async {
     final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));
-    final StreamChannel<String> channel = await _openChannel(wsUri);
+    final StreamChannel<String> channel = await _openChannel(wsUri, compression: compression);
     final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel));
     final VMService service = VMService(peer, httpUri, wsUri, reloadSources, restart, compileExpression);
     // This call is to ensure we are able to establish a connection instead of