[3.0 alpha][VM/Service] Update VM Service spec to v4.0

This CL updates the VM Service spec to version 4.0 in order to add
support for records. Some deprecated procedures and properties will also
be removed in v4.0.

As described in service.md's changelog, this CL:
Adds `Record` and `RecordType` `InstanceKind`s, adds a deprecation
notice to the `decl` property of `BoundField`, adds `name` property to
`BoundField`, adds a deprecation notice to the `parentListIndex`
property of `InboundReference`, changes the type of the `parentField`
property of `InboundReference` from `@Field` to `@Field|string|int`,
adds a deprecation notice to the `parentListIndex` property of
`RetainingObject`, changes the type of the `parentField` property of
`RetainingObject` from `string` to `string|int`, removes the deprecated
`setExceptionPauseMode` procedure, removes the deprecated `timeSpan`
property from `CpuSamples`, and removes the deprecated `timeSpan`
property from `CpuSamplesEvent.

TEST=CI

Issue: https://github.com/dart-lang/sdk/issues/49725
Change-Id: I7bf61c1ba11a0c7fd95a10c9c02c14282062b802
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/268521
Commit-Queue: Derek Xu <derekx@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dds_service_extensions/lib/dds_service_extensions.dart b/pkg/dds_service_extensions/lib/dds_service_extensions.dart
index 702cf58..e28fb7b 100644
--- a/pkg/dds_service_extensions/lib/dds_service_extensions.dart
+++ b/pkg/dds_service_extensions/lib/dds_service_extensions.dart
@@ -200,7 +200,6 @@
     required int? samplePeriod,
     required int? maxStackDepth,
     required int? sampleCount,
-    required int? timeSpan,
     required int? timeOriginMicros,
     required int? timeExtentMicros,
     required int? pid,
@@ -210,7 +209,6 @@
           samplePeriod: samplePeriod,
           maxStackDepth: maxStackDepth,
           sampleCount: sampleCount,
-          timeSpan: timeSpan,
           timeOriginMicros: timeOriginMicros,
           timeExtentMicros: timeExtentMicros,
           pid: pid,
@@ -225,7 +223,6 @@
           samplePeriod: json['samplePeriod'] ?? -1,
           maxStackDepth: json['maxStackDepth'] ?? -1,
           sampleCount: json['sampleCount'] ?? -1,
-          timeSpan: json['timeSpan'] ?? -1,
           timeOriginMicros: json['timeOriginMicros'] ?? -1,
           timeExtentMicros: json['timeExtentMicros'] ?? -1,
           pid: json['pid'] ?? -1,
diff --git a/pkg/vm_service/java/.gitignore b/pkg/vm_service/java/.gitignore
index 5fe823e..9e1e42e 100644
--- a/pkg/vm_service/java/.gitignore
+++ b/pkg/vm_service/java/.gitignore
@@ -34,7 +34,6 @@
 src/org/dartlang/vm/service/consumer/RemoveBreakpointConsumer.java
 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
diff --git a/pkg/vm_service/java/test/org/dartlang/vm/service/VmServiceTest.java b/pkg/vm_service/java/test/org/dartlang/vm/service/VmServiceTest.java
index aa191ed..5b31a47 100644
--- a/pkg/vm_service/java/test/org/dartlang/vm/service/VmServiceTest.java
+++ b/pkg/vm_service/java/test/org/dartlang/vm/service/VmServiceTest.java
@@ -587,17 +587,17 @@
   private static void vmPauseOnException(IsolateRef isolate, ExceptionPauseMode mode) {
     System.out.println("Request pause on exception: " + mode);
     final OpLatch latch = new OpLatch();
-    vmService.setExceptionPauseMode(isolate.getId(), mode, new SuccessConsumer() {
-      @Override
-      public void onError(RPCError error) {
-        showRPCError(error);
-      }
+    vmService.setIsolatePauseMode(isolate.getId(), mode, new SuccessConsumer() {
+        @Override
+        public void onError(RPCError error) {
+            showRPCError(error);
+        }
 
-      @Override
-      public void received(Success response) {
-        System.out.println("Successfully set pause on exception");
-        latch.opComplete();
-      }
+        @Override
+        public void received(Success response) {
+            System.out.println("Successfully set pause on exception");
+            latch.opComplete();
+        }
     });
     latch.waitAndAssertOpComplete();
   }
diff --git a/pkg/vm_service/java/version.properties b/pkg/vm_service/java/version.properties
index 8822aaf..db1ef8e5 100644
--- a/pkg/vm_service/java/version.properties
+++ b/pkg/vm_service/java/version.properties
@@ -1 +1 @@
-version=3.62
+version=4.0
diff --git a/pkg/vm_service/lib/src/vm_service.dart b/pkg/vm_service/lib/src/vm_service.dart
index 7b4745f..0e633ff 100644
--- a/pkg/vm_service/lib/src/vm_service.dart
+++ b/pkg/vm_service/lib/src/vm_service.dart
@@ -26,7 +26,7 @@
         HeapSnapshotObjectNoData,
         HeapSnapshotObjectNullData;
 
-const String vmServiceVersion = '3.62.0';
+const String vmServiceVersion = '4.0.0';
 
 /// @optional
 const String optional = 'optional';
@@ -236,7 +236,6 @@
   'requestHeapSnapshot': const ['Success'],
   'resume': const ['Success'],
   'setBreakpointState': const ['Breakpoint'],
-  'setExceptionPauseMode': const ['Success'],
   'setIsolatePauseMode': const ['Success'],
   'setFlag': const ['Success', 'Error'],
   'setLibraryDebuggable': const ['Success'],
@@ -1077,33 +1076,12 @@
   Future<Breakpoint> setBreakpointState(
       String isolateId, String breakpointId, bool enable);
 
-  /// 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.
-  @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
@@ -1598,13 +1576,6 @@
             params['enable'],
           );
           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'],
@@ -2149,12 +2120,6 @@
         '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,
@@ -2717,6 +2682,9 @@
   static const String kFloat32x4List = 'Float32x4List';
   static const String kFloat64x2List = 'Float64x2List';
 
+  /// An instance of the Dart class Record.
+  static const String kRecord = 'Record';
+
   /// An instance of the Dart class StackTrace.
   static const String kStackTrace = 'StackTrace';
 
@@ -2748,6 +2716,9 @@
   /// An instance of the Dart class FunctionType.
   static const String kFunctionType = 'FunctionType';
 
+  /// An instance of the Dart class RecordType.
+  static const String kRecordType = 'RecordType';
+
   /// An instance of the Dart class BoundedType.
   static const String kBoundedType = 'BoundedType';
 
@@ -2907,18 +2878,29 @@
   static BoundField? parse(Map<String, dynamic>? json) =>
       json == null ? null : BoundField._fromJson(json);
 
+  /// Provided for fields of instances that are NOT of the following instance
+  /// kinds:
+  ///  - Record
+  ///
+  /// Note: this property is deprecated and will be replaced by `name`.
   FieldRef? decl;
 
+  /// [name] can be one of [String] or [int].
+  dynamic name;
+
   /// [value] can be one of [InstanceRef] or [Sentinel].
   dynamic value;
 
   BoundField({
     this.decl,
+    this.name,
     this.value,
   });
 
   BoundField._fromJson(Map<String, dynamic> json) {
     decl = createServiceObject(json['decl'], const ['FieldRef']) as FieldRef?;
+    name =
+        createServiceObject(json['name'], const ['String', 'int']) as dynamic;
     value =
         createServiceObject(json['value'], const ['InstanceRef', 'Sentinel'])
             as dynamic;
@@ -2928,12 +2910,14 @@
     final json = <String, dynamic>{};
     json.addAll({
       'decl': decl?.toJson(),
+      'name': name?.toJson(),
       'value': value?.toJson(),
     });
     return json;
   }
 
-  String toString() => '[BoundField decl: ${decl}, value: ${value}]';
+  String toString() =>
+      '[BoundField decl: ${decl}, name: ${name}, value: ${value}]';
 }
 
 /// A `BoundVariable` represents a local variable bound to a particular value in
@@ -3626,13 +3610,6 @@
   /// The number of samples returned.
   int? sampleCount;
 
-  /// The timespan the set of returned samples covers, in microseconds
-  /// (deprecated).
-  ///
-  /// Note: this property is deprecated and will always return -1. Use
-  /// `timeExtentMicros` instead.
-  int? timeSpan;
-
   /// The start of the period of time in which the returned samples were
   /// collected.
   int? timeOriginMicros;
@@ -3656,7 +3633,6 @@
     this.samplePeriod,
     this.maxStackDepth,
     this.sampleCount,
-    this.timeSpan,
     this.timeOriginMicros,
     this.timeExtentMicros,
     this.pid,
@@ -3668,7 +3644,6 @@
     samplePeriod = json['samplePeriod'] ?? -1;
     maxStackDepth = json['maxStackDepth'] ?? -1;
     sampleCount = json['sampleCount'] ?? -1;
-    timeSpan = json['timeSpan'] ?? -1;
     timeOriginMicros = json['timeOriginMicros'] ?? -1;
     timeExtentMicros = json['timeExtentMicros'] ?? -1;
     pid = json['pid'] ?? -1;
@@ -3692,7 +3667,6 @@
       'samplePeriod': samplePeriod ?? -1,
       'maxStackDepth': maxStackDepth ?? -1,
       'sampleCount': sampleCount ?? -1,
-      'timeSpan': timeSpan ?? -1,
       'timeOriginMicros': timeOriginMicros ?? -1,
       'timeExtentMicros': timeExtentMicros ?? -1,
       'pid': pid ?? -1,
@@ -3702,7 +3676,9 @@
     return json;
   }
 
-  String toString() => '[CpuSamples]';
+  String toString() => '[CpuSamples ' //
+      'samplePeriod: ${samplePeriod}, maxStackDepth: ${maxStackDepth}, ' //
+      'sampleCount: ${sampleCount}, timeOriginMicros: ${timeOriginMicros}, timeExtentMicros: ${timeExtentMicros}, pid: ${pid}, functions: ${functions}, samples: ${samples}]';
 }
 
 class CpuSamplesEvent {
@@ -3718,13 +3694,6 @@
   /// The number of samples returned.
   int? sampleCount;
 
-  /// The timespan the set of returned samples covers, in microseconds
-  /// (deprecated).
-  ///
-  /// Note: this property is deprecated and will always return -1. Use
-  /// `timeExtentMicros` instead.
-  int? timeSpan;
-
   /// The start of the period of time in which the returned samples were
   /// collected.
   int? timeOriginMicros;
@@ -3748,7 +3717,6 @@
     this.samplePeriod,
     this.maxStackDepth,
     this.sampleCount,
-    this.timeSpan,
     this.timeOriginMicros,
     this.timeExtentMicros,
     this.pid,
@@ -3760,7 +3728,6 @@
     samplePeriod = json['samplePeriod'] ?? -1;
     maxStackDepth = json['maxStackDepth'] ?? -1;
     sampleCount = json['sampleCount'] ?? -1;
-    timeSpan = json['timeSpan'] ?? -1;
     timeOriginMicros = json['timeOriginMicros'] ?? -1;
     timeExtentMicros = json['timeExtentMicros'] ?? -1;
     pid = json['pid'] ?? -1;
@@ -3778,7 +3745,6 @@
       'samplePeriod': samplePeriod ?? -1,
       'maxStackDepth': maxStackDepth ?? -1,
       'sampleCount': sampleCount ?? -1,
-      'timeSpan': timeSpan ?? -1,
       'timeOriginMicros': timeOriginMicros ?? -1,
       'timeExtentMicros': timeExtentMicros ?? -1,
       'pid': pid ?? -1,
@@ -3788,7 +3754,9 @@
     return json;
   }
 
-  String toString() => '[CpuSamplesEvent]';
+  String toString() => '[CpuSamplesEvent ' //
+      'samplePeriod: ${samplePeriod}, maxStackDepth: ${maxStackDepth}, ' //
+      'sampleCount: ${sampleCount}, timeOriginMicros: ${timeOriginMicros}, timeExtentMicros: ${timeExtentMicros}, pid: ${pid}, functions: ${functions}, samples: ${samples}]';
 }
 
 /// See [getCpuSamples] and [CpuSamples].
@@ -6023,15 +5991,24 @@
   /// The object holding the inbound reference.
   ObjRef? source;
 
-  /// If source is a List, parentListIndex is the index of the inbound
-  /// reference.
+  /// If source is a List, parentListIndex is the index of the inbound reference
+  /// (deprecated).
+  ///
+  /// Note: this property is deprecated and will be replaced by `parentField`.
   @optional
   int? parentListIndex;
 
-  /// If source is a field of an object, parentField is the field containing the
-  /// inbound reference.
+  /// If `source` is a `List`, `parentField` is the index of the inbound
+  /// reference. If `source` is a record, `parentField` is the field name of the
+  /// inbound reference. If `source` is an instance of any other kind,
+  /// `parentField` is the field containing the inbound reference.
+  ///
+  /// Note: In v5.0 of the spec, `@Field` will no longer be a part of this
+  /// property's type, i.e. the type will become `string|int`.
+  ///
+  /// [parentField] can be one of [FieldRef], [String] or [int].
   @optional
-  FieldRef? parentField;
+  dynamic parentField;
 
   InboundReference({
     this.source,
@@ -6042,8 +6019,8 @@
   InboundReference._fromJson(Map<String, dynamic> json) {
     source = createServiceObject(json['source'], const ['ObjRef']) as ObjRef?;
     parentListIndex = json['parentListIndex'];
-    parentField = createServiceObject(json['parentField'], const ['FieldRef'])
-        as FieldRef?;
+    parentField = createServiceObject(
+        json['parentField'], const ['FieldRef', 'String', 'int']) as dynamic;
   }
 
   Map<String, dynamic> toJson() {
@@ -7141,7 +7118,9 @@
   ObjRef? value;
 
   /// If `value` is a List, `parentListIndex` is the index where the previous
-  /// object on the retaining path is located.
+  /// object on the retaining path is located (deprecated).
+  ///
+  /// Note: this property is deprecated and will be replaced by `parentField`.
   @optional
   int? parentListIndex;
 
@@ -7152,8 +7131,10 @@
 
   /// If `value` is a non-List, non-Map object, `parentField` is the name of the
   /// field containing the previous object on the retaining path.
+  ///
+  /// [parentField] can be one of [String] or [int].
   @optional
-  String? parentField;
+  dynamic parentField;
 
   RetainingObject({
     this.value,
@@ -7167,7 +7148,9 @@
     parentListIndex = json['parentListIndex'];
     parentMapKey =
         createServiceObject(json['parentMapKey'], const ['ObjRef']) as ObjRef?;
-    parentField = json['parentField'];
+    parentField =
+        createServiceObject(json['parentField'], const ['String', 'int'])
+            as dynamic;
   }
 
   Map<String, dynamic> toJson() {
@@ -7177,7 +7160,7 @@
     });
     _setIfNotNull(json, 'parentListIndex', parentListIndex);
     _setIfNotNull(json, 'parentMapKey', parentMapKey?.toJson());
-    _setIfNotNull(json, 'parentField', parentField);
+    _setIfNotNull(json, 'parentField', parentField?.toJson());
     return json;
   }
 
diff --git a/pkg/vm_service/test/get_object_rpc_test.dart b/pkg/vm_service/test/get_object_rpc_test.dart
index 34f880a..0fe673a 100644
--- a/pkg/vm_service/test/get_object_rpc_test.dart
+++ b/pkg/vm_service/test/get_object_rpc_test.dart
@@ -7,6 +7,7 @@
 
 library get_object_rpc_test;
 
+import 'dart:collection';
 import 'dart:convert' show base64Decode;
 import 'dart:typed_data';
 
@@ -693,19 +694,23 @@
             as InstanceRef;
     final objectId = evalResult.id!;
     final result = await service.getObject(isolateId, objectId) as Instance;
-    expect(result.kind, '_Record');
+    expect(result.kind, InstanceKind.kRecord);
     expect(result.json!['_vmType'], 'Record');
     expect(result.id, startsWith('objects/'));
     expect(result.valueAsString, isNull);
     expect(result.classRef!.name, '_Record');
     expect(result.size, isPositive);
-    final fields = result.fields!;
-    expect(fields.length, 4);
-    // TODO(derekx): Include field names in this test once they are accessible
-    // through package:vm_service.
-    Set<String> fieldValues =
-        Set.from(fields.map((f) => f.value.valueAsString));
-    expect(fieldValues.containsAll(['1', '2', '3.0', '4.0']), true);
+    final fieldsMap = HashMap.fromEntries(
+        result.fields!.map((f) => MapEntry(f.name, f.value)));
+    expect(fieldsMap.keys.length, 4);
+    expect(fieldsMap.containsKey(0), true);
+    expect(fieldsMap[0].valueAsString, '1');
+    expect(fieldsMap.containsKey("x"), true);
+    expect(fieldsMap["x"].valueAsString, '2');
+    expect(fieldsMap.containsKey(1), true);
+    expect(fieldsMap[1].valueAsString, '3.0');
+    expect(fieldsMap.containsKey("y"), true);
+    expect(fieldsMap["y"].valueAsString, '4.0');
   },
 
   // library.
diff --git a/pkg/vm_service/test/pause_on_exceptions_legacy_test.dart b/pkg/vm_service/test/pause_on_exceptions_legacy_test.dart
deleted file mode 100644
index 21ff3a8..0000000
--- a/pkg/vm_service/test/pause_on_exceptions_legacy_test.dart
+++ /dev/null
@@ -1,112 +0,0 @@
-// 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',
-    );
diff --git a/runtime/observatory/lib/src/elements/debugger.dart b/runtime/observatory/lib/src/elements/debugger.dart
index a4163a0..48fad75 100644
--- a/runtime/observatory/lib/src/elements/debugger.dart
+++ b/runtime/observatory/lib/src/elements/debugger.dart
@@ -637,7 +637,7 @@
   };
 
   static Future _setBreakOnException(debugger, name, value) async {
-    var result = await debugger.isolate.setExceptionPauseMode(value);
+    var result = await debugger.isolate.setIsolatePauseMode(value);
     if (result.isError) {
       debugger.console.print(result.toString());
     } else {
diff --git a/runtime/observatory/lib/src/service/object.dart b/runtime/observatory/lib/src/service/object.dart
index 81d1cea..d4b4505 100644
--- a/runtime/observatory/lib/src/service/object.dart
+++ b/runtime/observatory/lib/src/service/object.dart
@@ -1886,8 +1886,8 @@
     return invokeRpc('setName', {'name': newName});
   }
 
-  Future setExceptionPauseMode(String mode) {
-    return invokeRpc('setExceptionPauseMode', {'mode': mode});
+  Future setIsolatePauseMode(String mode) {
+    return invokeRpc('setIsolatePauseMode', {'exceptionPauseMode': mode});
   }
 
   Future<ServiceMap> getStack({int? limit}) {
diff --git a/runtime/observatory/tests/service/get_version_rpc_test.dart b/runtime/observatory/tests/service/get_version_rpc_test.dart
index 351000d..2d1ea37 100644
--- a/runtime/observatory/tests/service/get_version_rpc_test.dart
+++ b/runtime/observatory/tests/service/get_version_rpc_test.dart
@@ -11,8 +11,8 @@
   (VM vm) async {
     final result = await vm.invokeRpcNoUpgrade('getVersion', {});
     expect(result['type'], 'Version');
-    expect(result['major'], 3);
-    expect(result['minor'], 62);
+    expect(result['major'], 4);
+    expect(result['minor'], 0);
     expect(result['_privateMajor'], 0);
     expect(result['_privateMinor'], 0);
   },
diff --git a/runtime/observatory/tests/service/pause_on_exceptions_test.dart b/runtime/observatory/tests/service/pause_on_exceptions_test.dart
index 35bd59e..f73fdb8 100644
--- a/runtime/observatory/tests/service/pause_on_exceptions_test.dart
+++ b/runtime/observatory/tests/service/pause_on_exceptions_test.dart
@@ -52,8 +52,8 @@
         bool shouldBeCaught) async {
       print("Evaluating $expression with pause on $pauseMode exception");
 
-      expect((await isolate.setExceptionPauseMode(pauseMode)) is DartError,
-          isFalse);
+      expect(
+          (await isolate.setIsolatePauseMode(pauseMode)) is DartError, isFalse);
 
       var t;
       if (shouldPause) {
diff --git a/runtime/observatory_2/lib/src/elements/debugger.dart b/runtime/observatory_2/lib/src/elements/debugger.dart
index b63d349..9070138 100644
--- a/runtime/observatory_2/lib/src/elements/debugger.dart
+++ b/runtime/observatory_2/lib/src/elements/debugger.dart
@@ -632,7 +632,7 @@
   };
 
   static Future _setBreakOnException(debugger, name, value) async {
-    var result = await debugger.isolate.setExceptionPauseMode(value);
+    var result = await debugger.isolate.setIsolatePauseMode(value);
     if (result.isError) {
       debugger.console.print(result.toString());
     } else {
diff --git a/runtime/observatory_2/lib/src/service/object.dart b/runtime/observatory_2/lib/src/service/object.dart
index 63bf64f..9acde0e 100644
--- a/runtime/observatory_2/lib/src/service/object.dart
+++ b/runtime/observatory_2/lib/src/service/object.dart
@@ -1894,8 +1894,8 @@
     return invokeRpc('setName', {'name': newName});
   }
 
-  Future setExceptionPauseMode(String mode) {
-    return invokeRpc('setExceptionPauseMode', {'mode': mode});
+  Future setIsolatePauseMode(String mode) {
+    return invokeRpc('setIsolatePauseMode', {'exceptionPauseMode': mode});
   }
 
   Future<ServiceMap> getStack({int limit}) {
diff --git a/runtime/observatory_2/tests/service_2/get_version_rpc_test.dart b/runtime/observatory_2/tests/service_2/get_version_rpc_test.dart
index 6f53fb7..5fe3ff7 100644
--- a/runtime/observatory_2/tests/service_2/get_version_rpc_test.dart
+++ b/runtime/observatory_2/tests/service_2/get_version_rpc_test.dart
@@ -11,8 +11,8 @@
   (VM vm) async {
     final result = await vm.invokeRpcNoUpgrade('getVersion', {});
     expect(result['type'], equals('Version'));
-    expect(result['major'], equals(3));
-    expect(result['minor'], equals(62));
+    expect(result['major'], equals(4));
+    expect(result['minor'], equals(0));
     expect(result['_privateMajor'], equals(0));
     expect(result['_privateMinor'], equals(0));
   },
diff --git a/runtime/observatory_2/tests/service_2/pause_on_exceptions_test.dart b/runtime/observatory_2/tests/service_2/pause_on_exceptions_test.dart
index fa506c9..1b4da98 100644
--- a/runtime/observatory_2/tests/service_2/pause_on_exceptions_test.dart
+++ b/runtime/observatory_2/tests/service_2/pause_on_exceptions_test.dart
@@ -52,8 +52,8 @@
         bool shouldBeCaught) async {
       print("Evaluating $expression with pause on $pauseMode exception");
 
-      expect((await isolate.setExceptionPauseMode(pauseMode)) is DartError,
-          isFalse);
+      expect(
+          (await isolate.setIsolatePauseMode(pauseMode)) is DartError, isFalse);
 
       var t;
       if (shouldPause) {
diff --git a/runtime/vm/object_service.cc b/runtime/vm/object_service.cc
index 1222feb..2126bf0 100644
--- a/runtime/vm/object_service.cc
+++ b/runtime/vm/object_service.cc
@@ -1124,8 +1124,8 @@
           if (!field.is_static()) {
             field_value = GetField(field);
             JSONObject jsfield(&jsarr);
-            jsfield.AddProperty("type", "BoundField");
             jsfield.AddProperty("decl", field);
+            jsfield.AddProperty("name", field.UserVisibleNameCString());
             jsfield.AddProperty("value", field_value);
           }
         }
@@ -1219,7 +1219,7 @@
 void RecordType::PrintJSONImpl(JSONStream* stream, bool ref) const {
   JSONObject jsobj(stream);
   PrintSharedInstanceJSON(&jsobj, ref);
-  jsobj.AddProperty("kind", "_RecordType");
+  jsobj.AddProperty("kind", "RecordType");
   if (ref) {
     return;
   }
@@ -1629,7 +1629,7 @@
 void Record::PrintJSONImpl(JSONStream* stream, bool ref) const {
   JSONObject jsobj(stream);
   PrintSharedInstanceJSON(&jsobj, ref);
-  jsobj.AddProperty("kind", "_Record");
+  jsobj.AddProperty("kind", "Record");
   if (ref) {
     return;
   }
diff --git a/runtime/vm/profiler_service.cc b/runtime/vm/profiler_service.cc
index 52903dc..44105b1 100644
--- a/runtime/vm/profiler_service.cc
+++ b/runtime/vm/profiler_service.cc
@@ -1559,9 +1559,6 @@
   obj->AddProperty("maxStackDepth",
                    static_cast<intptr_t>(FLAG_max_profile_depth));
   obj->AddProperty("sampleCount", sample_count());
-  // TODO(bkonyi): remove timeSpan after next major revision.
-  ASSERT(SERVICE_PROTOCOL_MAJOR_VERSION == 3);
-  obj->AddProperty64("timeSpan", -1);
   obj->AddPropertyTimeMicros("timeOriginMicros", min_time());
   obj->AddPropertyTimeMicros("timeExtentMicros", GetTimeSpan());
   obj->AddProperty64("pid", pid);
diff --git a/runtime/vm/service.cc b/runtime/vm/service.cc
index c6353bb..b06b8c4 100644
--- a/runtime/vm/service.cc
+++ b/runtime/vm/service.cc
@@ -2319,6 +2319,7 @@
             (slot_offset.Value() - Array::element_offset(0)) /
             Array::kBytesPerElement;
         jselement.AddProperty("parentListIndex", element_index);
+        jselement.AddProperty("parentField", element_index);
       } else if (source.IsRecord()) {
         AddParentFieldToResponseBasedOnRecord(&field_names, &name, jselement,
                                               Record::Cast(source),
@@ -2349,6 +2350,7 @@
               (slot_offset.Value() - Context::variable_offset(0)) /
               Context::kBytesPerElement;
           jselement.AddProperty("parentListIndex", element_index);
+          jselement.AddProperty("parentField", element_index);
         } else {
           jselement.AddProperty("_parentWordOffset", slot_offset.Value());
         }
@@ -2443,6 +2445,7 @@
             (slot_offset.Value() - Array::element_offset(0)) /
             Array::kBytesPerElement;
         jselement.AddProperty("parentListIndex", element_index);
+        jselement.AddProperty("parentField", element_index);
       } else if (element.IsRecord()) {
         AddParentFieldToResponseBasedOnRecord(&field_names, &name, jselement,
                                               Record::Cast(element),
@@ -2489,6 +2492,7 @@
               (slot_offset.Value() - Context::variable_offset(0)) /
               Context::kBytesPerElement;
           jselement.AddProperty("parentListIndex", element_index);
+          jselement.AddProperty("parentField", element_index);
         } else {
           jselement.AddProperty("_parentWordOffset", slot_offset.Value());
         }
@@ -5413,27 +5417,6 @@
     NULL,
 };
 
-static void SetExceptionPauseMode(Thread* thread, JSONStream* js) {
-  const char* mode = js->LookupParam("mode");
-  if (mode == NULL) {
-    PrintMissingParamError(js, "mode");
-    return;
-  }
-  Dart_ExceptionPauseInfo info =
-      EnumMapper(mode, exception_pause_mode_names, exception_pause_mode_values);
-  if (info == kInvalidExceptionPauseInfo) {
-    PrintInvalidParamError(js, "mode");
-    return;
-  }
-  Isolate* isolate = thread->isolate();
-  isolate->debugger()->SetExceptionPauseInfo(info);
-  if (Service::debug_stream.enabled()) {
-    ServiceEvent event(isolate, ServiceEvent::kDebuggerSettingsUpdate);
-    Service::HandleEvent(&event);
-  }
-  PrintSuccess(js);
-}
-
 static const MethodParameter* const set_isolate_pause_mode_params[] = {
     ISOLATE_PARAMETER,
     new EnumParameter("exceptionPauseMode", false, exception_pause_mode_names),
@@ -5887,8 +5870,6 @@
     evaluate_compiled_expression_params },
   { "setBreakpointState", SetBreakpointState,
     set_breakpoint_state_params },
-  { "setExceptionPauseMode", SetExceptionPauseMode,
-    set_exception_pause_mode_params },
   { "setIsolatePauseMode", SetIsolatePauseMode,
     set_isolate_pause_mode_params },
   { "setFlag", SetFlag,
diff --git a/runtime/vm/service.h b/runtime/vm/service.h
index b596e14..d0aa669 100644
--- a/runtime/vm/service.h
+++ b/runtime/vm/service.h
@@ -16,8 +16,8 @@
 
 namespace dart {
 
-#define SERVICE_PROTOCOL_MAJOR_VERSION 3
-#define SERVICE_PROTOCOL_MINOR_VERSION 62
+#define SERVICE_PROTOCOL_MAJOR_VERSION 4
+#define SERVICE_PROTOCOL_MINOR_VERSION 0
 
 class Array;
 class EmbedderServiceHandler;
diff --git a/runtime/vm/service/service.md b/runtime/vm/service/service.md
index ded9453..034cccc 100644
--- a/runtime/vm/service/service.md
+++ b/runtime/vm/service/service.md
@@ -1,8 +1,8 @@
-# Dart VM Service Protocol 3.62
+# Dart VM Service Protocol 4.0
 
 > Please post feedback to the [observatory-discuss group][discuss-list]
 
-This document describes of _version 3.62_ of the Dart VM Service Protocol. This
+This document describes of _version 4.0_ 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.
@@ -71,7 +71,6 @@
   - [removeBreakpoint](#removebreakpoint)
   - [resume](#resume)
   - [setBreakpointState](#setbreakpointstate)
-  - [setExceptionPauseMode](#setexceptionpausemode)
   - [setFlag](#setflag)
   - [setLibraryDebuggable](#setlibrarydebuggable)
   - [setName](#setname)
@@ -121,7 +120,7 @@
   - [NativeFunction](#nativefunction)
   - [Null](#null)
   - [Object](#object)
-  - [Parameter](#parameter)[
+  - [Parameter](#parameter)
   - [PortList](#portlist)
   - [ReloadReport](#reloadreport)
   - [Response](#response)
@@ -144,7 +143,7 @@
   - [TimelineFlags](#timelineflags)
   - [Timestamp](#timestamp)
   - [TypeArguments](#typearguments)
-  - [TypeParameters](#typeparameters)[
+  - [TypeParameters](#typeparameters)
   - [UnresolvedSourceLocation](#unresolvedsourcelocation)
   - [UriList](#urilist)
   - [Version](#version)
@@ -1384,25 +1383,6 @@
 values.
 
 See [Breakpoint](#breakpoint).
-### setExceptionPauseMode
-
-```
-@deprecated('Use setIsolatePauseMode instead')
-Success|Sentinel setExceptionPauseMode(string isolateId,
-                                       ExceptionPauseMode mode)
-```
-
-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.
 
 ### setIsolatePauseMode
 
@@ -1417,9 +1397,6 @@
 
 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
@@ -1708,7 +1685,12 @@
 
 ```
 class BoundField {
+  // Provided for fields of instances that are NOT of the following instance kinds:
+  //   Record
+  //
+  // Note: this property is deprecated and will be replaced by `name`.
   @Field decl;
+  string|int name;
   @Instance|Sentinel value;
 }
 ```
@@ -1980,12 +1962,6 @@
   // The number of samples returned.
   int sampleCount;
 
-  // The timespan the set of returned samples covers, in microseconds (deprecated).
-  //
-  // Note: this property is deprecated and will always return -1. Use `timeExtentMicros`
-  // instead.
-  int timeSpan;
-
   // The start of the period of time in which the returned samples were
   // collected.
   int timeOriginMicros;
@@ -2022,12 +1998,6 @@
   // The number of samples returned.
   int sampleCount;
 
-  // The timespan the set of returned samples covers, in microseconds (deprecated).
-  //
-  // Note: this property is deprecated and will always return -1. Use `timeExtentMicros`
-  // instead.
-  int timeSpan;
-
   // The start of the period of time in which the returned samples were
   // collected.
   int timeOriginMicros;
@@ -3133,6 +3103,9 @@
   Float32x4List,
   Float64x2List,
 
+  // An instance of the Dart class Record.
+  Record,
+
   // An instance of the Dart class StackTrace.
   StackTrace,
 
@@ -3164,6 +3137,9 @@
   // An instance of the Dart class FunctionType.
   FunctionType,
 
+  // An instance of the Dart class RecordType.
+  RecordType,
+
   // An instance of the Dart class BoundedType.
   BoundedType,
 
@@ -3344,12 +3320,21 @@
   // The object holding the inbound reference.
   @Object source;
 
-  // If source is a List, parentListIndex is the index of the inbound reference.
+  // If source is a List, parentListIndex is the index of the inbound reference (deprecated).
+  //
+  // Note: this property is deprecated and will be replaced by `parentField`.
   int parentListIndex [optional];
 
-  // If source is a field of an object, parentField is the field containing the
-  // inbound reference.
-  @Field parentField [optional];
+  // If `source` is a `List`, `parentField` is the index of the inbound
+  // reference.
+  // If `source` is a record, `parentField` is the field name of the inbound
+  // reference.
+  // If `source` is an instance of any other kind, `parentField` is the field
+  // containing the inbound reference.
+  //
+  // Note: In v5.0 of the spec, `@Field` will no longer be a part of this
+  // property's type, i.e. the type will become `string|int`.
+  @Field|string|int parentField [optional];
 }
 ```
 
@@ -3761,7 +3746,9 @@
   @Object value;
 
   // If `value` is a List, `parentListIndex` is the index where the previous
-  // object on the retaining path is located.
+  // object on the retaining path is located (deprecated).
+  //
+  // Note: this property is deprecated and will be replaced by `parentField`.
   int parentListIndex [optional];
 
   // If `value` is a Map, `parentMapKey` is the key mapping to the previous
@@ -3770,7 +3757,7 @@
 
   // If `value` is a non-List, non-Map object, `parentField` is the name of the
   // field containing the previous object on the retaining path.
-  string parentField [optional];
+  string|int parentField [optional];
 }
 ```
 
@@ -4415,5 +4402,6 @@
 3.60 | Added `gcType` property to `Event`.
 3.61 | Added `isolateGroupId` property to `@Isolate` and `Isolate`.
 3.62 | Added `Set` to `InstanceKind`.
+4.0 | Added `Record` and `RecordType` `InstanceKind`s, added a deprecation notice to the `decl` property of `BoundField`, added `name` property to `BoundField`, added a deprecation notice to the `parentListIndex` property of `InboundReference`, changed the type of the `parentField` property of `InboundReference` from `@Field` to `@Field\|string\|int`, added a deprecation notice to the `parentListIndex` property of `RetainingObject`, changed the type of the `parentField` property of `RetainingObject` from `string` to `string\|int`, removed the deprecated `setExceptionPauseMode` procedure, removed the deprecated `timeSpan` property from `CpuSamples`, and removed the deprecated `timeSpan` property from `CpuSamplesEvent`.
 
 [discuss-list]: https://groups.google.com/a/dartlang.org/forum/#!forum/observatory-discuss