Skip to content

Commit d5d7bb5

Browse files
authored
[coverage] Proactively collect isolates as they pause (#595)
1 parent acc440e commit d5d7bb5

11 files changed

+1108
-109
lines changed

pkgs/coverage/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.10.0-wip
2+
3+
- Fix bug where tests involving multiple isolates never finish (#520).
4+
15
## 1.9.2
26

37
- Fix repository link in pubspec.

pkgs/coverage/lib/src/collect.dart

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:io';
88
import 'package:vm_service/vm_service.dart';
99

1010
import 'hitmap.dart';
11+
import 'isolate_paused_listener.dart';
1112
import 'util.dart';
1213

1314
const _retryInterval = Duration(milliseconds: 200);
@@ -25,8 +26,8 @@ const _debugTokenPositions = bool.fromEnvironment('DEBUG_COVERAGE');
2526
/// If [resume] is true, all isolates will be resumed once coverage collection
2627
/// is complete.
2728
///
28-
/// If [waitPaused] is true, collection will not begin until all isolates are
29-
/// in the paused state.
29+
/// If [waitPaused] is true, collection will not begin for an isolate until it
30+
/// is in the paused state.
3031
///
3132
/// If [includeDart] is true, code coverage for core `dart:*` libraries will be
3233
/// collected.
@@ -93,14 +94,17 @@ Future<Map<String, dynamic>> collect(Uri serviceUri, bool resume,
9394
}
9495

9596
try {
96-
if (waitPaused) {
97-
await _waitIsolatesPaused(service, timeout: timeout);
98-
}
99-
100-
return await _getAllCoverage(service, includeDart, functionCoverage,
101-
branchCoverage, scopedOutput, isolateIds, coverableLineCache);
97+
return await _getAllCoverage(
98+
service,
99+
includeDart,
100+
functionCoverage,
101+
branchCoverage,
102+
scopedOutput,
103+
isolateIds,
104+
coverableLineCache,
105+
waitPaused);
102106
} finally {
103-
if (resume) {
107+
if (resume && !waitPaused) {
104108
await _resumeIsolates(service);
105109
}
106110
// The signature changed in vm_service version 6.0.0.
@@ -114,11 +118,10 @@ Future<Map<String, dynamic>> _getAllCoverage(
114118
bool includeDart,
115119
bool functionCoverage,
116120
bool branchCoverage,
117-
Set<String>? scopedOutput,
121+
Set<String> scopedOutput,
118122
Set<String>? isolateIds,
119-
Map<String, Set<int>>? coverableLineCache) async {
120-
scopedOutput ??= <String>{};
121-
final vm = await service.getVM();
123+
Map<String, Set<int>>? coverableLineCache,
124+
bool waitPaused) async {
122125
final allCoverage = <Map<String, dynamic>>[];
123126

124127
final sourceReportKinds = [
@@ -133,11 +136,15 @@ Future<Map<String, dynamic>> _getAllCoverage(
133136
// group, otherwise we'll double count the hits.
134137
final coveredIsolateGroups = <String>{};
135138

136-
for (var isolateRef in vm.isolates!) {
137-
if (isolateIds != null && !isolateIds.contains(isolateRef.id)) continue;
139+
Future<void> collectIsolate(IsolateRef isolateRef) async {
140+
if (!(isolateIds?.contains(isolateRef.id) ?? true)) return;
141+
142+
// coveredIsolateGroups is only relevant for the !waitPaused flow. The
143+
// waitPaused flow achieves the same once-per-group behavior using the
144+
// isLastIsolateInGroup flag.
138145
final isolateGroupId = isolateRef.isolateGroupId;
139146
if (isolateGroupId != null) {
140-
if (coveredIsolateGroups.contains(isolateGroupId)) continue;
147+
if (coveredIsolateGroups.contains(isolateGroupId)) return;
141148
coveredIsolateGroups.add(isolateGroupId);
142149
}
143150

@@ -154,8 +161,9 @@ Future<Map<String, dynamic>> _getAllCoverage(
154161
librariesAlreadyCompiled: librariesAlreadyCompiled,
155162
);
156163
} on SentinelException {
157-
continue;
164+
return;
158165
}
166+
159167
final coverage = await _processSourceReport(
160168
service,
161169
isolateRef,
@@ -166,6 +174,21 @@ Future<Map<String, dynamic>> _getAllCoverage(
166174
scopedOutput);
167175
allCoverage.addAll(coverage);
168176
}
177+
178+
if (waitPaused) {
179+
await IsolatePausedListener(service,
180+
(IsolateRef isolateRef, bool isLastIsolateInGroup) async {
181+
if (isLastIsolateInGroup) {
182+
await collectIsolate(isolateRef);
183+
}
184+
}, stderr.writeln)
185+
.waitUntilAllExited();
186+
} else {
187+
for (final isolateRef in await getAllIsolates(service)) {
188+
await collectIsolate(isolateRef);
189+
}
190+
}
191+
169192
return <String, dynamic>{'type': 'CodeCoverage', 'coverage': allCoverage};
170193
}
171194

@@ -190,29 +213,6 @@ Future _resumeIsolates(VmService service) async {
190213
}
191214
}
192215

193-
Future _waitIsolatesPaused(VmService service, {Duration? timeout}) async {
194-
final pauseEvents = <String>{
195-
EventKind.kPauseStart,
196-
EventKind.kPauseException,
197-
EventKind.kPauseExit,
198-
EventKind.kPauseInterrupted,
199-
EventKind.kPauseBreakpoint
200-
};
201-
202-
Future allPaused() async {
203-
final vm = await service.getVM();
204-
if (vm.isolates!.isEmpty) throw StateError('No isolates.');
205-
for (var isolateRef in vm.isolates!) {
206-
final isolate = await service.getIsolate(isolateRef.id!);
207-
if (!pauseEvents.contains(isolate.pauseEvent!.kind)) {
208-
throw StateError('Unpaused isolates remaining.');
209-
}
210-
}
211-
}
212-
213-
return retry(allPaused, _retryInterval, timeout: timeout);
214-
}
215-
216216
/// Returns the line number to which the specified token position maps.
217217
///
218218
/// Performs a binary search within the script's token position table to locate
@@ -278,6 +278,7 @@ Future<List<Map<String, dynamic>>> _processSourceReport(
278278
return;
279279
}
280280
final funcName = await _getFuncName(service, isolateRef, func);
281+
// TODO(liama): Is this still necessary, or is location.line valid?
281282
final tokenPos = location.tokenPos!;
282283
final line = _getLineFromTokenPos(script, tokenPos);
283284
if (line == null) {
@@ -299,7 +300,7 @@ Future<List<Map<String, dynamic>>> _processSourceReport(
299300
if (!scopedOutput.includesScript(scriptUriString)) {
300301
// Sometimes a range's script can be different to the function's script
301302
// (eg mixins), so we have to re-check the scope filter.
302-
// See https://github.com/dart-lang/coverage/issues/495
303+
// See https://github.com/dart-lang/tools/issues/530
303304
continue;
304305
}
305306
final scriptUri = Uri.parse(scriptUriString!);
@@ -308,7 +309,7 @@ Future<List<Map<String, dynamic>>> _processSourceReport(
308309
// SourceReportCoverage.misses: to add zeros to the coverage result for all
309310
// the lines that don't have a hit. Afterwards, add all the lines that were
310311
// hit or missed to the cache, so that the next coverage collection won't
311-
// need to compile this libarry.
312+
// need to compile this library.
312313
final coverableLines =
313314
coverableLineCache?.putIfAbsent(scriptUriString, () => <int>{});
314315

0 commit comments

Comments
 (0)