32dffaa8d8
- Deprecate `getAvailableCachedCpuSamples` and `getCachedCpuSamples` RPCs. - Deprecate the `cachedUserTags` parameter of `DartDevelopmentService.startDartDevelopmentService`. - Deprecate the `DartDevelopmentService.cachedUserTags` getter. Issue: https://github.com/dart-lang/sdk/issues/61267 Change-Id: I9f9088a810584c2031d8475c767a312dff6ab2cd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/444100 Reviewed-by: Ben Konyi <bkonyi@google.com> Commit-Queue: Derek Xu <derekx@google.com>
470 lines
16 KiB
Dart
470 lines
16 KiB
Dart
// Copyright (c) 2020, 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:convert' show base64Decode, base64Encode;
|
|
|
|
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
|
|
import 'package:vm_service/vm_service.dart';
|
|
|
|
import 'client.dart';
|
|
import 'constants.dart';
|
|
import 'dds_impl.dart';
|
|
import 'utils/mutex.dart';
|
|
|
|
/// This file contains functionality used to track the running state of
|
|
/// all isolates in a given Dart process.
|
|
///
|
|
/// [_RunningIsolate] is a representation of a single live isolate and contains
|
|
/// running state information for that isolate. In addition, approvals from
|
|
/// clients used to synchronize isolate resuming across multiple clients are
|
|
/// tracked in this class.
|
|
///
|
|
/// The [IsolateManager] keeps track of all the isolates in the
|
|
/// target process and handles isolate lifecycle events including:
|
|
/// - Startup
|
|
/// - Shutdown
|
|
/// - Pauses
|
|
///
|
|
/// The [IsolateManager] also handles the `resume` RPC, which checks the
|
|
/// resume approvals in the target [_RunningIsolate] to determine if the
|
|
/// isolate should be resumed or wait for additional approvals to be granted.
|
|
|
|
enum _IsolateState {
|
|
start,
|
|
running,
|
|
pauseStart,
|
|
pauseExit,
|
|
pausePostRequest,
|
|
unknown,
|
|
}
|
|
|
|
class RunningIsolate {
|
|
RunningIsolate(this.isolateManager, this.id, this.name)
|
|
: _state = _IsolateState.unknown;
|
|
|
|
// State setters.
|
|
void pausedOnExit() => _state = _IsolateState.pauseExit;
|
|
|
|
void pausedOnStart() => _state = _IsolateState.pauseStart;
|
|
|
|
void pausedPostRequest() => _state = _IsolateState.pausePostRequest;
|
|
|
|
void resumed() => running();
|
|
|
|
void running() => _state = _IsolateState.running;
|
|
|
|
void started() => _state = _IsolateState.start;
|
|
|
|
/// Resumes the isolate if all clients which need to approve a resume have
|
|
/// done so. Called when the last client of a given name disconnects or
|
|
/// changes name to ensure we don't deadlock waiting for approval to resume
|
|
/// from a disconnected client.
|
|
Future<void> maybeResumeAfterClientChange(String? clientName) async {
|
|
// Remove approvals from the disconnected client.
|
|
_resumeApprovalsByName.remove(clientName);
|
|
|
|
if (shouldResume()) {
|
|
clearResumeApprovals();
|
|
await isolateManager.dds.vmServiceClient.sendRequest('resume', {
|
|
'isolateId': id,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Returns true if this isolate should resume given its client approvals
|
|
/// state.
|
|
///
|
|
/// If `resumingClient` is provided, it will be added to the set of clients
|
|
/// which have provided approval to resume this isolate. If not provided,
|
|
/// the existing approvals state will be examined to see if the isolate
|
|
/// should resume due to a client disconnect or name change.
|
|
bool shouldResume({DartDevelopmentServiceClient? resumingClient}) {
|
|
if (resumingClient != null) {
|
|
// Mark approval by the client.
|
|
_resumeApprovalsByName.add(resumingClient.name);
|
|
}
|
|
|
|
// If the user is required to resume the isolate, we won't resume until a
|
|
// `resume` request is received or `setRequireUserPermissionToResume` is
|
|
// invoked and removes the requirement for this pause type.
|
|
final userPermissionMask =
|
|
isolateManager._requireUserPermissionToResumeMask;
|
|
if (userPermissionMask & _isolateStateMask != 0) {
|
|
return false;
|
|
}
|
|
|
|
final requiredClientApprovals = <String>{};
|
|
final permissions =
|
|
isolateManager.dds.clientManager.clientResumePermissions;
|
|
|
|
// Determine which clients require approval for this pause type.
|
|
permissions.forEach((clientName, clientNamePermissions) {
|
|
if (clientNamePermissions.permissionsMask & _isolateStateMask != 0) {
|
|
requiredClientApprovals.add(clientName!);
|
|
}
|
|
});
|
|
|
|
// We require at least a single client to resume, even if that client
|
|
// doesn't require resume approval.
|
|
if (_resumeApprovalsByName.isEmpty) {
|
|
return false;
|
|
}
|
|
// If all the required approvals are present, we should resume.
|
|
return _resumeApprovalsByName.containsAll(requiredClientApprovals);
|
|
}
|
|
|
|
/// Resets the internal resume approvals state.
|
|
///
|
|
/// Should always be called after an isolate is resumed.
|
|
void clearResumeApprovals() => _resumeApprovalsByName.clear();
|
|
|
|
int get _isolateStateMask => isolateStateToMaskMapping[_state] ?? 0;
|
|
|
|
static const isolateStateToMaskMapping = {
|
|
_IsolateState.pauseStart: PauseTypeMasks.pauseOnStartMask,
|
|
_IsolateState.pausePostRequest: PauseTypeMasks.pauseOnReloadMask,
|
|
_IsolateState.pauseExit: PauseTypeMasks.pauseOnExitMask,
|
|
};
|
|
|
|
final IsolateManager isolateManager;
|
|
final String name;
|
|
final String id;
|
|
final Set<String?> _resumeApprovalsByName = {};
|
|
_IsolateState _state;
|
|
}
|
|
|
|
class IsolateManager {
|
|
IsolateManager(this.dds);
|
|
|
|
/// Handles state changes for isolates.
|
|
void handleIsolateEvent(Event event) {
|
|
// There's no interesting information about isolate state associated with
|
|
// IsolateSpawn or IsolateReload events.
|
|
// TODO(bkonyi): why isn't IsolateSpawn in package:vm_service
|
|
if (event.kind! == ServiceEvents.isolateSpawn ||
|
|
event.kind == EventKind.kIsolateReload) {
|
|
return;
|
|
}
|
|
|
|
final isolateData = event.isolate!;
|
|
final id = isolateData.id!;
|
|
final name = isolateData.name!;
|
|
_updateIsolateState(id, name, event.kind!);
|
|
}
|
|
|
|
void _updateIsolateState(String id, String name, String eventKind) {
|
|
_mutex.runGuarded(
|
|
() {
|
|
switch (eventKind) {
|
|
case ServiceEvents.isolateStart:
|
|
isolateStarted(id, name);
|
|
break;
|
|
case ServiceEvents.isolateExit:
|
|
isolateExited(id);
|
|
break;
|
|
default:
|
|
final isolate = isolates[id];
|
|
// The isolate may have disappeared after the state event was sent.
|
|
if (isolate == null) {
|
|
return;
|
|
}
|
|
switch (eventKind) {
|
|
case ServiceEvents.pauseExit:
|
|
isolate.pausedOnExit();
|
|
break;
|
|
case ServiceEvents.pausePostRequest:
|
|
isolate.pausedPostRequest();
|
|
break;
|
|
case ServiceEvents.pauseStart:
|
|
isolate.pausedOnStart();
|
|
break;
|
|
case ServiceEvents.resume:
|
|
isolate.resumed();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Initializes the set of running isolates.
|
|
Future<void> initialize() async {
|
|
if (_initialized) {
|
|
return;
|
|
}
|
|
final vm = await dds.vmServiceClient.sendRequest('getVM');
|
|
final isolateRefs = vm['isolates'].cast<Map<String, dynamic>>();
|
|
// Check the pause event for each isolate to determine whether or not the
|
|
// isolate is already paused.
|
|
for (final isolateRef in isolateRefs) {
|
|
final id = isolateRef['id'];
|
|
final name = isolateRef['name'];
|
|
|
|
// Create an entry for the running isolate.
|
|
initializeRunningIsolate(id, name);
|
|
|
|
// The calls to `getIsolate` are intentionally unawaited as it's
|
|
// possible for the isolate to be in a state where it is unable to
|
|
// process service messages, potentially indefinitely. For example,
|
|
// an isolate that invoked FFI code will be blocked until control is
|
|
// returned from native code.
|
|
//
|
|
// See b/323386606 for details.
|
|
unawaited(
|
|
dds.vmServiceClient.sendRequest('getIsolate', {
|
|
'isolateId': id,
|
|
}).then(
|
|
(isolate) async => await _mutex.runGuarded(
|
|
() {
|
|
// If the isolate has shutdown after the getVM request, ignore it and
|
|
// continue to the next isolate.
|
|
if (isolate['type'] == 'Sentinel') {
|
|
return;
|
|
}
|
|
if (isolate.containsKey('pauseEvent')) {
|
|
isolates[id] = RunningIsolate(this, id, name);
|
|
final eventKind = isolate['pauseEvent']['kind'];
|
|
_updateIsolateState(id, name, eventKind);
|
|
} else {
|
|
// If the isolate doesn't have a pauseEvent, assume it's running.
|
|
isolateStarted(id, name);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
await _determineRequireUserPermissionToResumeFromFlags();
|
|
_initialized = true;
|
|
}
|
|
|
|
/// This method creates an entry for a running isolate, leaves its run state
|
|
/// as [_IsolateState.unknown].
|
|
RunningIsolate initializeRunningIsolate(String id, String name) =>
|
|
isolates.putIfAbsent(
|
|
id,
|
|
() => RunningIsolate(this, id, name),
|
|
);
|
|
|
|
/// Initializes state for a newly started isolate.
|
|
void isolateStarted(String id, String name) {
|
|
final isolate = initializeRunningIsolate(id, name);
|
|
isolate.running();
|
|
isolates[id] = isolate;
|
|
}
|
|
|
|
/// Cleans up state for an isolate that has exited.
|
|
void isolateExited(String id) {
|
|
isolates.remove(id);
|
|
}
|
|
|
|
/// Handles `resume` RPC requests.
|
|
///
|
|
/// Invocations of `resume` are treated as user initiated and will bypass any
|
|
/// resume permissions set by tooling, force resuming the isolate and clear
|
|
/// any resume approvals.
|
|
///
|
|
/// Returns a collected sentinel if the isolate no longer exists.
|
|
Future<Map<String, dynamic>> resumeIsolate(
|
|
DartDevelopmentServiceClient client,
|
|
json_rpc.Parameters parameters,
|
|
) async {
|
|
return await _mutex.runGuarded(
|
|
() async {
|
|
final isolateId = parameters['isolateId'].asString;
|
|
final isolate = isolates[isolateId];
|
|
if (isolate == null) {
|
|
return RPCResponses.collectedSentinel;
|
|
}
|
|
return await _resumeCommon(isolate, parameters);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Handles `readyToResume` RPC requests. If the client requires
|
|
/// that approval be given before resuming an isolate, this method will:
|
|
///
|
|
/// - Update the approval state for the isolate.
|
|
/// - Resume the isolate if approval has been given by all clients which
|
|
/// require approval.
|
|
///
|
|
/// Returns a collected sentinel if the isolate no longer exists.
|
|
Future<Map<String, dynamic>> readyToResume(
|
|
DartDevelopmentServiceClient client,
|
|
json_rpc.Parameters parameters,
|
|
) async {
|
|
return await _mutex.runGuarded(
|
|
() async {
|
|
final isolateId = parameters['isolateId'].asString;
|
|
final isolate = isolates[isolateId];
|
|
if (isolate == null) {
|
|
return RPCResponses.collectedSentinel;
|
|
}
|
|
if (isolate.shouldResume(resumingClient: client)) {
|
|
return await _resumeCommon(isolate, parameters);
|
|
}
|
|
return RPCResponses.success;
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> maybeResumeIsolates() async {
|
|
await _mutex.runGuarded(() async {
|
|
for (final isolate in isolates.values) {
|
|
if (isolate.shouldResume()) {
|
|
await _resumeCommon(isolate, json_rpc.Parameters('', {}));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _resumeCommon(
|
|
RunningIsolate isolate,
|
|
json_rpc.Parameters parameters,
|
|
) async {
|
|
isolate.clearResumeApprovals();
|
|
return await _sendResumeRequest(isolate.id, parameters);
|
|
}
|
|
|
|
/// Handles `requireUserPermissionToResume` requests.
|
|
///
|
|
/// Notifies DDS if it should wait for a `resume` request to resume isolates
|
|
/// paused on start or exit.
|
|
///
|
|
/// This RPC should only be invoked by tooling which launched the target Dart
|
|
/// process and knows if the user indicated they wanted isolates paused on
|
|
/// start or exit.
|
|
Future<Map<String, dynamic>> requireUserPermissionToResume(
|
|
DartDevelopmentServiceClient client,
|
|
json_rpc.Parameters parameters,
|
|
) async {
|
|
_setRequireUserPermissionToResume(
|
|
onPauseStart: parameters['onPauseStart'].asBoolOr(false),
|
|
onPauseExit: parameters['onPauseExit'].asBoolOr(false),
|
|
);
|
|
|
|
// Check if isolates have been waiting for a user resume and resume any
|
|
// isolates that no longer need to wait for a user resume.
|
|
await maybeResumeIsolates();
|
|
|
|
return RPCResponses.success;
|
|
}
|
|
|
|
/// Handles `getRequireUserPermissionToResume` requests.
|
|
///
|
|
/// Returns an object indicating whether or not a `resume` request is
|
|
/// required for DDS to resume an isolate paused on start or exit.
|
|
Future<Map<String, dynamic>> getRequireUserPermissionToResume(
|
|
DartDevelopmentServiceClient client,
|
|
json_rpc.Parameters parameters) async {
|
|
bool flagFromMask(int mask) =>
|
|
_requireUserPermissionToResumeMask & mask != 0;
|
|
return <String, dynamic>{
|
|
'type': 'ResumePermissionsRequired',
|
|
'onPauseStart': flagFromMask(PauseTypeMasks.pauseOnStartMask),
|
|
'onPauseExit': flagFromMask(PauseTypeMasks.pauseOnExitMask),
|
|
};
|
|
}
|
|
|
|
Future<void> _determineRequireUserPermissionToResumeFromFlags() async {
|
|
try {
|
|
final result = await dds.vmServiceClient.sendRequest('getFlagList')
|
|
as Map<String, dynamic>;
|
|
final flagList = FlagList.parse(result);
|
|
final flags = flagList!.flags!;
|
|
|
|
bool? pauseOnStartValue;
|
|
bool? pauseOnExitValue;
|
|
for (final flag in flags) {
|
|
if (flag.name == 'pause_isolates_on_start') {
|
|
pauseOnStartValue = flag.valueAsString == 'true';
|
|
}
|
|
if (flag.name == 'pause_isolates_on_exit') {
|
|
pauseOnExitValue = flag.valueAsString == 'true';
|
|
}
|
|
if (pauseOnStartValue != null && pauseOnExitValue != null) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
_setRequireUserPermissionToResume(
|
|
onPauseStart: pauseOnStartValue ?? false,
|
|
onPauseExit: pauseOnExitValue ?? false,
|
|
);
|
|
} catch (_) {
|
|
// Swallow any errors. Otherwise, this will cause initialization to
|
|
// silently fail.
|
|
}
|
|
}
|
|
|
|
void _setRequireUserPermissionToResume({
|
|
required bool onPauseStart,
|
|
required bool onPauseExit,
|
|
}) {
|
|
int pauseTypeMask = 0;
|
|
if (onPauseStart) {
|
|
pauseTypeMask |= PauseTypeMasks.pauseOnStartMask;
|
|
}
|
|
if (onPauseExit) {
|
|
pauseTypeMask |= PauseTypeMasks.pauseOnExitMask;
|
|
}
|
|
_requireUserPermissionToResumeMask = pauseTypeMask;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> getPerfettoVMTimelineWithCpuSamples(
|
|
json_rpc.Parameters parameters) async {
|
|
final timeOriginMicros = parameters['timeOriginMicros'].asIntOr(-1);
|
|
final timeExtentMicros = parameters['timeExtentMicros'].asIntOr(-1);
|
|
|
|
final timelineResult = await dds.vmServiceClient.sendRequest(
|
|
'getPerfettoVMTimeline', {
|
|
'timeOriginMicros': timeOriginMicros,
|
|
'timeExtentMicros': timeExtentMicros
|
|
});
|
|
|
|
final combinedBytes = base64Decode(timelineResult['trace']).toList();
|
|
|
|
for (final isolateId in isolates.keys) {
|
|
try {
|
|
final samplesResult =
|
|
await dds.vmServiceClient.sendRequest('getPerfettoCpuSamples', {
|
|
'isolateId': isolateId,
|
|
'timeOriginMicros': timeOriginMicros,
|
|
'timeExtentMicros': timeExtentMicros
|
|
});
|
|
combinedBytes.addAll(base64Decode(samplesResult['samples']));
|
|
} on json_rpc.RpcException {
|
|
// The isolate may not yet be runnable.
|
|
}
|
|
}
|
|
timelineResult['trace'] = base64Encode(combinedBytes);
|
|
return timelineResult;
|
|
}
|
|
|
|
/// Forwards a `resume` request to the VM service.
|
|
Future<Map<String, dynamic>> _sendResumeRequest(
|
|
String isolateId,
|
|
json_rpc.Parameters parameters,
|
|
) async {
|
|
const invalidFrameIndex = -1;
|
|
final step = parameters['step'].asStringOr('');
|
|
final frameIndex = parameters['frameIndex'].asIntOr(invalidFrameIndex);
|
|
final resumeResult = await dds.vmServiceClient.sendRequest('resume', {
|
|
'isolateId': isolateId,
|
|
if (step.isNotEmpty) 'step': step,
|
|
if (frameIndex != invalidFrameIndex) 'frameIndex': frameIndex,
|
|
});
|
|
return resumeResult;
|
|
}
|
|
|
|
bool _initialized = false;
|
|
final DartDevelopmentServiceImpl dds;
|
|
final _mutex = Mutex();
|
|
int _requireUserPermissionToResumeMask = 0;
|
|
final isolates = <String, RunningIsolate>{};
|
|
}
|