blob: 59760f888512adb9bcc15657c24309cc0bffcf2f [file] [log] [blame]
// Copyright (c) 2014, 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:async';
import 'dart:io';
import 'package:vm_service/vm_service.dart';
import 'util.dart';
import 'hitmap.dart';
const _retryInterval = Duration(milliseconds: 200);
/// Collects coverage for all isolates in the running VM.
///
/// Collects a hit-map containing merged coverage for all isolates in the Dart
/// VM associated with the specified [serviceUri]. Returns a map suitable for
/// input to the coverage formatters that ship with this package.
///
/// [serviceUri] must specify the http/https URI of the service port of a
/// running Dart VM and must not be null.
///
/// If [resume] is true, all isolates will be resumed once coverage collection
/// is complete.
///
/// If [waitPaused] is true, collection will not begin until all isolates are
/// in the paused state.
///
/// If [includeDart] is true, code coverage for core `dart:*` libraries will be
/// collected.
///
/// If [scopedOutput] is non-empty, coverage will be restricted so that only
/// scripts that start with any of the provided paths are considered.
///
/// if [isolateIds] is set, the coverage gathering will be restricted to only
/// those VM isolates.
Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
bool waitPaused, bool includeDart, Set<String>? scopedOutput,
{Set<String>? isolateIds, Duration? timeout}) async {
scopedOutput ??= <String>{};
// Create websocket URI. Handle any trailing slashes.
final pathSegments =
serviceUri.pathSegments.where((c) => c.isNotEmpty).toList()..add('ws');
final uri = serviceUri.replace(scheme: 'ws', pathSegments: pathSegments);
late VmService service;
await retry(() async {
try {
final options = const CompressionOptions(enabled: false);
final socket = await WebSocket.connect('$uri', compression: options);
final controller = StreamController<String>();
socket.listen((data) => controller.add(data as String), onDone: () {
controller.close();
service.dispose();
});
service = VmService(
controller.stream, (String message) => socket.add(message),
log: StdoutLog(), disposeHandler: () => socket.close());
await service.getVM().timeout(_retryInterval);
} on TimeoutException {
// The signature changed in vm_service version 6.0.0.
// ignore: await_only_futures
await service.dispose();
rethrow;
}
}, _retryInterval, timeout: timeout);
try {
if (waitPaused) {
await _waitIsolatesPaused(service, timeout: timeout);
}
return await _getAllCoverage(
service, includeDart, scopedOutput, isolateIds);
} finally {
if (resume) {
await _resumeIsolates(service);
}
// The signature changed in vm_service version 6.0.0.
// ignore: await_only_futures
await service.dispose();
}
}
Future<Map<String, dynamic>> _getAllCoverage(
VmService service,
bool includeDart,
Set<String>? scopedOutput,
Set<String>? isolateIds) async {
scopedOutput ??= <String>{};
final vm = await service.getVM();
final allCoverage = <Map<String, dynamic>>[];
for (var isolateRef in vm.isolates!) {
if (isolateIds != null && !isolateIds.contains(isolateRef.id)) continue;
if (scopedOutput.isNotEmpty) {
final scripts = await service.getScripts(isolateRef.id!);
for (var script in scripts.scripts!) {
final uri = Uri.parse(script.uri!);
if (uri.scheme != 'package') continue;
final scope = uri.path.split('/').first;
// Skip scripts which should not be included in the report.
if (!scopedOutput.contains(scope)) continue;
final scriptReport = await service.getSourceReport(
isolateRef.id!, <String>[SourceReportKind.kCoverage],
forceCompile: true, scriptId: script.id);
final coverage = await _getCoverageJson(
service, isolateRef, scriptReport, includeDart);
allCoverage.addAll(coverage);
}
} else {
final isolateReport = await service.getSourceReport(
isolateRef.id!,
<String>[SourceReportKind.kCoverage],
forceCompile: true,
);
final coverage = await _getCoverageJson(
service, isolateRef, isolateReport, includeDart);
allCoverage.addAll(coverage);
}
}
return <String, dynamic>{'type': 'CodeCoverage', 'coverage': allCoverage};
}
Future _resumeIsolates(VmService service) async {
final vm = await service.getVM();
final futures = <Future>[];
for (var isolateRef in vm.isolates!) {
// Guard against sync as well as async errors: sync - when we are writing
// message to the socket, the socket might be closed; async - when we are
// waiting for the response, the socket again closes.
futures.add(Future.sync(() async {
final isolate = await service.getIsolate(isolateRef.id!);
if (isolate.pauseEvent!.kind != EventKind.kResume) {
await service.resume(isolateRef.id!);
}
}));
}
try {
await Future.wait(futures);
} catch (_) {
// Ignore resume isolate failures
}
}
Future _waitIsolatesPaused(VmService service, {Duration? timeout}) async {
final pauseEvents = <String>{
EventKind.kPauseStart,
EventKind.kPauseException,
EventKind.kPauseExit,
EventKind.kPauseInterrupted,
EventKind.kPauseBreakpoint
};
Future allPaused() async {
final vm = await service.getVM();
if (vm.isolates!.isEmpty) throw 'No isolates.';
for (var isolateRef in vm.isolates!) {
final isolate = await service.getIsolate(isolateRef.id!);
if (!pauseEvents.contains(isolate.pauseEvent!.kind)) {
throw 'Unpaused isolates remaining.';
}
}
}
return retry(allPaused, _retryInterval, timeout: timeout);
}
/// Returns the line number to which the specified token position maps.
///
/// Performs a binary search within the script's token position table to locate
/// the line in question.
int? _getLineFromTokenPos(Script script, int tokenPos) {
// TODO(cbracken): investigate whether caching this lookup results in
// significant performance gains.
var min = 0;
var max = script.tokenPosTable!.length;
while (min < max) {
final mid = min + ((max - min) >> 1);
final row = script.tokenPosTable![mid];
if (row[1] > tokenPos) {
max = mid;
} else {
for (var i = 1; i < row.length; i += 2) {
if (row[i] == tokenPos) return row.first;
}
min = mid + 1;
}
}
return null;
}
/// Returns a JSON coverage list backward-compatible with pre-1.16.0 SDKs.
Future<List<Map<String, dynamic>>> _getCoverageJson(VmService service,
IsolateRef isolateRef, SourceReport report, bool includeDart) async {
// script uri -> { line -> hit count }
final hitMaps = <Uri, HitMap>{};
final scripts = <ScriptRef, Script>{};
final functions = <LibraryRef, Map<ScriptRef, Set<int>>>{};
for (var range in report.ranges!) {
final scriptRef = report.scripts![range.scriptIndex!];
final scriptUri = Uri.parse(report.scripts![range.scriptIndex!].uri!);
// Not returned in scripts section of source report.
if (scriptUri.scheme == 'evaluate') continue;
// Skip scripts from dart:.
if (!includeDart && scriptUri.scheme == 'dart') continue;
if (!scripts.containsKey(scriptRef)) {
scripts[scriptRef] =
await service.getObject(isolateRef.id!, scriptRef.id!) as Script;
}
final script = scripts[scriptRef];
if (script == null) continue;
// Look up the hit maps for this script (shared across isolates).
final hits = hitMaps.putIfAbsent(scriptUri, () => HitMap());
// If the script's library isn't loaded, load it then look up all its funcs.
final libRef = script.library;
if (libRef != null && !functions.containsKey(libRef)) {
final library =
await service.getObject(isolateRef.id!, libRef.id!) as Library;
final Map<ScriptRef, Set<int>> libFuncs = {};
if (library.functions != null) {
for (var funcRef in library.functions!) {
final func =
await service.getObject(isolateRef.id!, funcRef.id!) as Func;
final location = func.location;
if (location != null) {
final scriptFuncs =
libFuncs.putIfAbsent(location.script!, () => <int>{});
final funcName = await _getFuncName(service, isolateRef, func);
final tokenPos = location.tokenPos!;
scriptFuncs.add(tokenPos);
final line = _getLineFromTokenPos(script, tokenPos);
if (line == null) {
print(
'tokenPos $tokenPos has no line mapping for script $scriptUri');
continue;
}
if (hits.funcNames.containsKey(line)) {
print(
'Multiple functions defined on line $line of script $scriptUri');
continue;
}
hits.funcNames[line] = funcName;
}
}
}
functions[libRef] = libFuncs;
}
final scriptFuncs = functions[libRef]![scriptRef]!;
// Collect hits and misses.
final coverage = range.coverage;
if (coverage == null) continue;
for (final tokenPos in coverage.hits!) {
final line = _getLineFromTokenPos(script, tokenPos);
if (line == null) {
print('tokenPos $tokenPos has no line mapping for script $scriptUri');
continue;
}
_increment(hits.lineHits, line);
if (scriptFuncs.contains(tokenPos)) _increment(hits.funcHits, line);
}
for (final tokenPos in coverage.misses!) {
final line = _getLineFromTokenPos(script, tokenPos);
if (line == null) {
print('tokenPos $tokenPos has no line mapping for script $scriptUri');
continue;
}
hits.lineHits.putIfAbsent(line, () => 0);
}
scriptFuncs.forEach((tokenPos) {
final line = _getLineFromTokenPos(script, tokenPos);
if (line == null) {
print('tokenPos $tokenPos has no line mapping for script $scriptUri');
return;
}
hits.funcHits.putIfAbsent(line, () => 0);
});
}
// Output JSON
final coverage = <Map<String, dynamic>>[];
hitMaps.forEach((uri, hits) {
coverage.add(toScriptCoverageJson(uri, hits));
});
return coverage;
}
void _increment(Map<int, int> counter, int key) {
counter[key] = counter.containsKey(key) ? counter[key]! + 1 : 1;
}
Future<String> _getFuncName(
VmService service, IsolateRef isolateRef, Func func) async {
if (func.name == null) {
return "${func.type}:${func.location!.tokenPos}";
}
if (func.owner is ClassRef) {
final cls =
await service.getObject(isolateRef.id!, func.owner.id!) as Class;
if (cls.name != null) return "${cls.name}.${func.name}";
}
return func.name!;
}
class StdoutLog extends Log {
@override
void warning(String message) => print(message);
@override
void severe(String message) => print(message);
}