Reland "[ VM / Service ] Add setIsolatePauseMode RPC"

This reverts commit d21897b88d.

Reason for revert: Reland before fixing failures in google3 (requires copybara update of package:vm_service which requires this change to have already been landed in the SDK).

Original change's description:
> Revert "[ VM / Service ] Add setIsolatePauseMode RPC"
>
> This reverts commit cad8a34d83.
>
> Reason for revert: broke the g3 cbuild, see cbuild logs
> (Error: The method 'setIsolatePauseMode' isn't defined for the class 'VmServiceInterface')
>
> Original change's description:
> > [ VM / Service ] Add setIsolatePauseMode RPC
> >
> > Allows for service clients to set pause behaviors on a per-isolate
> > basis at runtime. setIsolatePauseMode is a more general version of
> > setExceptionPauseMode and setExceptionPauseMode has been marked as deprecated.
> >
> > TEST=pause_on_exceptions_*_test.dart,should_pause_on_exit_test.dart
> >
> > Change-Id: I09d80aa2123791dd74d02441c162c19cc0486955
> > Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/219580
> > Commit-Queue: Ben Konyi <bkonyi@google.com>
> > Reviewed-by: Siva Annamalai <asiva@google.com>
>
> TBR=bkonyi@google.com,rmacnak@google.com,asiva@google.com
>
> Change-Id: Ic326c54e0fd682e382bd70e36d87467d4148c990
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/220067
> Reviewed-by: Siva Annamalai <asiva@google.com>
> Commit-Queue: Siva Annamalai <asiva@google.com>

# Not skipping CQ checks because this is a reland.

Change-Id: I0e25654e1991b7246cd413454ef45594d36f4bde
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/220127
Reviewed-by: Siva Annamalai <asiva@google.com>
This commit is contained in:
Ben Konyi 2021-11-12 23:24:08 +00:00
parent 39d930305c
commit 5de4830a4e
20 changed files with 453 additions and 15 deletions

View file

@ -581,7 +581,10 @@ class IsolateManager {
return;
}
await service.setExceptionPauseMode(isolate.id!, _exceptionPauseMode);
await service.setIsolatePauseMode(
isolate.id!,
exceptionPauseMode: _exceptionPauseMode,
);
}
/// Calls setLibraryDebuggable for all libraries in the given isolate based

View file

@ -25,7 +25,7 @@ dependencies:
shelf_web_socket: ^1.0.0
sse: ^4.0.0
stream_channel: ^2.0.0
vm_service: ^7.2.0
vm_service: ^7.5.0
web_socket_channel: ^2.0.0
dev_dependencies:

View file

@ -1,5 +1,10 @@
# Changelog
## 7.5.0
- Update to version `3.53` of the spec.
- Added `setIsolatePauseMode` RPC.
- Deprecated `setExceptionPauseMode` in favor of `setIsolatePauseMode`.
## 7.4.0
- Update to version `3.52` of the spec.
- Added `lookupResolvedPackageUris` and `lookupPackageUris` RPCs and `UriList`

View file

@ -36,6 +36,7 @@ src/org/dartlang/vm/service/consumer/RequestHeapSnapshotConsumer.java
src/org/dartlang/vm/service/consumer/ResumeConsumer.java
src/org/dartlang/vm/service/consumer/SetExceptionPauseModeConsumer.java
src/org/dartlang/vm/service/consumer/SetFlagConsumer.java
src/org/dartlang/vm/service/consumer/SetIsolatePauseModeConsumer.java
src/org/dartlang/vm/service/consumer/SetLibraryDebuggableConsumer.java
src/org/dartlang/vm/service/consumer/SetNameConsumer.java
src/org/dartlang/vm/service/consumer/SetTraceClassAllocationConsumer.java

View file

@ -1 +1 @@
version=3.52
version=3.53

View file

@ -26,7 +26,7 @@ export 'snapshot_graph.dart'
HeapSnapshotObjectNoData,
HeapSnapshotObjectNullData;
const String vmServiceVersion = '3.52.0';
const String vmServiceVersion = '3.53.0';
/// @optional
const String optional = 'optional';
@ -236,6 +236,7 @@ Map<String, List<String>> _methodReturnTypes = {
'resume': const ['Success'],
'setBreakpointState': const ['Breakpoint'],
'setExceptionPauseMode': const ['Success'],
'setIsolatePauseMode': const ['Success'],
'setFlag': const ['Success', 'Error'],
'setLibraryDebuggable': const ['Success'],
'setName': const ['Success'],
@ -1078,9 +1079,34 @@ abstract class VmServiceInterface {
///
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
@Deprecated('Use setIsolatePauseMode instead')
Future<Success> setExceptionPauseMode(
String isolateId, /*ExceptionPauseMode*/ String mode);
/// The `setIsolatePauseMode` RPC is used to control if or when an isolate
/// will pause due to a change in execution state.
///
/// The `shouldPauseOnExit` parameter specify whether the target isolate
/// should pause on exit.
///
/// The `setExceptionPauseMode` RPC is used to control if an isolate pauses
/// when an exception is thrown.
///
/// mode | meaning
/// ---- | -------
/// None | Do not pause isolate on thrown exceptions
/// Unhandled | Pause isolate on unhandled exceptions
/// All | Pause isolate on all thrown exceptions
///
/// If `isolateId` refers to an isolate which has exited, then the `Collected`
/// [Sentinel] is returned.
///
/// This method will throw a [SentinelException] in the case a [Sentinel] is
/// returned.
Future<Success> setIsolatePauseMode(String isolateId,
{/*ExceptionPauseMode*/ String? exceptionPauseMode,
bool? shouldPauseOnExit});
/// The `setFlag` RPC is used to set a VM flag at runtime. Returns an error if
/// the named flag does not exist, the flag may not be set at runtime, or the
/// value is of the wrong type for the flag.
@ -1101,6 +1127,7 @@ abstract class VmServiceInterface {
/// provided value. If set to false when the profiler is already running, the
/// profiler will be stopped but may not free its sample buffer depending on
/// platform limitations.
/// - Isolate pause settings will only be applied to newly spawned isolates.
///
/// See [Success].
///
@ -1549,11 +1576,19 @@ class VmServerConnection {
);
break;
case 'setExceptionPauseMode':
// ignore: deprecated_member_use_from_same_package
response = await _serviceImplementation.setExceptionPauseMode(
params!['isolateId'],
params['mode'],
);
break;
case 'setIsolatePauseMode':
response = await _serviceImplementation.setIsolatePauseMode(
params!['isolateId'],
exceptionPauseMode: params['exceptionPauseMode'],
shouldPauseOnExit: params['shouldPauseOnExit'],
);
break;
case 'setFlag':
response = await _serviceImplementation.setFlag(
params!['name'],
@ -2081,11 +2116,23 @@ class VmService implements VmServiceInterface {
'enable': enable
});
@Deprecated('Use setIsolatePauseMode instead')
@override
Future<Success> setExceptionPauseMode(
String isolateId, /*ExceptionPauseMode*/ String mode) =>
_call('setExceptionPauseMode', {'isolateId': isolateId, 'mode': mode});
@override
Future<Success> setIsolatePauseMode(String isolateId,
{/*ExceptionPauseMode*/ String? exceptionPauseMode,
bool? shouldPauseOnExit}) =>
_call('setIsolatePauseMode', {
'isolateId': isolateId,
if (exceptionPauseMode != null)
'exceptionPauseMode': exceptionPauseMode,
if (shouldPauseOnExit != null) 'shouldPauseOnExit': shouldPauseOnExit,
});
@override
Future<Response> setFlag(String name, String value) =>
_call('setFlag', {'name': name, 'value': value});

View file

@ -3,7 +3,7 @@ description: >-
A library to communicate with a service implementing the Dart VM
service protocol.
version: 7.4.0
version: 7.5.0
homepage: https://github.com/dart-lang/sdk/tree/master/pkg/vm_service

View file

@ -277,7 +277,6 @@ class _ServiceTesterRunner {
vm = await vmServiceConnectUri(serviceWebsocketAddress);
print('Done loading VM');
isolate = await getFirstIsolate(vm);
print('Got first isolate');
});
});
@ -335,14 +334,12 @@ class _ServiceTesterRunner {
if (event.kind == EventKind.kIsolateRunnable) {
print(event.isolate!.name);
vm = await service.getVM();
//assert(vmIsolates.isNotEmpty);
await subscription.cancel();
await service.streamCancel(EventStreams.kIsolate);
completer!.complete(event.isolate!);
completer = null;
}
});
await service.streamListen(EventStreams.kIsolate);
// The isolate may have started before we subscribed.

View file

@ -0,0 +1,112 @@
// Copyright (c) 2021, 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 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/test_helper.dart';
doThrow() {
throw "TheException"; // Line 13.
}
doCaught() {
try {
doThrow();
} catch (e) {
return "end of doCaught";
}
}
doUncaught() {
doThrow();
return "end of doUncaught";
}
final tests = <IsolateTest>[
(VmService service, IsolateRef isolateRef) async {
final isolate = await service.getIsolate(isolateRef.id!);
final lib = await service.getObject(isolateRef.id!, isolate.rootLib!.id!);
Completer? onPaused;
Completer? onResume;
final stream = service.onDebugEvent;
final subscription = stream.listen((Event event) {
print("Event $event");
if (event.kind == EventKind.kPauseException) {
if (onPaused == null) throw "Unexpected pause event $event";
final t = onPaused;
onPaused = null;
t!.complete(event);
}
if (event.kind == EventKind.kResume) {
if (onResume == null) throw "Unexpected resume event $event";
final t = onResume;
onResume = null;
t!.complete(event);
}
});
await service.streamListen(EventStreams.kDebug);
test(String pauseMode, String expression, bool shouldPause,
bool shouldBeCaught) async {
print("Evaluating $expression with pause on $pauseMode exception");
// ignore: deprecated_member_use_from_same_package
await service.setExceptionPauseMode(isolate.id!, pauseMode);
late Completer t;
if (shouldPause) {
t = Completer();
onPaused = t;
}
final fres = service.evaluate(isolate.id!, lib.id!, expression);
if (shouldPause) {
await t.future;
final stack = await service.getStack(isolate.id!);
expect(stack.frames![0].function!.name, 'doThrow');
t = Completer();
onResume = t;
await service.resume(isolate.id!);
await t.future;
}
dynamic res = await fres;
if (shouldBeCaught) {
expect(res is InstanceRef, true);
expect(res.kind, 'String');
expect(res.valueAsString, equals("end of doCaught"));
} else {
print(res.json);
expect(res is ErrorRef, true);
res = await service.getObject(isolate.id!, res.id!);
expect(res is Error, true);
expect(res.exception.kind, 'String');
expect(res.exception.valueAsString, equals("TheException"));
}
}
await test("All", "doCaught()", true, true);
await test("All", "doUncaught()", true, false);
await test("Unhandled", "doCaught()", false, true);
await test("Unhandled", "doUncaught()", true, false);
await test("None", "doCaught()", false, true);
await test("None", "doUncaught()", false, false);
await subscription.cancel();
},
];
main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'pause_on_exceptions_test.dart',
);

View file

@ -0,0 +1,112 @@
// Copyright (c) 2021, 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 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/test_helper.dart';
doThrow() {
throw "TheException"; // Line 13.
}
doCaught() {
try {
doThrow();
} catch (e) {
return "end of doCaught";
}
}
doUncaught() {
doThrow();
return "end of doUncaught";
}
final tests = <IsolateTest>[
(VmService service, IsolateRef isolateRef) async {
final isolate = await service.getIsolate(isolateRef.id!);
final lib = await service.getObject(isolateRef.id!, isolate.rootLib!.id!);
Completer? onPaused;
Completer? onResume;
final stream = service.onDebugEvent;
final subscription = stream.listen((Event event) {
print("Event $event");
if (event.kind == EventKind.kPauseException) {
if (onPaused == null) throw "Unexpected pause event $event";
final t = onPaused;
onPaused = null;
t!.complete(event);
}
if (event.kind == EventKind.kResume) {
if (onResume == null) throw "Unexpected resume event $event";
final t = onResume;
onResume = null;
t!.complete(event);
}
});
await service.streamListen(EventStreams.kDebug);
test(String pauseMode, String expression, bool shouldPause,
bool shouldBeCaught) async {
print("Evaluating $expression with pause on $pauseMode exception");
await service.setIsolatePauseMode(isolate.id!,
exceptionPauseMode: pauseMode);
late Completer t;
if (shouldPause) {
t = Completer();
onPaused = t;
}
final fres = service.evaluate(isolate.id!, lib.id!, expression);
if (shouldPause) {
await t.future;
final stack = await service.getStack(isolate.id!);
expect(stack.frames![0].function!.name, 'doThrow');
t = Completer();
onResume = t;
await service.resume(isolate.id!);
await t.future;
}
dynamic res = await fres;
if (shouldBeCaught) {
expect(res is InstanceRef, true);
expect(res.kind, 'String');
expect(res.valueAsString, equals("end of doCaught"));
} else {
print(res.json);
expect(res is ErrorRef, true);
res = await service.getObject(isolate.id!, res.id!);
expect(res is Error, true);
expect(res.exception.kind, 'String');
expect(res.exception.valueAsString, equals("TheException"));
}
}
await test("All", "doCaught()", true, true);
await test("All", "doUncaught()", true, false);
await test("Unhandled", "doCaught()", false, true);
await test("Unhandled", "doUncaught()", true, false);
await test("None", "doCaught()", false, true);
await test("None", "doUncaught()", false, false);
await subscription.cancel();
},
];
main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'pause_on_exceptions_test.dart',
);

View file

@ -0,0 +1,51 @@
// Copyright (c) 2021, 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 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/test_helper.dart';
void testMain() {
print('Hello world!');
}
Future<bool> shouldPauseOnExit(VmService service, IsolateRef isolateRef) async {
final isolate = await service.getIsolate(isolateRef.id!);
return isolate.pauseOnExit!;
}
final tests = <IsolateTest>[
(VmService service, IsolateRef isolateRef) async {
await service.setIsolatePauseMode(isolateRef.id!, shouldPauseOnExit: false);
expect(await shouldPauseOnExit(service, isolateRef), false);
final completer = Completer<void>();
final stream = service.onDebugEvent;
final subscription = stream.listen((Event event) {
if (event.kind == EventKind.kPauseExit) {
completer.complete();
}
});
await service.streamListen(EventStreams.kDebug);
await service.setIsolatePauseMode(isolateRef.id!, shouldPauseOnExit: true);
expect(await shouldPauseOnExit(service, isolateRef), true);
await service.resume(isolateRef.id!);
await completer.future;
await service.resume(isolateRef.id!);
await subscription.cancel();
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'should_pause_on_exit_test.dart',
pause_on_start: true,
pause_on_exit: true,
testeeConcurrent: testMain,
);

View file

@ -167,6 +167,23 @@ abstract class Parser {
.trim();
}
String? consumeString() {
StringBuffer buf = StringBuffer();
String startQuotation = advance()!.text!;
if (startQuotation != '"' && startQuotation != "'") {
return null;
}
while (peek()!.text != startQuotation) {
Token t = advance()!;
if (t.text == null) {
throw FormatException('Reached EOF');
}
buf.write('${t.text} ');
}
advance();
return buf.toString().trim();
}
void validate(bool result, String message) {
if (!result) throw 'expected ${message}';
}

View file

@ -725,6 +725,9 @@ abstract class VmServiceInterface {
}
return result;
};
if (m.deprecated) {
gen.writeln("// ignore: deprecated_member_use_from_same_package");
}
gen.write("response = await _serviceImplementation.${m.name}(");
// Positional args
m.args.where((arg) => !arg.optional).forEach((MethodArg arg) {
@ -1130,6 +1133,8 @@ class Method extends Member {
final String? docs;
MemberType returnType = MemberType();
bool get deprecated => deprecationMessage != null;
String? deprecationMessage;
List<MethodArg> args = [];
Method(this.name, String definition, [this.docs]) {
@ -1196,6 +1201,9 @@ class Method extends Member {
}
if (_docs.isNotEmpty) gen.writeDocs(_docs);
}
if (deprecated) {
gen.writeln("@Deprecated('$deprecationMessage')");
}
if (withOverrides) gen.writeln('@override');
gen.write('Future<${returnType.name}> ${name}(');
bool startedOptional = false;
@ -2128,7 +2136,12 @@ class MethodParser extends Parser {
void parseInto(Method method) {
// method is return type, name, (, args )
// args is type name, [optional], comma
if (peek()?.text?.startsWith('@deprecated') ?? false) {
advance();
expect('(');
method.deprecationMessage = consumeString()!;
expect(')');
}
method.returnType.parse(this, isReturnType: true);
Token t = expectName();

View file

@ -496,6 +496,7 @@ class Method extends Member {
final String? docs;
MemberType returnType = MemberType();
bool deprecated = false;
List<MethodArg> args = [];
Method(this.name, String definition, [this.docs]) {
@ -606,7 +607,7 @@ class Method extends Member {
}
}
writer.addLine('request("$name", params, consumer);');
}, javadoc: javadoc.toString());
}, javadoc: javadoc.toString(), isDeprecated: deprecated);
}
void _parse(Token? token) {
@ -647,6 +648,13 @@ class MethodParser extends Parser {
// method is return type, name, (, args )
// args is type name, [optional], comma
if (peek()?.text?.startsWith('@deprecated') ?? false) {
advance();
expect('(');
consumeString();
expect(')');
method.deprecated = true;
}
method.returnType.parse(this);
Token t = expectName();

View file

@ -214,6 +214,7 @@ class TypeWriter {
String? modifiers = 'public',
String? returnType = 'void',
bool isOverride = false,
bool isDeprecated = false,
}) {
var methodDecl = StringBuffer();
if (javadoc != null && javadoc.isNotEmpty) {
@ -223,6 +224,9 @@ class TypeWriter {
.forEach((line) => methodDecl.writeln(' * $line'.trimRight()));
methodDecl.writeln(' */');
}
if (isDeprecated) {
methodDecl.writeln(' @Deprecated');
}
if (isOverride) {
methodDecl.writeln(' @Override');
}

View file

@ -12,7 +12,7 @@ var tests = <VMTest>[
final result = await vm.invokeRpcNoUpgrade('getVersion', {});
expect(result['type'], 'Version');
expect(result['major'], 3);
expect(result['minor'], 52);
expect(result['minor'], 53);
expect(result['_privateMajor'], 0);
expect(result['_privateMinor'], 0);
},

View file

@ -12,7 +12,7 @@ var tests = <VMTest>[
final result = await vm.invokeRpcNoUpgrade('getVersion', {});
expect(result['type'], equals('Version'));
expect(result['major'], equals(3));
expect(result['minor'], equals(52));
expect(result['minor'], equals(53));
expect(result['_privateMajor'], equals(0));
expect(result['_privateMinor'], equals(0));
},

View file

@ -5164,6 +5164,44 @@ static void SetExceptionPauseMode(Thread* thread, JSONStream* js) {
PrintSuccess(js);
}
static const MethodParameter* const set_isolate_pause_mode_params[] = {
ISOLATE_PARAMETER,
new EnumParameter("exceptionPauseMode", false, exception_pause_mode_names),
new BoolParameter("shouldPauseOnExit", false),
nullptr,
};
static void SetIsolatePauseMode(Thread* thread, JSONStream* js) {
bool state_changed = false;
const char* exception_pause_mode = js->LookupParam("exceptionPauseMode");
if (exception_pause_mode != nullptr) {
Dart_ExceptionPauseInfo info =
EnumMapper(exception_pause_mode, exception_pause_mode_names,
exception_pause_mode_values);
if (info == kInvalidExceptionPauseInfo) {
PrintInvalidParamError(js, "exceptionPauseMode");
return;
}
Isolate* isolate = thread->isolate();
isolate->debugger()->SetExceptionPauseInfo(info);
state_changed = true;
}
const char* pause_isolate_on_exit = js->LookupParam("shouldPauseOnExit");
if (pause_isolate_on_exit != nullptr) {
bool enable = BoolParameter::Parse(pause_isolate_on_exit, false);
thread->isolate()->message_handler()->set_should_pause_on_exit(enable);
state_changed = true;
}
if (state_changed && Service::debug_stream.enabled()) {
ServiceEvent event(thread->isolate(),
ServiceEvent::kDebuggerSettingsUpdate);
Service::HandleEvent(&event);
}
PrintSuccess(js);
}
static const MethodParameter* const set_breakpoint_state_params[] = {
ISOLATE_PARAMETER,
new IdParameter("breakpointId", true),
@ -5580,6 +5618,8 @@ static const ServiceMethodDescriptor service_methods_[] = {
set_breakpoint_state_params },
{ "setExceptionPauseMode", SetExceptionPauseMode,
set_exception_pause_mode_params },
{ "setIsolatePauseMode", SetIsolatePauseMode,
set_isolate_pause_mode_params },
{ "setFlag", SetFlag,
set_flags_params },
{ "setLibraryDebuggable", SetLibraryDebuggable,

View file

@ -15,7 +15,7 @@
namespace dart {
#define SERVICE_PROTOCOL_MAJOR_VERSION 3
#define SERVICE_PROTOCOL_MINOR_VERSION 52
#define SERVICE_PROTOCOL_MINOR_VERSION 53
class Array;
class EmbedderServiceHandler;

View file

@ -1,8 +1,8 @@
# Dart VM Service Protocol 3.52
# Dart VM Service Protocol 3.53
> Please post feedback to the [observatory-discuss group][discuss-list]
This document describes of _version 3.52_ of the Dart VM Service Protocol. This
This document describes of _version 3.53_ of the Dart VM Service Protocol. This
protocol is used to communicate with a running Dart Virtual Machine.
To use the Service Protocol, start the VM with the *--observe* flag.
@ -1378,6 +1378,7 @@ See [Breakpoint](#breakpoint).
### setExceptionPauseMode
```
@deprecated('Use setIsolatePauseMode instead')
Success|Sentinel setExceptionPauseMode(string isolateId,
ExceptionPauseMode mode)
```
@ -1394,6 +1395,31 @@ All | Pause isolate on all thrown exceptions
If _isolateId_ refers to an isolate which has exited, then the
_Collected_ [Sentinel](#sentinel) is returned.
### setIsolatePauseMode
```
Success|Sentinel setIsolatePauseMode(string isolateId,
ExceptionPauseMode exceptionPauseMode [optional],
bool shouldPauseOnExit [optional])
```
The _setIsolatePauseMode_ RPC is used to control if or when an isolate will
pause due to a change in execution state.
The _shouldPauseOnExit_ parameter specify whether the target isolate should pause on exit.
The _setExceptionPauseMode_ RPC is used to control if an isolate pauses when
an exception is thrown.
mode | meaning
---- | -------
None | Do not pause isolate on thrown exceptions
Unhandled | Pause isolate on unhandled exceptions
All | Pause isolate on all thrown exceptions
If _isolateId_ refers to an isolate which has exited, then the
_Collected_ [Sentinel](#sentinel) is returned.
### setFlag
```
@ -1420,6 +1446,7 @@ Notes:
provided value. If set to false when the profiler is already running, the
profiler will be stopped but may not free its sample buffer depending on
platform limitations.
* Isolate pause settings will only be applied to newly spawned isolates.
See [Success](#success).
@ -4235,4 +4262,5 @@ version | comments
3.50 | Added `returnType`, `parameters`, and `typeParameters` to `@Instance`, and `implicit` to `@Function`. Added `Parameter` type.
3.51 | Added optional `reportLines` parameter to `getSourceReport` RPC.
3.52 | Added `lookupResolvedPackageUris` and `lookupPackageUris` RPCs and `UriList` type.
3.53 | Added `setIsolatePauseMode` RPC.
[discuss-list]: https://groups.google.com/a/dartlang.org/forum/#!forum/observatory-discuss