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