Skip to content

Commit e7168ae

Browse files
authored
[coverage] Finish collection as soon as main isolate exits (#2069)
1 parent d74f9e1 commit e7168ae

6 files changed

+199
-157
lines changed

Diff for: pkgs/coverage/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## 1.12.0-wip
22

33
- Introduced support for specifying coverage flags through a YAML file.
4+
- Fix a [bug](https://github.com/dart-lang/tools/issues/2049) where coverage
5+
collection could hang forever if the package under test used background
6+
isolates that were never shut down.
47

58
## 1.11.1
69

Diff for: pkgs/coverage/lib/src/isolate_paused_listener.dart

+68-70
Original file line numberDiff line numberDiff line change
@@ -28,101 +28,93 @@ class IsolatePausedListener {
2828

2929
final _isolateGroups = <String, IsolateGroupState>{};
3030

31-
int _numNonMainIsolates = 0;
32-
final _allNonMainIsolatesExited = Completer<void>();
33-
bool _finished = false;
31+
int _numIsolates = 0;
32+
bool _finishedListening = false;
3433

3534
IsolateRef? _mainIsolate;
36-
final _mainIsolatePaused = Completer<bool>();
35+
bool _hasMainIsolate = false;
36+
37+
// Completes when either:
38+
// - there is a main isolate, and it is paused or exited
39+
// - there is no main isolate, and all isolates have exited
40+
final _mainIsolatePausedOrAllIsolatesExited = Completer<void>();
3741

3842
/// Starts listening and returns a future that completes when all isolates
3943
/// have exited.
4044
Future<void> waitUntilAllExited() async {
4145
await listenToIsolateLifecycleEvents(_service, _onStart, _onPause, _onExit);
4246

43-
await _allNonMainIsolatesExited.future;
44-
45-
// Resume the main isolate.
46-
try {
47-
if (_mainIsolate != null) {
48-
if (await _mainIsolatePaused.future) {
49-
await _runCallbackAndResume(
50-
_mainIsolate!, !_getGroup(_mainIsolate!).collected);
47+
await _mainIsolatePausedOrAllIsolatesExited.future;
48+
_finishedListening = true;
49+
50+
// Collect all remaining uncollected groups.
51+
final collectionTasks = <Future<void>>[];
52+
for (final group in _isolateGroups.values) {
53+
if (!group.collected) {
54+
group.collected = true;
55+
final iso = group.paused.firstOrNull ?? group.running.firstOrNull;
56+
if (iso != null) {
57+
collectionTasks.add(_onIsolatePaused(iso, true));
5158
}
5259
}
53-
} finally {
54-
_finished = true;
60+
}
61+
await Future.wait(collectionTasks);
62+
63+
// Resume the main isolate.
64+
if (_mainIsolate != null) {
65+
await _service.resume(_mainIsolate!.id!);
5566
}
5667
}
5768

5869
IsolateGroupState _getGroup(IsolateRef isolateRef) =>
5970
_isolateGroups[isolateRef.isolateGroupId!] ??= IsolateGroupState();
6071

6172
void _onStart(IsolateRef isolateRef) {
62-
if (_finished) return;
73+
if (_finishedListening) return;
6374
final group = _getGroup(isolateRef);
64-
group.start(isolateRef.id!);
65-
if (_mainIsolate == null && _isMainIsolate(isolateRef)) {
75+
group.start(isolateRef);
76+
++_numIsolates;
77+
if (!_hasMainIsolate && _isMainIsolate(isolateRef)) {
6678
_mainIsolate = isolateRef;
67-
} else {
68-
++_numNonMainIsolates;
79+
_hasMainIsolate = true;
6980
}
7081
}
7182

7283
Future<void> _onPause(IsolateRef isolateRef) async {
73-
if (_finished) return;
84+
if (_finishedListening) return;
7485
final group = _getGroup(isolateRef);
75-
group.pause(isolateRef.id!);
86+
group.pause(isolateRef);
7687
if (isolateRef.id! == _mainIsolate?.id) {
77-
_mainIsolatePaused.complete(true);
78-
79-
// If the main isolate is the only isolate, then _allNonMainIsolatesExited
80-
// will never be completed. So check that case here.
81-
_checkCompleted();
88+
_mainIsolatePausedOrAllIsolatesExited.safeComplete();
8289
} else {
83-
await _runCallbackAndResume(isolateRef, group.noRunningIsolates);
84-
}
85-
}
86-
87-
Future<void> _runCallbackAndResume(
88-
IsolateRef isolateRef, bool isLastIsolateInGroup) async {
89-
if (isLastIsolateInGroup) {
90-
_getGroup(isolateRef).collected = true;
91-
}
92-
try {
93-
await _onIsolatePaused(isolateRef, isLastIsolateInGroup);
94-
} finally {
95-
await _service.resume(isolateRef.id!);
90+
final isLastIsolateInGroup = group.noRunningIsolates;
91+
if (isLastIsolateInGroup) {
92+
_getGroup(isolateRef).collected = true;
93+
}
94+
try {
95+
await _onIsolatePaused(isolateRef, isLastIsolateInGroup);
96+
} finally {
97+
group.exit(isolateRef);
98+
await _service.resume(isolateRef.id!);
99+
}
96100
}
97101
}
98102

99103
void _onExit(IsolateRef isolateRef) {
100-
if (_finished) return;
104+
if (_finishedListening) return;
101105
final group = _getGroup(isolateRef);
102-
group.exit(isolateRef.id!);
106+
group.exit(isolateRef);
107+
--_numIsolates;
103108
if (group.noLiveIsolates && !group.collected) {
104109
_log('ERROR: An isolate exited without pausing, causing '
105110
'coverage data to be lost for group ${isolateRef.isolateGroupId!}.');
106111
}
107112
if (isolateRef.id! == _mainIsolate?.id) {
108-
if (!_mainIsolatePaused.isCompleted) {
109-
// Main isolate exited without pausing.
110-
_mainIsolatePaused.complete(false);
111-
}
112-
} else {
113-
--_numNonMainIsolates;
114-
_checkCompleted();
115-
}
116-
}
117-
118-
bool get _mainRunning =>
119-
_mainIsolate != null && !_mainIsolatePaused.isCompleted;
120-
121-
void _checkCompleted() {
122-
if (_numNonMainIsolates == 0 &&
123-
!_mainRunning &&
124-
!_allNonMainIsolatesExited.isCompleted) {
125-
_allNonMainIsolatesExited.complete();
113+
// Main isolate exited without pausing.
114+
_mainIsolate = null;
115+
_mainIsolatePausedOrAllIsolatesExited.safeComplete();
116+
} else if (!_hasMainIsolate && _numIsolates == 0) {
117+
_mainIsolatePausedOrAllIsolatesExited.safeComplete();
126118
}
127119
}
128120

@@ -139,6 +131,12 @@ class IsolatePausedListener {
139131
}
140132
}
141133

134+
extension on Completer {
135+
void safeComplete() {
136+
if (!isCompleted) complete();
137+
}
138+
}
139+
142140
/// Listens to isolate start and pause events, and backfills events for isolates
143141
/// that existed before listening started.
144142
///
@@ -220,30 +218,30 @@ Future<void> listenToIsolateLifecycleEvents(
220218
class IsolateGroupState {
221219
// IDs of the isolates running in this group.
222220
@visibleForTesting
223-
final running = <String>{};
221+
final running = <IsolateRef>{};
224222

225223
// IDs of the isolates paused just before exiting in this group.
226224
@visibleForTesting
227-
final paused = <String>{};
225+
final paused = <IsolateRef>{};
228226

229227
bool collected = false;
230228

231229
bool get noRunningIsolates => running.isEmpty;
232230
bool get noLiveIsolates => running.isEmpty && paused.isEmpty;
233231

234-
void start(String id) {
235-
paused.remove(id);
236-
running.add(id);
232+
void start(IsolateRef iso) {
233+
paused.remove(iso);
234+
running.add(iso);
237235
}
238236

239-
void pause(String id) {
240-
running.remove(id);
241-
paused.add(id);
237+
void pause(IsolateRef iso) {
238+
running.remove(iso);
239+
paused.add(iso);
242240
}
243241

244-
void exit(String id) {
245-
running.remove(id);
246-
paused.remove(id);
242+
void exit(IsolateRef iso) {
243+
running.remove(iso);
244+
paused.remove(iso);
247245
}
248246

249247
@override

Diff for: pkgs/coverage/test/collect_coverage_test.dart

+12-11
Original file line numberDiff line numberDiff line change
@@ -133,24 +133,25 @@ void main() {
133133
39: 1,
134134
41: 1,
135135
42: 4,
136-
43: 1,
137-
44: 3,
138-
45: 1,
139-
48: 1,
136+
43: 2,
137+
44: 1,
138+
45: 3,
139+
46: 1,
140140
49: 1,
141-
51: 1,
142-
54: 1,
141+
50: 1,
142+
52: 1,
143143
55: 1,
144144
56: 1,
145-
59: 1,
145+
57: 1,
146146
60: 1,
147-
62: 1,
147+
61: 1,
148148
63: 1,
149149
64: 1,
150-
66: 1,
150+
65: 1,
151151
67: 1,
152152
68: 1,
153-
71: 3,
153+
69: 1,
154+
72: 3,
154155
};
155156
expect(isolateFile?.lineHits, expectedHits);
156157
expect(isolateFile?.funcHits, {11: 1, 19: 1, 23: 1, 28: 1, 38: 1});
@@ -174,7 +175,7 @@ void main() {
174175
32: 0,
175176
38: 1,
176177
42: 1,
177-
71: 1,
178+
72: 1,
178179
},
179180
);
180181
});

0 commit comments

Comments
 (0)