-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[coverage] Proactively collect isolates as they pause #595
Merged
Merged
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
868e4d1
WIP
liamappelbe c9e2ca3
wip
liamappelbe 8d4d8e2
Working
liamappelbe 783ea52
cleaning up
liamappelbe 63f2ebe
Fix test
liamappelbe 6637e3a
More clean up
liamappelbe 9730eb7
Merge branch 'main' into fix520
liamappelbe 63177b1
Fix analysis
liamappelbe f51dc95
Remove _numAwaitedPauseCallbacks
liamappelbe 8e1c33e
Simplify more
liamappelbe 1b7384c
Fix pubspec
liamappelbe 6925b50
Simplify and factorize for easier unit tests
liamappelbe e7991ba
Regen mocks
liamappelbe 6c05c8a
Fix test
liamappelbe 1685679
Force onExit to wait for onPaused
liamappelbe 0f68e12
Typedefs
liamappelbe 42b0654
Fix a couple more edge cases and add a test
liamappelbe 65d57c0
fmt
liamappelbe f4670dd
Handle case where main is the only isolate
liamappelbe 6a15be4
Make meta dep less strict for flutter compatibility
liamappelbe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
// Copyright (c) 2024, 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:collection'; | ||
|
||
import 'package:meta/meta.dart'; | ||
import 'package:vm_service/vm_service.dart'; | ||
|
||
import 'util.dart'; | ||
|
||
/// Calls onIsolatePaused whenever an isolate reaches the pause-on-exit state, | ||
/// and passes a flag stating whether that isolate is the last one in the group. | ||
class IsolatePausedListener { | ||
IsolatePausedListener(this._service, this._onIsolatePaused); | ||
|
||
final VmService _service; | ||
final Future<void> Function(IsolateRef isolate, bool isLastIsolateInGroup) | ||
_onIsolatePaused; | ||
final _allExitedCompleter = Completer<void>(); | ||
IsolateRef? _mainIsolate; | ||
|
||
@visibleForTesting | ||
final isolateGroups = <String, IsolateGroupState>{}; | ||
|
||
/// Starts listening and returns a future that completes when all isolates | ||
/// have exited. | ||
Future<void> waitUntilAllExited() async { | ||
await listenToIsolateLifecycleEvents(_service, _onStart, _onPause, _onExit); | ||
|
||
await _allExitedCompleter.future; | ||
|
||
// Resume the main isolate. | ||
if (_mainIsolate != null) { | ||
await _service.resume(_mainIsolate!.id!); | ||
} | ||
} | ||
|
||
IsolateGroupState _getGroup(IsolateRef isolateRef) => | ||
isolateGroups[isolateRef.isolateGroupId!] ??= IsolateGroupState(); | ||
|
||
void _onStart(IsolateRef isolateRef) { | ||
if (_allExitedCompleter.isCompleted) return; | ||
_getGroup(isolateRef).start(isolateRef.id!); | ||
} | ||
|
||
Future<void> _onPause(IsolateRef isolateRef) async { | ||
if (_allExitedCompleter.isCompleted) return; | ||
final group = _getGroup(isolateRef); | ||
group.pause(isolateRef.id!); | ||
try { | ||
await _onIsolatePaused(isolateRef, group.noRunningIsolates); | ||
} finally { | ||
await _maybeResumeIsolate(isolateRef); | ||
} | ||
} | ||
|
||
Future<void> _maybeResumeIsolate(IsolateRef isolateRef) async { | ||
if (_mainIsolate == null && _isMainIsolate(isolateRef)) { | ||
_mainIsolate = isolateRef; | ||
// Pretend this isolate has exited so _allExitedCompleter can complete. | ||
_onExit(isolateRef); | ||
} else { | ||
await _service.resume(isolateRef.id!); | ||
} | ||
} | ||
|
||
void _onExit(IsolateRef isolateRef) { | ||
if (_allExitedCompleter.isCompleted) return; | ||
_getGroup(isolateRef).exit(isolateRef.id!); | ||
if (isolateGroups.values.every((group) => group.noLiveIsolates)) { | ||
_allExitedCompleter.complete(); | ||
} | ||
} | ||
|
||
static bool _isMainIsolate(IsolateRef isolateRef) { | ||
// HACK: This should pretty reliably detect the main isolate, but it's not | ||
// foolproof and relies on unstable features. The Dart standalone embedder | ||
// and Flutter both call the main isolate "main", and they both also list | ||
// this isolate first when querying isolates from the VM service. So | ||
// selecting the first isolate named "main" combines these conditions and | ||
// should be reliable enough for now, while we wait for a better test. | ||
// TODO(https://github.com/dart-lang/sdk/issues/56732): Switch to more | ||
// reliable test when it's available. | ||
return isolateRef.name == 'main'; | ||
} | ||
} | ||
|
||
/// Listens to isolate start and pause events, and backfills events for isolates | ||
/// that existed before listening started. | ||
/// | ||
/// Ensures that: | ||
/// - Every [onIsolatePaused] and [onIsolateExited] call will be preceeded by | ||
/// an [onIsolateStarted] call for the same isolate. | ||
/// - Not every [onIsolateExited] call will be preceeded by a [onIsolatePaused] | ||
/// call, but a [onIsolatePaused] will never follow a [onIsolateExited]. | ||
/// - [onIsolateExited] will always run after [onIsolatePaused] completes, even | ||
/// if an exit event arrives while [onIsolatePaused] is being awaited. | ||
/// - Each callback will only be called once per isolate. | ||
Future<void> listenToIsolateLifecycleEvents( | ||
VmService service, | ||
void Function(IsolateRef isolate) onIsolateStarted, | ||
Future<void> Function(IsolateRef isolate) onIsolatePaused, | ||
void Function(IsolateRef isolate) onIsolateExited) async { | ||
final started = <String>{}; | ||
void onStart(IsolateRef isolateRef) { | ||
if (started.add(isolateRef.id!)) onIsolateStarted(isolateRef); | ||
} | ||
|
||
final paused = <String, Future<void>>{}; | ||
Future<void> onPause(IsolateRef isolateRef) async { | ||
onStart(isolateRef); | ||
await (paused[isolateRef.id!] ??= onIsolatePaused(isolateRef)); | ||
} | ||
|
||
final exited = <String>{}; | ||
Future<void> onExit(IsolateRef isolateRef) async { | ||
onStart(isolateRef); | ||
if (exited.add(isolateRef.id!)) { | ||
// Wait for in-progress pause callbacks. Otherwise prevent future pause | ||
// callbacks from running. | ||
await (paused[isolateRef.id!] ??= Future<void>.value()); | ||
onIsolateExited(isolateRef); | ||
} | ||
} | ||
|
||
final eventBuffer = IsolateEventBuffer((Event event) async { | ||
switch (event.kind) { | ||
case EventKind.kIsolateStart: | ||
return onStart(event.isolate!); | ||
case EventKind.kPauseExit: | ||
return await onPause(event.isolate!); | ||
case EventKind.kIsolateExit: | ||
return await onExit(event.isolate!); | ||
} | ||
}); | ||
|
||
// Listen for isolate start/exit events. | ||
service.onIsolateEvent.listen(eventBuffer.add); | ||
await service.streamListen(EventStreams.kIsolate); | ||
|
||
// Listen for isolate paused events. | ||
service.onDebugEvent.listen(eventBuffer.add); | ||
await service.streamListen(EventStreams.kDebug); | ||
|
||
// Backfill. Add/pause isolates that existed before we subscribed. | ||
for (final isolateRef in await getAllIsolates(service)) { | ||
onStart(isolateRef); | ||
final isolate = await service.getIsolate(isolateRef.id!); | ||
if (isolate.pauseEvent?.kind == EventKind.kPauseExit) { | ||
await onPause(isolateRef); | ||
} | ||
} | ||
|
||
// Flush the buffered stream events, and the start processing them as they | ||
// arrive. | ||
await eventBuffer.flush(); | ||
} | ||
|
||
/// Keeps track of isolates in an isolate group. | ||
class IsolateGroupState { | ||
// IDs of the isolates running in this group. | ||
@visibleForTesting | ||
final running = <String>{}; | ||
|
||
// IDs of the isolates paused just before exiting in this group. | ||
@visibleForTesting | ||
final paused = <String>{}; | ||
|
||
bool get noRunningIsolates => running.isEmpty; | ||
bool get noLiveIsolates => running.isEmpty && paused.isEmpty; | ||
|
||
void start(String id) { | ||
paused.remove(id); | ||
running.add(id); | ||
} | ||
|
||
void pause(String id) { | ||
running.remove(id); | ||
paused.add(id); | ||
} | ||
|
||
void exit(String id) { | ||
running.remove(id); | ||
paused.remove(id); | ||
} | ||
} | ||
|
||
/// Buffers VM service isolate [Event]s until [flush] is called. | ||
/// | ||
/// [flush] passes each buffered event to the handler function. After that, any | ||
/// further events are immediately passed to the handler. [flush] returns a | ||
/// future that completes when all the events in the queue have been handled (as | ||
/// well as any events that arrive while flush is in progress). | ||
class IsolateEventBuffer { | ||
IsolateEventBuffer(this._handler); | ||
|
||
final Future<void> Function(Event event) _handler; | ||
final _buffer = Queue<Event>(); | ||
var _flushed = false; | ||
|
||
Future<void> add(Event event) async { | ||
if (_flushed) { | ||
await _handler(event); | ||
} else { | ||
_buffer.add(event); | ||
} | ||
} | ||
|
||
Future<void> flush() async { | ||
while (_buffer.isNotEmpty) { | ||
final event = _buffer.removeFirst(); | ||
await _handler(event); | ||
} | ||
_flushed = true; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: consider creating a typedef for all these callback types. Maybe something like:
typedef FutureOr<void> Function(IsolateRef) IsolateLifecycleCallback
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've made some typedefs, but it's important to distinguish the async callbacks from the sync ones. If all of them were allowed to be async, then ensuring they don't interleave would be more complicated.