Version 2.17.0-30.0.dev

Merge commit 'b449fe4398f5188a54694bfcea8f2dc8f645e45e' into 'dev'
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/data_driven.dart b/pkg/analysis_server/lib/src/services/correction/dart/data_driven.dart
index 029d157..f7c9e13 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/data_driven.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/data_driven.dart
@@ -33,15 +33,17 @@
         importedUris.add(Uri.parse(uri));
       }
     }
-    var matcher = ElementMatcher.forNode(node);
-    if (matcher == null) {
-      // The node doesn't represent an element that can be transformed.
+    var matchers = ElementMatcher.matchersForNode(node);
+    if (matchers.isEmpty) {
+      // The node doesn't represent any element that can be transformed.
       return;
     }
     for (var set in _availableTransformSetsForLibrary(library)) {
-      for (var transform
-          in set.transformsFor(matcher, applyingBulkFixes: applyingBulkFixes)) {
-        yield DataDrivenFix(transform);
+      for (var matcher in matchers) {
+        for (var transform in set.transformsFor(matcher,
+            applyingBulkFixes: applyingBulkFixes)) {
+          yield DataDrivenFix(transform);
+        }
       }
     }
   }
diff --git a/pkg/analysis_server/lib/src/services/correction/fix/data_driven/element_matcher.dart b/pkg/analysis_server/lib/src/services/correction/fix/data_driven/element_matcher.dart
index 837025a..252ccf6 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix/data_driven/element_matcher.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix/data_driven/element_matcher.dart
@@ -26,13 +26,14 @@
   final List<ElementKind> validKinds;
 
   /// Initialize a newly created matcher representing a reference to an element
-  /// with the given [name] in a library that imports the [importedUris].
+  /// whose name matches the given [components] and element [kinds] in a library
+  /// that imports the [importedUris].
   ElementMatcher(
       {required this.importedUris,
       required this.components,
-      List<ElementKind>? kinds})
+      required List<ElementKind> kinds})
       : assert(components.isNotEmpty),
-        validKinds = kinds ?? const [];
+        validKinds = kinds;
 
   /// Return `true` if this matcher matches the given [element].
   bool matches(ElementDescriptor element) {
@@ -93,25 +94,68 @@
     return false;
   }
 
-  /// Return an element matcher that will match the element that is, or should
-  /// be, associated with the given [node], or `null` if there is no appropriate
-  /// matcher for the node.
-  static ElementMatcher? forNode(AstNode? node) {
+  /// Return a list of element matchers that will match the element that is, or
+  /// should be, associated with the given [node]. The list will be empty if
+  /// there are no appropriate matchers for the [node].
+  static List<ElementMatcher> matchersForNode(AstNode? node) {
     if (node == null) {
-      return null;
+      return const <ElementMatcher>[];
     }
     var importedUris = _importElementsForNode(node);
     if (importedUris == null) {
+      return const <ElementMatcher>[];
+    }
+    var builder = _MatcherBuilder(importedUris);
+    builder.buildMatchersForNode(node);
+    return builder.matchers.toList();
+  }
+
+  /// Return the URIs of the imports in the library containing the [node], or
+  /// `null` if the imports can't be determined.
+  static List<Uri>? _importElementsForNode(AstNode node) {
+    var root = node.root;
+    if (root is! CompilationUnit) {
       return null;
     }
+    var importedUris = <Uri>[];
+    var library = root.declaredElement?.library;
+    if (library == null) {
+      return null;
+    }
+    for (var importElement in library.imports) {
+      // TODO(brianwilkerson) Filter based on combinators to help avoid making
+      //  invalid suggestions.
+      var uri = importElement.importedLibrary?.source.uri;
+      if (uri != null) {
+        // The [uri] is `null` if the literal string is not a valid URI.
+        importedUris.add(uri);
+      }
+    }
+    return importedUris;
+  }
+}
+
+/// A helper class used to build a list of element matchers.
+class _MatcherBuilder {
+  final List<ElementMatcher> matchers = [];
+
+  final List<Uri> importedUris;
+
+  _MatcherBuilder(this.importedUris);
+
+  void buildMatchersForNode(AstNode? node) {
     var components = _componentsForNode(node);
     if (components == null) {
-      return null;
+      return;
     }
-    return ElementMatcher(
-        importedUris: importedUris,
-        components: components,
-        kinds: _kindsForNode(node));
+    var kinds = _kindsForNode(node) ?? [];
+    _addMatcher(components: components, kinds: kinds);
+  }
+
+  void _addMatcher(
+      {required List<String> components, required List<ElementKind> kinds}) {
+    matchers.add(ElementMatcher(
+        importedUris: importedUris, components: components, kinds: kinds));
   }
 
   /// Return the components of the path of the element associated with the given
@@ -244,30 +288,6 @@
     return null;
   }
 
-  /// Return the URIs of the imports in the library containing the [node], or
-  /// `null` if the imports can't be determined.
-  static List<Uri>? _importElementsForNode(AstNode node) {
-    var root = node.root;
-    if (root is! CompilationUnit) {
-      return null;
-    }
-    var importedUris = <Uri>[];
-    var library = root.declaredElement?.library;
-    if (library == null) {
-      return null;
-    }
-    for (var importElement in library.imports) {
-      // TODO(brianwilkerson) Filter based on combinators to help avoid making
-      //  invalid suggestions.
-      var uri = importElement.importedLibrary?.source.uri;
-      if (uri != null) {
-        // The [uri] is `null` if the literal string is not a valid URI.
-        importedUris.add(uri);
-      }
-    }
-    return importedUris;
-  }
-
   /// Return `true` if the [node] is a prefix
   static bool _isPrefix(AstNode? node) {
     return node is SimpleIdentifier && node.staticElement is PrefixElement;
@@ -312,6 +332,7 @@
       }
       return const [
         ElementKind.classKind,
+//        ElementKind.constructorKind,
         ElementKind.enumKind,
         ElementKind.mixinKind,
         ElementKind.typedefKind
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/element_matcher_test.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/element_matcher_test.dart
index 16ec4ae..716cd55 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/element_matcher_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/element_matcher_test.dart
@@ -23,7 +23,9 @@
       List<ElementKind>? expectedKinds,
       List<String>? expectedUris}) {
     var node = findNode.any(search);
-    var matcher = ElementMatcher.forNode(node)!;
+    var matchers = ElementMatcher.matchersForNode(node);
+    expect(matchers, hasLength(1));
+    var matcher = matchers[0];
     if (expectedUris != null) {
       expect(matcher.importedUris,
           unorderedEquals(expectedUris.map((uri) => Uri.parse(uri))));
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_use_case_test.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_use_case_test.dart
index e8d6d1b..ce50e6b 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_use_case_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/flutter_use_case_test.dart
@@ -706,6 +706,117 @@
 ''');
   }
 
+  @failingTest
+  Future<void> test_material_FlatButton_deprecated() async {
+    setPackageContent('''
+@deprecated
+class FlatButton {
+  factory FlatButton.icon({
+    Key? key,
+    required VoidCallback? onPressed,
+    VoidCallback? onLongPress,
+    ValueChanged<bool>? onHighlightChanged,
+    ButtonTextTheme? textTheme,
+    Color? highlightColor,
+    Brightness? colorBrightness,
+    Clip? clipBehavior,
+    FocusNode? focusNode,
+    bool autofocus = true,
+    required Widget icon,
+    required Widget label,
+  }) => FlatButton();
+  FlatButton();
+}
+class Key {}
+class UniqueKey extends Key {}
+typedef VoidCallback = void Function();
+typedef ValueChanged<T> = void Function(T value);
+class ButtonTextTheme {}
+class Color {}
+class Colors {
+  static Color blue = Color();
+}
+class Brightness {
+  static Brightness dark = Brightness();
+}
+class Clip {
+  static Clip hardEdge = Clip();
+}
+class FocusNode {}
+class Widget {}
+class Icon extends Widget {
+  Icon(String icon);
+}
+class Text extends Widget {
+  Text(String content);
+}
+class Icons {
+  static String ten_k_outlined = '';
+}
+''');
+    addPackageDataFile('''
+version: 1
+transforms:
+  # Changes made in https://github.com/flutter/flutter/pull/73352
+  - title: "Migrate to 'TextButton.icon'"
+    date: 2021-01-08
+    element:
+      uris: [ '$importUri' ]
+      constructor: 'icon'
+      inClass: 'FlatButton'
+    changes:
+      - kind: 'removeParameter'
+        name: 'onHighlightChanged'
+      - kind: 'removeParameter'
+        name: 'textTheme'
+      - kind: 'removeParameter'
+        name: 'highlightColor'
+      - kind: 'removeParameter'
+        name: 'colorBrightness'
+      - kind: 'replacedBy'
+        newElement:
+          uris: [ '$importUri' ]
+          constructor: 'icon'
+          inClass: 'TextButton'
+''');
+    await resolveTestCode('''
+import '$importUri';
+
+void f() {
+  FlatButton.icon(
+    key: UniqueKey(),
+    icon: Icon(Icons.ten_k_outlined),
+    label: Text('FlatButton'),
+    onPressed: (){},
+    onLongPress: (){},
+    clipBehavior: Clip.hardEdge,
+    focusNode: FocusNode(),
+    autofocus: true,
+    onHighlightChanged: (_) {},
+    textTheme: ButtonTextTheme(),
+    highlightColor: Colors.blue,
+    colorBrightness: Brightness.dark,
+  );
+}
+''');
+    await assertHasFix('''
+import '$importUri';
+
+void f() {
+  TextButton.icon(
+    key: UniqueKey(),
+    icon: Icon(Icons.ten_k_outlined),
+    label: Text('FlatButton'),
+    onPressed: (){},
+    onLongPress: (){},
+    clipBehavior: Clip.hardEdge,
+    focusNode: FocusNode(),
+    autofocus: true,
+  );
+}
+''');
+  }
+
   Future<void>
       test_material_InputDecoration_defaultConstructor_matchFirstCase_deprecated() async {
     setPackageContent('''
diff --git a/pkg/analysis_server/test/src/services/correction/fix/data_driven/transform_set_parser_test.dart b/pkg/analysis_server/test/src/services/correction/fix/data_driven/transform_set_parser_test.dart
index 95055f4..c3d8722 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/data_driven/transform_set_parser_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/data_driven/transform_set_parser_test.dart
@@ -784,7 +784,7 @@
       (transform.changesSelector as UnconditionalChangesSelector).changes;
 
   ElementMatcher _matcher(String name) =>
-      ElementMatcher(importedUris: uris, components: [name]);
+      ElementMatcher(importedUris: uris, components: [name], kinds: const []);
 
   List<Transform> _transforms(String name) =>
       result!.transformsFor(_matcher(name), applyingBulkFixes: false);
diff --git a/pkg/analyzer/lib/src/dart/element/inheritance_manager3.dart b/pkg/analyzer/lib/src/dart/element/inheritance_manager3.dart
index 168cc9a..6195260 100644
--- a/pkg/analyzer/lib/src/dart/element/inheritance_manager3.dart
+++ b/pkg/analyzer/lib/src/dart/element/inheritance_manager3.dart
@@ -863,6 +863,7 @@
       result.parameters = resultType.parameters;
 
       var field = FieldElementImpl(variableName, -1);
+      field.enclosingElement = targetClass;
       if (firstAccessor.isGetter) {
         field.getter = result;
         field.type = result.returnType;
diff --git a/pkg/analyzer/lib/src/error/codes.g.dart b/pkg/analyzer/lib/src/error/codes.g.dart
index 0bbf444..d3dc572 100644
--- a/pkg/analyzer/lib/src/error/codes.g.dart
+++ b/pkg/analyzer/lib/src/error/codes.g.dart
@@ -3483,9 +3483,10 @@
    */
   // #### Description
   //
-  // The analyzer produces this diagnostic when there's more than one field
-  // formal parameter for the same field in a constructor's parameter list. It
-  // isn't useful to assign a value that will immediately be overwritten.
+  // The analyzer produces this diagnostic when there's more than one
+  // initializing formal parameter for the same field in a constructor's
+  // parameter list. It isn't useful to assign a value that will immediately be
+  // overwritten.
   //
   // #### Example
   //
@@ -3502,7 +3503,7 @@
   //
   // #### Common fixes
   //
-  // Remove one of the field formal parameters:
+  // Remove one of the initializing formal parameters:
   //
   // ```dart
   // class C {
@@ -4751,7 +4752,8 @@
   // #### Example
   //
   // The following code produces this diagnostic because the field `f` is
-  // initialized both by a field formal parameter and in the initializer list:
+  // initialized both by an initializing formal parameter and in the
+  // initializer list:
   //
   // ```dart
   // class C {
@@ -4809,14 +4811,14 @@
    */
   // #### Description
   //
-  // The analyzer produces this diagnostic when a factory constructor has a
-  // field formal parameter. Factory constructors can't assign values to fields
-  // because no instance is created; hence, there is no field to assign.
+  // The analyzer produces this diagnostic when a factory constructor has an
+  // initializing formal parameter. Factory constructors can't assign values to
+  // fields because no instance is created; hence, there is no field to assign.
   //
   // #### Example
   //
   // The following code produces this diagnostic because the factory constructor
-  // uses a field formal parameter:
+  // uses an initializing formal parameter:
   //
   // ```dart
   // class C {
@@ -4828,7 +4830,7 @@
   //
   // #### Common fixes
   //
-  // Replace the field formal parameter with a normal parameter:
+  // Replace the initializing formal parameter with a normal parameter:
   //
   // ```dart
   // class C {
@@ -4924,8 +4926,8 @@
   // #### Examples
   //
   // The following code produces this diagnostic because the constructor
-  // `C.zero`, which redirects to the constructor `C`, has a field formal
-  // parameter that initializes the field `f`:
+  // `C.zero`, which redirects to the constructor `C`, has an initializing
+  // formal parameter that initializes the field `f`:
   //
   // ```dart
   // class C {
@@ -4953,8 +4955,8 @@
   //
   // #### Common fixes
   //
-  // If the initialization is done by a field formal parameter, then use a
-  // normal parameter:
+  // If the initialization is done by an initializing formal parameter, then
+  // use a normal parameter:
   //
   // ```dart
   // class C {
@@ -4994,14 +4996,16 @@
    */
   // #### Description
   //
-  // The analyzer produces this diagnostic when the type of a field formal
-  // parameter isn't assignable to the type of the field being initialized.
+  // The analyzer produces this diagnostic when the type of an initializing
+  // formal parameter isn't assignable to the type of the field being
+  // initialized.
   //
   // #### Example
   //
-  // The following code produces this diagnostic because the field formal
-  // parameter has the type `String`, but the type of the field is `int`. The
-  // parameter must have a type that is a subtype of the field's type.
+  // The following code produces this diagnostic because the initializing
+  // formal parameter has the type `String`, but the type of the field is
+  // `int`. The parameter must have a type that is a subtype of the field's
+  // type.
   //
   // ```dart
   // class C {
@@ -5037,8 +5041,8 @@
   // ```
   //
   // If the types of both the field and the parameter are correct, then use an
-  // initializer rather than a field formal parameter to convert the parameter
-  // value into a value of the correct type:
+  // initializer rather than an initializing formal parameter to convert the
+  // parameter value into a value of the correct type:
   //
   // ```dart
   // class C {
@@ -5139,7 +5143,7 @@
   //
   // For instance fields, you can add an initializer as shown in the previous
   // example, or you can initialize the field in every constructor. You can
-  // initialize the field by using a field formal parameter:
+  // initialize the field by using an initializing formal parameter:
   //
   // ```dart
   // class C {
@@ -5191,8 +5195,8 @@
   //
   // #### Common fixes
   //
-  // If the value should be passed in to the constructor directly, then use a
-  // field formal parameter to initialize the field `value`:
+  // If the value should be passed in to the constructor directly, then use an
+  // initializing formal parameter to initialize the field `value`:
   //
   // ```dart
   // class C {
@@ -6199,14 +6203,14 @@
    */
   // #### Description
   //
-  // The analyzer produces this diagnostic when a static field is initialized in
-  // a constructor using either a field formal parameter or an assignment in the
-  // initializer list.
+  // The analyzer produces this diagnostic when a static field is initialized
+  // in a constructor using either an initializing formal parameter or an
+  // assignment in the initializer list.
   //
   // #### Example
   //
-  // The following code produces this diagnostic because the static field `a` is
-  // being initialized by the field formal parameter `this.a`:
+  // The following code produces this diagnostic because the static field `a`
+  // is being initialized by the initializing formal parameter `this.a`:
   //
   // ```dart
   // class C {
@@ -6264,10 +6268,10 @@
    */
   // #### Description
   //
-  // The analyzer produces this diagnostic when a field formal parameter is
-  // found in a constructor in a class that doesn't declare the field being
-  // initialized. Constructors can't initialize fields that aren't declared and
-  // fields that are inherited from superclasses.
+  // The analyzer produces this diagnostic when an initializing formal
+  // parameter is found in a constructor in a class that doesn't declare the
+  // field being initialized. Constructors can't initialize fields that aren't
+  // declared and fields that are inherited from superclasses.
   //
   // #### Example
   //
@@ -7016,7 +7020,7 @@
   static const CompileTimeErrorCode INVALID_CAST_LITERAL_MAP =
       CompileTimeErrorCode(
     'INVALID_CAST_LITERAL_MAP',
-    "The map literal type '{0}' isn't of expected type '{1}'. The maps's type "
+    "The map literal type '{0}' isn't of expected type '{1}'. The map's type "
         "can be changed with an explicit generic type arguments or by changing "
         "the key and value types.",
   );
diff --git a/pkg/analyzer/lib/src/summary2/element_builder.dart b/pkg/analyzer/lib/src/summary2/element_builder.dart
index 3c46109..d2ef4a6 100644
--- a/pkg/analyzer/lib/src/summary2/element_builder.dart
+++ b/pkg/analyzer/lib/src/summary2/element_builder.dart
@@ -214,32 +214,17 @@
     var reference = _enclosingContext.addEnum(name, element);
     _libraryBuilder.localScope.declare(name, reference);
 
-    var holder = _EnclosingContext(reference, element);
-    _withEnclosing(holder, () {
-      var typeParameters = node.typeParameters;
-      if (typeParameters != null) {
-        typeParameters.accept(this);
-        element.typeParameters = holder.typeParameters;
-      }
-    });
-
-    var accessors = <PropertyAccessorElement>[];
-    var fields = <FieldElementImpl>[];
-    var methods = <MethodElement>[];
+    var holder = _EnclosingContext(
+      reference,
+      element,
+      hasConstConstructor: true,
+    );
 
     // Build the 'index' field.
-    ConstFieldElementImpl indexField;
-    {
-      indexField = ConstFieldElementImpl('index', -1)
-        ..isSynthetic = true
-        ..isFinal = true;
-      indexField.bindReference(
-        reference.getChild('@field').getChild('index'),
-      );
-      fields.add(indexField);
-      accessors.add(PropertyAccessorElementImpl_ImplicitGetter(indexField,
-          reference: reference.getChild('@getter').getChild('index')));
-    }
+    var indexField = ConstFieldElementImpl('index', -1)
+      ..isSynthetic = true
+      ..isFinal = true;
+    holder.addNonSyntheticField(indexField);
 
     var constructorIndexParameter = FieldFormalParameterElementImpl(
       name: 'index',
@@ -272,13 +257,11 @@
     element.constructors = [constructor];
 
     // Build fields for all enum constants.
-    var containerRef = reference.getChild('@field');
     var constants = node.constants;
     var valuesElements = <Expression>[];
     for (var i = 0; i < constants.length; ++i) {
       var constant = constants[i];
       var name = constant.name.name;
-      var reference = containerRef.getChild(name);
       var field = ConstFieldElementImpl(name, constant.name.offset)
         ..hasImplicitType = true
         ..isConst = true
@@ -287,7 +270,6 @@
         ..type = DynamicTypeImpl.instance;
       _setCodeRange(field, constant);
       _setDocumentation(field, constant);
-      field.reference = reference;
       field.metadata = _buildAnnotationsWithUnit(
         _unitElement,
         constant.metadata,
@@ -334,9 +316,7 @@
       _linker.elementNodes[field] = variableDeclaration;
 
       field.constantInitializer = initializer;
-      field.createImplicitAccessors(containerRef.parent!, name);
-      fields.add(field);
-      accessors.add(field.getter as PropertyAccessorElementImpl);
+      holder.addNonSyntheticField(field);
       valuesElements.add(
         astFactory.simpleIdentifier(
           StringToken(TokenType.STRING, name, -1),
@@ -391,9 +371,7 @@
       );
       _linker.elementNodes[valuesField] = variableDeclaration;
 
-      fields.add(valuesField);
-      accessors.add(PropertyAccessorElementImpl_ImplicitGetter(valuesField,
-          reference: reference.getChild('@getter').getChild('values')));
+      holder.addNonSyntheticField(valuesField);
     }
 
     // TODO(scheglov) implement
@@ -401,23 +379,16 @@
     // node.withClause?.accept(this);
     // node.implementsClause?.accept(this);
 
-    // TODO(scheglov) don't create a duplicate
-    {
-      var holder2 = _buildClassMembers(element, node.members);
-      fields.addAll(
-        holder2.properties.whereType<FieldElementImpl>(),
-      );
-      accessors.addAll(holder2.propertyAccessors);
-      methods.addAll(holder2.methods);
-    }
+    _withEnclosing(holder, () {
+      node.typeParameters?.accept(this);
+      _visitPropertyFirst<FieldDeclaration>(node.members);
+    });
 
-    // TODO(scheglov) only if no explicit
-    MethodElementImpl toStringMethod;
-    {
-      toStringMethod = MethodElementImpl('toString', -1)..isSynthetic = true;
-      methods.add(toStringMethod);
-      toStringMethod.reference =
-          reference.getChild('@method').getChild('toString');
+    MethodElementImpl? syntheticToStringMethod;
+    if (holder.getMethod('toString').element == null) {
+      syntheticToStringMethod = MethodElementImpl('toString', -1)
+        ..isSynthetic = true;
+      holder.addMethod(name, syntheticToStringMethod);
     }
 
     _libraryBuilder.implicitEnumNodes.add(
@@ -428,13 +399,14 @@
         valuesField: valuesField,
         constructorIndexParameter: constructorIndexParameter,
         constructorNameParameter: constructorNameParameter,
-        syntheticToStringMethod: toStringMethod,
+        syntheticToStringMethod: syntheticToStringMethod,
       ),
     );
 
-    element.accessors = accessors;
-    element.fields = fields;
-    element.methods = methods;
+    element.typeParameters = holder.typeParameters;
+    element.accessors = holder.propertyAccessors;
+    element.fields = holder.properties.whereType<FieldElementImpl>().toList();
+    element.methods = holder.methods;
 
     // TODO(scheglov) resolve field formals
   }
@@ -486,8 +458,12 @@
       }
     });
 
+    // TODO(scheglov) don't create a duplicate
     {
-      var holder = _buildClassMembers(element, node.members);
+      var holder = _EnclosingContext(reference, element);
+      _withEnclosing(holder, () {
+        _visitPropertyFirst<FieldDeclaration>(node.members);
+      });
       element.accessors = holder.propertyAccessors;
       element.fields = holder.properties.whereType<FieldElement>().toList();
       element.methods = holder.methods;
@@ -500,8 +476,6 @@
   void visitFieldDeclaration(
     covariant FieldDeclarationImpl node,
   ) {
-    var enclosingRef = _enclosingContext.reference;
-
     var metadata = _buildAnnotations(node.metadata);
     for (var variable in node.fields.variables) {
       var nameNode = variable.name as SimpleIdentifierImpl;
@@ -533,21 +507,10 @@
         element.type = DynamicTypeImpl.instance;
       }
 
-      element.createImplicitAccessors(enclosingRef, name);
+      _enclosingContext.addNonSyntheticField(element);
 
       _linker.elementNodes[element] = variable;
-      _enclosingContext.addField(name, element);
       nameNode.staticElement = element;
-
-      var getter = element.getter;
-      if (getter is PropertyAccessorElementImpl) {
-        _enclosingContext.addGetter(name, getter);
-      }
-
-      var setter = element.setter;
-      if (setter is PropertyAccessorElementImpl) {
-        _enclosingContext.addSetter(name, setter);
-      }
     }
     _buildType(node.fields.type);
   }
@@ -1165,23 +1128,17 @@
     return _buildAnnotationsWithUnit(_unitElement, nodeList);
   }
 
-  _EnclosingContext _buildClassMembers(
-      ElementImpl element, List<ClassMember> members) {
-    var hasConstConstructor = members.any((e) {
+  void _buildClassOrMixin(ClassOrMixinDeclaration node) {
+    var element = node.declaredElement as ClassElementImpl;
+    var hasConstConstructor = node.members.any((e) {
       return e is ConstructorDeclaration && e.constKeyword != null;
     });
+    // TODO(scheglov) don't create a duplicate
     var holder = _EnclosingContext(element.reference!, element,
         hasConstConstructor: hasConstConstructor);
     _withEnclosing(holder, () {
-      _visitPropertyFirst<FieldDeclaration>(members);
+      _visitPropertyFirst<FieldDeclaration>(node.members);
     });
-    return holder;
-  }
-
-  void _buildClassOrMixin(ClassOrMixinDeclaration node) {
-    var element = node.declaredElement as ClassElementImpl;
-    // TODO(scheglov) don't create a duplicate
-    var holder = _buildClassMembers(element, node.members);
     element.accessors = holder.propertyAccessors;
     element.fields = holder.properties.whereType<FieldElement>().toList();
     element.methods = holder.methods;
@@ -1483,6 +1440,23 @@
     return _bindReference('@mixin', name, element);
   }
 
+  void addNonSyntheticField(FieldElementImpl element) {
+    var name = element.name;
+    element.createImplicitAccessors(reference, name);
+
+    addField(name, element);
+
+    var getter = element.getter;
+    if (getter is PropertyAccessorElementImpl) {
+      addGetter(name, getter);
+    }
+
+    var setter = element.setter;
+    if (setter is PropertyAccessorElementImpl) {
+      addSetter(name, setter);
+    }
+  }
+
   Reference? addParameter(String? name, ParameterElementImpl element) {
     parameters.add(element);
     if (name == null) {
@@ -1513,6 +1487,10 @@
     this.element.encloseElement(element);
   }
 
+  Reference getMethod(String name) {
+    return reference.getChild('@method').getChild(name);
+  }
+
   Reference _bindReference(
     String containerName,
     String name,
diff --git a/pkg/analyzer/lib/src/summary2/library_builder.dart b/pkg/analyzer/lib/src/summary2/library_builder.dart
index c26e6f7..e8b40c2 100644
--- a/pkg/analyzer/lib/src/summary2/library_builder.dart
+++ b/pkg/analyzer/lib/src/summary2/library_builder.dart
@@ -27,7 +27,7 @@
   final ConstFieldElementImpl valuesField;
   final ParameterElementImpl constructorIndexParameter;
   final ParameterElementImpl constructorNameParameter;
-  final MethodElementImpl syntheticToStringMethod;
+  final MethodElementImpl? syntheticToStringMethod;
 
   ImplicitEnumNodes({
     required this.element,
@@ -149,7 +149,7 @@
       enum_.valuesField.type = valuesType;
       enum_.constructorIndexParameter.type = typeProvider.intType;
       enum_.constructorNameParameter.type = typeProvider.stringType;
-      enum_.syntheticToStringMethod.returnType = typeProvider.stringType;
+      enum_.syntheticToStringMethod?.returnType = typeProvider.stringType;
     }
   }
 
diff --git a/pkg/analyzer/lib/src/test_utilities/find_element.dart b/pkg/analyzer/lib/src/test_utilities/find_element.dart
index 8535a39..25dfccc 100644
--- a/pkg/analyzer/lib/src/test_utilities/find_element.dart
+++ b/pkg/analyzer/lib/src/test_utilities/find_element.dart
@@ -502,13 +502,6 @@
       }
     }
 
-    for (var extension_ in unitElement.extensions) {
-      if (of != null && extension_.name != of) {
-        continue;
-      }
-      findIn(extension_.methods);
-    }
-
     for (var class_ in unitElement.classes) {
       if (of != null && class_.name != of) {
         continue;
@@ -516,6 +509,20 @@
       findIn(class_.methods);
     }
 
+    for (var enum_ in unitElement.enums) {
+      if (of != null && enum_.name != of) {
+        continue;
+      }
+      findIn(enum_.methods);
+    }
+
+    for (var extension_ in unitElement.extensions) {
+      if (of != null && extension_.name != of) {
+        continue;
+      }
+      findIn(extension_.methods);
+    }
+
     for (var mixin in unitElement.mixins) {
       if (of != null && mixin.name != of) {
         continue;
diff --git a/pkg/analyzer/messages.yaml b/pkg/analyzer/messages.yaml
index 4a3af55..8507f19 100644
--- a/pkg/analyzer/messages.yaml
+++ b/pkg/analyzer/messages.yaml
@@ -3203,9 +3203,10 @@
     documentation: |-
       #### Description
 
-      The analyzer produces this diagnostic when there's more than one field
-      formal parameter for the same field in a constructor's parameter list. It
-      isn't useful to assign a value that will immediately be overwritten.
+      The analyzer produces this diagnostic when there's more than one
+      initializing formal parameter for the same field in a constructor's
+      parameter list. It isn't useful to assign a value that will immediately be
+      overwritten.
 
       #### Example
 
@@ -3222,7 +3223,7 @@
 
       #### Common fixes
 
-      Remove one of the field formal parameters:
+      Remove one of the initializing formal parameters:
 
       ```dart
       class C {
@@ -4169,7 +4170,8 @@
       #### Example
 
       The following code produces this diagnostic because the field `f` is
-      initialized both by a field formal parameter and in the initializer list:
+      initialized both by an initializing formal parameter and in the
+      initializer list:
 
       ```dart
       class C {
@@ -4221,14 +4223,14 @@
     documentation: |-
       #### Description
 
-      The analyzer produces this diagnostic when a factory constructor has a
-      field formal parameter. Factory constructors can't assign values to fields
-      because no instance is created; hence, there is no field to assign.
+      The analyzer produces this diagnostic when a factory constructor has an
+      initializing formal parameter. Factory constructors can't assign values to
+      fields because no instance is created; hence, there is no field to assign.
 
       #### Example
 
       The following code produces this diagnostic because the factory constructor
-      uses a field formal parameter:
+      uses an initializing formal parameter:
 
       ```dart
       class C {
@@ -4240,7 +4242,7 @@
 
       #### Common fixes
 
-      Replace the field formal parameter with a normal parameter:
+      Replace the initializing formal parameter with a normal parameter:
 
       ```dart
       class C {
@@ -4331,8 +4333,8 @@
       #### Examples
 
       The following code produces this diagnostic because the constructor
-      `C.zero`, which redirects to the constructor `C`, has a field formal
-      parameter that initializes the field `f`:
+      `C.zero`, which redirects to the constructor `C`, has an initializing
+      formal parameter that initializes the field `f`:
 
       ```dart
       class C {
@@ -4360,8 +4362,8 @@
 
       #### Common fixes
 
-      If the initialization is done by a field formal parameter, then use a
-      normal parameter:
+      If the initialization is done by an initializing formal parameter, then
+      use a normal parameter:
 
       ```dart
       class C {
@@ -4396,14 +4398,16 @@
     documentation: |-
       #### Description
 
-      The analyzer produces this diagnostic when the type of a field formal
-      parameter isn't assignable to the type of the field being initialized.
+      The analyzer produces this diagnostic when the type of an initializing
+      formal parameter isn't assignable to the type of the field being
+      initialized.
 
       #### Example
 
-      The following code produces this diagnostic because the field formal
-      parameter has the type `String`, but the type of the field is `int`. The
-      parameter must have a type that is a subtype of the field's type.
+      The following code produces this diagnostic because the initializing
+      formal parameter has the type `String`, but the type of the field is
+      `int`. The parameter must have a type that is a subtype of the field's
+      type.
 
       ```dart
       class C {
@@ -4439,8 +4443,8 @@
       ```
 
       If the types of both the field and the parameter are correct, then use an
-      initializer rather than a field formal parameter to convert the parameter
-      value into a value of the correct type:
+      initializer rather than an initializing formal parameter to convert the
+      parameter value into a value of the correct type:
 
       ```dart
       class C {
@@ -4530,7 +4534,7 @@
 
       For instance fields, you can add an initializer as shown in the previous
       example, or you can initialize the field in every constructor. You can
-      initialize the field by using a field formal parameter:
+      initialize the field by using an initializing formal parameter:
 
       ```dart
       class C {
@@ -4579,8 +4583,8 @@
 
       #### Common fixes
 
-      If the value should be passed in to the constructor directly, then use a
-      field formal parameter to initialize the field `value`:
+      If the value should be passed in to the constructor directly, then use an
+      initializing formal parameter to initialize the field `value`:
 
       ```dart
       class C {
@@ -5380,14 +5384,14 @@
     documentation: |-
       #### Description
 
-      The analyzer produces this diagnostic when a static field is initialized in
-      a constructor using either a field formal parameter or an assignment in the
-      initializer list.
+      The analyzer produces this diagnostic when a static field is initialized
+      in a constructor using either an initializing formal parameter or an
+      assignment in the initializer list.
 
       #### Example
 
-      The following code produces this diagnostic because the static field `a` is
-      being initialized by the field formal parameter `this.a`:
+      The following code produces this diagnostic because the static field `a`
+      is being initialized by the initializing formal parameter `this.a`:
 
       ```dart
       class C {
@@ -5440,10 +5444,10 @@
     documentation: |-
       #### Description
 
-      The analyzer produces this diagnostic when a field formal parameter is
-      found in a constructor in a class that doesn't declare the field being
-      initialized. Constructors can't initialize fields that aren't declared and
-      fields that are inherited from superclasses.
+      The analyzer produces this diagnostic when an initializing formal
+      parameter is found in a constructor in a class that doesn't declare the
+      field being initialized. Constructors can't initialize fields that aren't
+      declared and fields that are inherited from superclasses.
 
       #### Example
 
@@ -6076,7 +6080,7 @@
       0: the type of the list literal
       1: the expected type
   INVALID_CAST_LITERAL_MAP:
-    problemMessage: "The map literal type '{0}' isn't of expected type '{1}'. The maps's type can be changed with an explicit generic type arguments or by changing the key and value types."
+    problemMessage: "The map literal type '{0}' isn't of expected type '{1}'. The map's type can be changed with an explicit generic type arguments or by changing the key and value types."
     comment: |-
       Parameters:
       0: the type of the map literal
diff --git a/pkg/analyzer/test/src/dart/element/inheritance_manager3_test.dart b/pkg/analyzer/test/src/dart/element/inheritance_manager3_test.dart
index ba5aa89..73da833 100644
--- a/pkg/analyzer/test/src/dart/element/inheritance_manager3_test.dart
+++ b/pkg/analyzer/test/src/dart/element/inheritance_manager3_test.dart
@@ -1425,6 +1425,7 @@
 
       if (element is PropertyAccessorElement) {
         var variable = element.variable;
+        expect(variable.enclosingElement, same(element.enclosingElement));
         expect(variable.name, element.displayName);
         if (element.isGetter) {
           expect(variable.type, element.returnType);
diff --git a/pkg/analyzer/test/src/dart/resolution/enum_test.dart b/pkg/analyzer/test/src/dart/resolution/enum_test.dart
index 7fe267d..1e71eb6 100644
--- a/pkg/analyzer/test/src/dart/resolution/enum_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/enum_test.dart
@@ -15,6 +15,20 @@
 
 @reflectiveTest
 class EnumDriverResolutionTest extends PubPackageResolutionTest {
+  test_field() async {
+    await assertNoErrorsInCode(r'''
+enum E<T> {
+  v;
+  final foo = 42;
+}
+''');
+
+    assertElement(
+      findNode.variableDeclaration('foo ='),
+      findElement.field('foo', of: 'E'),
+    );
+  }
+
   test_inference_listLiteral() async {
     await assertNoErrorsInCode(r'''
 enum E1 {a, b}
@@ -72,6 +86,20 @@
     );
   }
 
+  test_method_toString() async {
+    await assertNoErrorsInCode(r'''
+enum E<T> {
+  v;
+  String toString() => 'E';
+}
+''');
+
+    assertElement(
+      findNode.methodDeclaration('toString'),
+      findElement.method('toString', of: 'E'),
+    );
+  }
+
   test_value_underscore() async {
     await assertNoErrorsInCode(r'''
 enum E { _ }
diff --git a/pkg/analyzer/test/src/summary/resynthesize_common.dart b/pkg/analyzer/test/src/summary/resynthesize_common.dart
index a61787f..4c29cd7 100644
--- a/pkg/analyzer/test/src/summary/resynthesize_common.dart
+++ b/pkg/analyzer/test/src/summary/resynthesize_common.dart
@@ -17582,6 +17582,90 @@
 ''');
   }
 
+  test_enum_field() async {
+    var library = await checkLibrary(r'''
+enum E {
+  v;
+  final foo = 42;
+}
+''');
+    checkElementText(library, r'''
+library
+  definingUnit
+    enums
+      enum E @5
+        supertype: Enum
+        fields
+          synthetic final index @-1
+            type: int
+          static const enumConstant v @11
+            type: E
+            constantInitializer
+              InstanceCreationExpression
+                argumentList: ArgumentList
+                  arguments
+                    IntegerLiteral
+                      literal: 0 @0
+                      staticType: int
+                    SimpleStringLiteral
+                      literal: 'v' @0
+                  leftParenthesis: ( @0
+                  rightParenthesis: ) @0
+                constructorName: ConstructorName
+                  name: SimpleIdentifier
+                    staticElement: self::@enum::E::@constructor::_
+                    staticType: null
+                    token: _ @-1
+                  period: . @0
+                  staticElement: self::@enum::E::@constructor::_
+                  type: NamedType
+                    name: SimpleIdentifier
+                      staticElement: self::@enum::E
+                      staticType: null
+                      token: E @-1
+                    type: E
+                staticType: E
+          synthetic static const values @-1
+            type: List<E>
+            constantInitializer
+              ListLiteral
+                elements
+                  SimpleIdentifier
+                    staticElement: self::@enum::E::@getter::v
+                    staticType: E
+                    token: v @-1
+                leftBracket: [ @0
+                rightBracket: ] @0
+                staticType: List<E>
+          final foo @22
+            type: int
+            constantInitializer
+              IntegerLiteral
+                literal: 42 @28
+                staticType: int
+        constructors
+          synthetic const _ @-1
+            parameters
+              requiredPositional final this.index @-1
+                type: int
+                field: self::@enum::E::@field::index
+              requiredPositional name @-1
+                type: String
+        accessors
+          synthetic get index @-1
+            returnType: int
+          synthetic static get v @-1
+            returnType: E
+          synthetic static get values @-1
+            returnType: List<E>
+          synthetic get foo @-1
+            returnType: int
+        methods
+          synthetic toString @-1
+            returnType: String
+''');
+  }
+
   test_enum_method() async {
     var library = await checkLibrary(r'''
 enum E<T> {
@@ -17674,6 +17758,82 @@
 ''');
   }
 
+  test_enum_method_toString() async {
+    var library = await checkLibrary(r'''
+enum E {
+  v;
+  String toString() => 'E';
+}
+''');
+    checkElementText(library, r'''
+library
+  definingUnit
+    enums
+      enum E @5
+        supertype: Enum
+        fields
+          synthetic final index @-1
+            type: int
+          static const enumConstant v @11
+            type: E
+            constantInitializer
+              InstanceCreationExpression
+                argumentList: ArgumentList
+                  arguments
+                    IntegerLiteral
+                      literal: 0 @0
+                      staticType: int
+                    SimpleStringLiteral
+                      literal: 'v' @0
+                  leftParenthesis: ( @0
+                  rightParenthesis: ) @0
+                constructorName: ConstructorName
+                  name: SimpleIdentifier
+                    staticElement: self::@enum::E::@constructor::_
+                    staticType: null
+                    token: _ @-1
+                  period: . @0
+                  staticElement: self::@enum::E::@constructor::_
+                  type: NamedType
+                    name: SimpleIdentifier
+                      staticElement: self::@enum::E
+                      staticType: null
+                      token: E @-1
+                    type: E
+                staticType: E
+          synthetic static const values @-1
+            type: List<E>
+            constantInitializer
+              ListLiteral
+                elements
+                  SimpleIdentifier
+                    staticElement: self::@enum::E::@getter::v
+                    staticType: E
+                    token: v @-1
+                leftBracket: [ @0
+                rightBracket: ] @0
+                staticType: List<E>
+        constructors
+          synthetic const _ @-1
+            parameters
+              requiredPositional final this.index @-1
+                type: int
+                field: self::@enum::E::@field::index
+              requiredPositional name @-1
+                type: String
+        accessors
+          synthetic get index @-1
+            returnType: int
+          synthetic static get v @-1
+            returnType: E
+          synthetic static get values @-1
+            returnType: List<E>
+        methods
+          toString @23
+            returnType: String
+''');
+  }
+
   test_enum_typeParameters() async {
     var library = await checkLibrary('''
 enum E<T> {
diff --git a/pkg/analyzer/tool/diagnostics/diagnostics.md b/pkg/analyzer/tool/diagnostics/diagnostics.md
index 0b6ebfb..282257c 100644
--- a/pkg/analyzer/tool/diagnostics/diagnostics.md
+++ b/pkg/analyzer/tool/diagnostics/diagnostics.md
@@ -3501,9 +3501,10 @@
 
 #### Description
 
-The analyzer produces this diagnostic when there's more than one field
-formal parameter for the same field in a constructor's parameter list. It
-isn't useful to assign a value that will immediately be overwritten.
+The analyzer produces this diagnostic when there's more than one
+initializing formal parameter for the same field in a constructor's
+parameter list. It isn't useful to assign a value that will immediately be
+overwritten.
 
 #### Example
 
@@ -3520,7 +3521,7 @@
 
 #### Common fixes
 
-Remove one of the field formal parameters:
+Remove one of the initializing formal parameters:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -4718,7 +4719,8 @@
 #### Example
 
 The following code produces this diagnostic because the field `f` is
-initialized both by a field formal parameter and in the initializer list:
+initialized both by an initializing formal parameter and in the
+initializer list:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -4769,14 +4771,14 @@
 
 #### Description
 
-The analyzer produces this diagnostic when a factory constructor has a
-field formal parameter. Factory constructors can't assign values to fields
-because no instance is created; hence, there is no field to assign.
+The analyzer produces this diagnostic when a factory constructor has an
+initializing formal parameter. Factory constructors can't assign values to
+fields because no instance is created; hence, there is no field to assign.
 
 #### Example
 
 The following code produces this diagnostic because the factory constructor
-uses a field formal parameter:
+uses an initializing formal parameter:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -4788,7 +4790,7 @@
 
 #### Common fixes
 
-Replace the field formal parameter with a normal parameter:
+Replace the initializing formal parameter with a normal parameter:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -4862,8 +4864,8 @@
 #### Examples
 
 The following code produces this diagnostic because the constructor
-`C.zero`, which redirects to the constructor `C`, has a field formal
-parameter that initializes the field `f`:
+`C.zero`, which redirects to the constructor `C`, has an initializing
+formal parameter that initializes the field `f`:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -4891,8 +4893,8 @@
 
 #### Common fixes
 
-If the initialization is done by a field formal parameter, then use a
-normal parameter:
+If the initialization is done by an initializing formal parameter, then
+use a normal parameter:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -4923,14 +4925,16 @@
 
 #### Description
 
-The analyzer produces this diagnostic when the type of a field formal
-parameter isn't assignable to the type of the field being initialized.
+The analyzer produces this diagnostic when the type of an initializing
+formal parameter isn't assignable to the type of the field being
+initialized.
 
 #### Example
 
-The following code produces this diagnostic because the field formal
-parameter has the type `String`, but the type of the field is `int`. The
-parameter must have a type that is a subtype of the field's type.
+The following code produces this diagnostic because the initializing
+formal parameter has the type `String`, but the type of the field is
+`int`. The parameter must have a type that is a subtype of the field's
+type.
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -4966,8 +4970,8 @@
 {% endprettify %}
 
 If the types of both the field and the parameter are correct, then use an
-initializer rather than a field formal parameter to convert the parameter
-value into a value of the correct type:
+initializer rather than an initializing formal parameter to convert the
+parameter value into a value of the correct type:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -5052,7 +5056,7 @@
 
 For instance fields, you can add an initializer as shown in the previous
 example, or you can initialize the field in every constructor. You can
-initialize the field by using a field formal parameter:
+initialize the field by using an initializing formal parameter:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -5102,8 +5106,8 @@
 
 #### Common fixes
 
-If the value should be passed in to the constructor directly, then use a
-field formal parameter to initialize the field `value`:
+If the value should be passed in to the constructor directly, then use an
+initializing formal parameter to initialize the field `value`:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -5880,14 +5884,14 @@
 
 #### Description
 
-The analyzer produces this diagnostic when a static field is initialized in
-a constructor using either a field formal parameter or an assignment in the
-initializer list.
+The analyzer produces this diagnostic when a static field is initialized
+in a constructor using either an initializing formal parameter or an
+assignment in the initializer list.
 
 #### Example
 
-The following code produces this diagnostic because the static field `a` is
-being initialized by the field formal parameter `this.a`:
+The following code produces this diagnostic because the static field `a`
+is being initialized by the initializing formal parameter `this.a`:
 
 {% prettify dart tag=pre+code %}
 class C {
@@ -5936,10 +5940,10 @@
 
 #### Description
 
-The analyzer produces this diagnostic when a field formal parameter is
-found in a constructor in a class that doesn't declare the field being
-initialized. Constructors can't initialize fields that aren't declared and
-fields that are inherited from superclasses.
+The analyzer produces this diagnostic when an initializing formal
+parameter is found in a constructor in a class that doesn't declare the
+field being initialized. Constructors can't initialize fields that aren't
+declared and fields that are inherited from superclasses.
 
 #### Example
 
diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart
index 3e75181..f3d60c4 100644
--- a/pkg/dartdev/lib/dartdev.dart
+++ b/pkg/dartdev/lib/dartdev.dart
@@ -11,7 +11,6 @@
 import 'package:args/command_runner.dart';
 import 'package:cli_util/cli_logging.dart';
 import 'package:dart_style/src/cli/format_command.dart';
-import 'package:devtools_server/devtools_server.dart';
 import 'package:meta/meta.dart';
 import 'package:pub/pub.dart';
 import 'package:usage/usage.dart';
@@ -21,6 +20,7 @@
 import 'src/commands/compile.dart';
 import 'src/commands/create.dart';
 import 'src/commands/debug_adapter.dart';
+import 'src/commands/devtools.dart';
 import 'src/commands/doc.dart';
 import 'src/commands/fix.dart';
 import 'src/commands/language_server.dart';
@@ -30,7 +30,6 @@
 import 'src/core.dart';
 import 'src/events.dart';
 import 'src/experiments.dart';
-import 'src/sdk.dart';
 import 'src/utils.dart';
 import 'src/vm_interop_handler.dart';
 
@@ -118,7 +117,6 @@
     addCommand(DocCommand(verbose: verbose));
     addCommand(DevToolsCommand(
       verbose: verbose,
-      customDevToolsPath: sdk.devToolsBinaries,
     ));
     addCommand(FixCommand(verbose: verbose));
     addCommand(FormatCommand(verbose: verbose));
diff --git a/pkg/dartdev/lib/src/commands/devtools.dart b/pkg/dartdev/lib/src/commands/devtools.dart
new file mode 100644
index 0000000..e01c6bb
--- /dev/null
+++ b/pkg/dartdev/lib/src/commands/devtools.dart
@@ -0,0 +1,257 @@
+// Copyright (c) 2022, 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 'package:dds/devtools_server.dart';
+import 'package:dds/src/devtools/utils.dart';
+import 'package:path/path.dart' as path;
+
+import '../core.dart';
+import '../sdk.dart';
+
+class DevToolsCommand extends DartdevCommand {
+  static const commandDescription =
+      'Open DevTools (optionally connecting to an existing application).';
+
+  static const protocolVersion = '1.1.0';
+  static const argHelp = 'help';
+  static const argVmUri = 'vm-uri';
+  static const argEnableNotifications = 'enable-notifications';
+  static const argAllowEmbedding = 'allow-embedding';
+  static const argAppSizeBase = 'appSizeBase';
+  static const argAppSizeTest = 'appSizeTest';
+  static const argHeadlessMode = 'headless';
+  static const argDebugMode = 'debug';
+  static const argLaunchBrowser = 'launch-browser';
+  static const argMachine = 'machine';
+  static const argHost = 'host';
+  static const argPort = 'port';
+  static const argProfileMemory = 'record-memory-profile';
+  static const argTryPorts = 'try-ports';
+  static const argVerbose = 'verbose';
+  static const argVersion = 'version';
+  static const launchDevToolsService = 'launchDevTools';
+
+  DevToolsCommand({
+    this.customDevToolsPath,
+    bool verbose = false,
+  }) : super(
+          'devtools',
+          commandDescription,
+          verbose,
+        ) {
+    argParser
+      ..addFlag(
+        argVersion,
+        negatable: false,
+        help: 'Prints the DevTools version.',
+      )
+      ..addFlag(
+        argVerbose,
+        negatable: false,
+        abbr: 'v',
+        help: 'Output more informational messages.',
+      )
+      ..addOption(
+        argHost,
+        valueHelp: 'host',
+        help: 'Hostname to serve DevTools on (defaults to localhost).',
+      )
+      ..addOption(
+        argPort,
+        defaultsTo: '9100',
+        valueHelp: 'port',
+        help: 'Port to serve DevTools on; specify 0 to automatically use any '
+            'available port.',
+      )
+      ..addFlag(
+        argLaunchBrowser,
+        help:
+            'Launches DevTools in a browser immediately at start.\n(defaults to on unless in --machine mode)',
+      )
+      ..addFlag(
+        argMachine,
+        negatable: false,
+        help: 'Sets output format to JSON for consumption in tools.',
+      )
+      ..addSeparator('Memory profiling options:')
+      ..addOption(
+        argProfileMemory,
+        valueHelp: 'file',
+        defaultsTo: 'memory_samples.json',
+        help:
+            'Start devtools headlessly and write memory profiling samples to the '
+            'indicated file.',
+      );
+
+    if (verbose) {
+      argParser.addSeparator('App size options:');
+    }
+
+    // TODO(devoncarew): --appSizeBase and --appSizeTest should be renamed to
+    // something like --app-size-base and --app-size-test; #3146.
+    argParser
+      ..addOption(
+        argAppSizeBase,
+        valueHelp: 'appSizeBase',
+        help: 'Path to the base app size file used for app size debugging.',
+        hide: !verbose,
+      )
+      ..addOption(
+        argAppSizeTest,
+        valueHelp: 'appSizeTest',
+        help:
+            'Path to the test app size file used for app size debugging.\nThis '
+            'file should only be specified if --$argAppSizeBase is also specified.',
+        hide: !verbose,
+      );
+
+    if (verbose) {
+      argParser.addSeparator('Advanced options:');
+    }
+
+    // Args to show for verbose mode.
+    argParser
+      ..addOption(
+        argTryPorts,
+        defaultsTo: DevToolsServer.defaultTryPorts.toString(),
+        valueHelp: 'count',
+        help: 'The number of ascending ports to try binding to before failing '
+            'with an error. ',
+        hide: !verbose,
+      )
+      ..addFlag(
+        argEnableNotifications,
+        negatable: false,
+        help: 'Requests notification permissions immediately when a client '
+            'connects back to the server.',
+        hide: !verbose,
+      )
+      ..addFlag(
+        argAllowEmbedding,
+        help: 'Allow embedding DevTools inside an iframe.',
+        hide: !verbose,
+      )
+      ..addFlag(
+        argHeadlessMode,
+        negatable: false,
+        help: 'Causes the server to spawn Chrome in headless mode for use in '
+            'automated testing.',
+        hide: !verbose,
+      );
+
+    // Deprecated and hidden argResults.
+    // TODO: Remove this - prefer that clients use the rest arg.
+    argParser
+      ..addOption(
+        argVmUri,
+        defaultsTo: '',
+        help: 'VM Service protocol URI.',
+        hide: true,
+      )
+
+      // Development only argResults.
+      ..addFlag(
+        argDebugMode,
+        negatable: false,
+        help: 'Run a debug build of the DevTools web frontend.',
+        hide: true,
+      );
+  }
+
+  final String customDevToolsPath;
+
+  @override
+  String get name => 'devtools';
+
+  @override
+  String get description => commandDescription;
+
+  @override
+  String get invocation => '${super.invocation} [service protocol uri]';
+
+  @override
+  Future<int> run() async {
+    final bool version = argResults[argVersion];
+    final bool machineMode = argResults[argMachine];
+    // launchBrowser defaults based on machine-mode if not explicitly supplied.
+    final bool launchBrowser = argResults.wasParsed(argLaunchBrowser)
+        ? argResults[argLaunchBrowser]
+        : !machineMode;
+    final bool enableNotifications = argResults[argEnableNotifications];
+    final bool allowEmbedding = argResults.wasParsed(argAllowEmbedding)
+        ? argResults[argAllowEmbedding]
+        : true;
+
+    final port = argResults[argPort] != null
+        ? int.tryParse(argResults[argPort]) ?? 0
+        : 0;
+
+    final bool headlessMode = argResults[argHeadlessMode];
+    final bool debugMode = argResults[argDebugMode];
+
+    final numPortsToTry = argResults[argTryPorts] != null
+        ? int.tryParse(argResults[argTryPorts]) ?? 0
+        : DevToolsServer.defaultTryPorts;
+
+    final bool verboseMode = argResults[argVerbose];
+    final String hostname = argResults[argHost];
+    final String appSizeBase = argResults[argAppSizeBase];
+    final String appSizeTest = argResults[argAppSizeTest];
+
+    final sdkDir = path.dirname(sdk.dart);
+    final fullSdk = sdkDir.endsWith('bin');
+    final devToolsBinaries =
+        fullSdk ? sdk.devToolsBinaries : path.absolute(sdkDir, 'devtools');
+
+    if (version) {
+      final versionStr = await DevToolsUtils.getVersion(devToolsBinaries);
+      DevToolsUtils.printOutput(
+        'Dart DevTools version $versionStr',
+        {
+          'version': versionStr,
+        },
+        machineMode: machineMode,
+      );
+      return null;
+    }
+
+    // Prefer getting the VM URI from the rest argResults; fall back on the 'vm-url'
+    // option otherwise.
+    String serviceProtocolUri;
+    if (argResults.rest.isNotEmpty) {
+      serviceProtocolUri = argResults.rest.first;
+    } else if (argResults.wasParsed(argVmUri)) {
+      serviceProtocolUri = argResults[argVmUri];
+    }
+
+    // Support collecting profile data.
+    String profileFilename;
+    if (argResults.wasParsed(argProfileMemory)) {
+      profileFilename = argResults[argProfileMemory];
+    }
+    if (profileFilename != null && !path.isAbsolute(profileFilename)) {
+      profileFilename = path.absolute(profileFilename);
+    }
+
+    final server = await DevToolsServer().serveDevTools(
+      machineMode: machineMode,
+      debugMode: debugMode,
+      launchBrowser: launchBrowser,
+      enableNotifications: enableNotifications,
+      allowEmbedding: allowEmbedding,
+      port: port,
+      headlessMode: headlessMode,
+      numPortsToTry: numPortsToTry,
+      customDevToolsPath: customDevToolsPath ?? devToolsBinaries,
+      serviceProtocolUri: serviceProtocolUri,
+      profileFilename: profileFilename,
+      verboseMode: verboseMode,
+      hostname: hostname,
+      appSizeBase: appSizeBase,
+      appSizeTest: appSizeTest,
+    );
+
+    return server == null ? -1 : 0;
+  }
+}
diff --git a/pkg/dartdev/lib/src/commands/doc.dart b/pkg/dartdev/lib/src/commands/doc.dart
index 8faff48..6201a28 100644
--- a/pkg/dartdev/lib/src/commands/doc.dart
+++ b/pkg/dartdev/lib/src/commands/doc.dart
@@ -23,15 +23,20 @@
 
   DocCommand({bool verbose = false}) : super(cmdName, cmdDescription, verbose) {
     argParser.addOption(
-      'output-dir',
+      'output',
       abbr: 'o',
+      valueHelp: 'directory',
       defaultsTo: path.join('doc', 'api'),
-      help: 'Output directory',
+      aliases: [
+        // The CLI option that shipped with Dart 2.16.
+        'output-dir',
+      ],
+      help: 'Configure the output directory.',
     );
     argParser.addFlag(
       'validate-links',
-      negatable: true,
-      help: 'Display context aware warnings for broken links (slow)',
+      negatable: false,
+      help: 'Display warnings for broken links.',
     );
   }
 
@@ -51,21 +56,17 @@
       usageException("Error: Input directory doesn't exist: ${dir.path}");
     }
 
-    // Parse options.
-    final options = [
-      '--input=${dir.path}',
-      '--output=${argResults['output-dir']}',
-    ];
-    if (argResults['validate-links']) {
-      options.add('--validate-links');
-    } else {
-      options.add('--no-validate-links');
-    }
-
     // Specify where dartdoc resources are located.
     final resourcesPath =
         path.absolute(sdk.sdkPath, 'bin', 'resources', 'dartdoc', 'resources');
-    options.add('--resources-dir=$resourcesPath');
+
+    // Build options.
+    final options = [
+      '--input=${dir.path}',
+      '--output=${argResults['output']}',
+      '--resources-dir=$resourcesPath',
+      if (argResults['validate-links']) '--validate-links'
+    ];
 
     final config = await parseOptions(pubPackageMetaProvider, options);
     if (config == null) {
@@ -73,11 +74,10 @@
       return 2;
     }
 
-    // Call dartdoc.
+    // Call into package:dartdoc.
     if (verbose) {
       log.stdout('Using the following options: $options');
     }
-
     final packageConfigProvider = PhysicalPackageConfigProvider();
     final packageBuilder = PubPackageBuilder(
         config, pubPackageMetaProvider, packageConfigProvider);
diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml
index 9d78178..ea17317 100644
--- a/pkg/dartdev/pubspec.yaml
+++ b/pkg/dartdev/pubspec.yaml
@@ -20,7 +20,6 @@
   dartdoc: any
   dds:
     path: ../dds
-  devtools_server: any
   front_end:
     path: ../front_end
   meta:
diff --git a/pkg/dartdev/test/commands/devtools_test.dart b/pkg/dartdev/test/commands/devtools_test.dart
index e14ba0ac..24cb80b 100644
--- a/pkg/dartdev/test/commands/devtools_test.dart
+++ b/pkg/dartdev/test/commands/devtools_test.dart
@@ -37,8 +37,10 @@
     expect(result.exitCode, 0);
     expect(result.stderr, isEmpty);
     expect(result.stdout, contains('Open DevTools'));
-    expect(result.stdout,
-        contains('Usage: dart devtools [arguments] [service protocol uri]'));
+    expect(
+        result.stdout,
+        contains(
+            'Usage: dart [vm-options] devtools [arguments] [service protocol uri]'));
 
     // Shows verbose help.
     expect(result.stdout, contains('--try-ports'));
diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md
index 03a0044..3cb8dd8 100644
--- a/pkg/dds/CHANGELOG.md
+++ b/pkg/dds/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 2.2.0
+- Add support for serving DevTools via `package:dds/devtools_server.dart`
+
 # 2.1.7
 - Re-release 2.1.6+1.
 
diff --git a/pkg/dds/lib/devtools_server.dart b/pkg/dds/lib/devtools_server.dart
new file mode 100644
index 0000000..ee451fa
--- /dev/null
+++ b/pkg/dds/lib/devtools_server.dart
@@ -0,0 +1,403 @@
+// Copyright (c) 2022, 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:io';
+
+import 'package:browser_launcher/browser_launcher.dart';
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf;
+
+import 'src/devtools/client.dart';
+import 'src/devtools/handler.dart';
+import 'src/devtools/machine_mode_command_handler.dart';
+import 'src/devtools/memory_profile.dart';
+import 'src/devtools/utils.dart';
+import 'src/utils/console.dart';
+
+class DevToolsServer {
+  static const protocolVersion = '1.1.0';
+  static const defaultTryPorts = 10;
+
+  MachineModeCommandHandler? _machineModeCommandHandler;
+  late ClientManager clientManager;
+  final bool _isChromeOS = File('/dev/.cros_milestone').existsSync();
+
+  /// Serves DevTools.
+  ///
+  /// `handler` is the [shelf.Handler] that the server will use for all requests.
+  /// If null, [defaultHandler] will be used. Defaults to null.
+  ///
+  /// `customDevToolsPath` is a path to a directory containing a pre-built
+  /// DevTools application.
+  ///
+  // Note: this method is used by the Dart CLI and by package:dwds.
+  Future<HttpServer?> serveDevTools({
+    bool enableStdinCommands = true,
+    bool machineMode = false,
+    bool debugMode = false,
+    bool launchBrowser = false,
+    bool enableNotifications = false,
+    bool allowEmbedding = false,
+    bool headlessMode = false,
+    bool verboseMode = false,
+    String? hostname,
+    String? customDevToolsPath,
+    int port = 0,
+    int numPortsToTry = defaultTryPorts,
+    shelf.Handler? handler,
+    String? serviceProtocolUri,
+    String? profileFilename,
+    String? appSizeBase,
+    String? appSizeTest,
+  }) async {
+    hostname ??= 'localhost';
+
+    // Collect profiling information.
+    if (profileFilename != null && serviceProtocolUri != null) {
+      final Uri? vmServiceUri = Uri.tryParse(serviceProtocolUri);
+      if (vmServiceUri != null) {
+        await _hookupMemoryProfiling(
+          vmServiceUri,
+          profileFilename,
+          verboseMode,
+        );
+      }
+      return null;
+    }
+
+    if (machineMode) {
+      assert(
+        enableStdinCommands,
+        'machineMode only works with enableStdinCommands.',
+      );
+    }
+
+    clientManager = ClientManager(
+      requestNotificationPermissions: enableNotifications,
+    );
+    handler ??= await defaultHandler(
+      buildDir: customDevToolsPath!,
+      clientManager: clientManager,
+    );
+
+    HttpServer? server;
+    SocketException? ex;
+    while (server == null && numPortsToTry >= 0) {
+      // If we have tried [numPortsToTry] ports and still have not been able to
+      // connect, try port 0 to find a random available port.
+      if (numPortsToTry == 0) port = 0;
+
+      try {
+        server = await HttpMultiServer.bind(hostname, port);
+      } on SocketException catch (e) {
+        ex = e;
+        numPortsToTry--;
+        port++;
+      }
+    }
+
+    // Re-throw the last exception if we failed to bind.
+    if (server == null && ex != null) {
+      throw ex;
+    }
+
+    final _server = server!;
+    if (allowEmbedding) {
+      _server.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
+    }
+
+    // Ensure browsers don't cache older versions of the app.
+    _server.defaultResponseHeaders.add(
+      HttpHeaders.cacheControlHeader,
+      'max-age=900',
+    );
+
+    // Serve requests in an error zone to prevent failures
+    // when running from another error zone.
+    runZonedGuarded(
+      () => shelf.serveRequests(_server, handler!),
+      (e, _) => print('Error serving requests: $e'),
+    );
+
+    final devToolsUrl = 'http://${_server.address.host}:${_server.port}';
+
+    if (launchBrowser) {
+      if (serviceProtocolUri != null) {
+        serviceProtocolUri =
+            _normalizeVmServiceUri(serviceProtocolUri).toString();
+      }
+
+      final queryParameters = {
+        if (serviceProtocolUri != null) 'uri': serviceProtocolUri,
+        if (appSizeBase != null) 'appSizeBase': appSizeBase,
+        if (appSizeTest != null) 'appSizeTest': appSizeTest,
+      };
+      String url = Uri.parse(devToolsUrl)
+          .replace(queryParameters: queryParameters)
+          .toString();
+
+      // If app size parameters are present, open to the standalone `appsize`
+      // page, regardless if there is a vm service uri specified. We only check
+      // for the presence of [appSizeBase] here because [appSizeTest] may or may
+      // not be specified (it should only be present for diffs). If [appSizeTest]
+      // is present without [appSizeBase], we will ignore the parameter.
+      if (appSizeBase != null) {
+        final startQueryParamIndex = url.indexOf('?');
+        if (startQueryParamIndex != -1) {
+          url = '${url.substring(0, startQueryParamIndex)}'
+              '/#/appsize'
+              '${url.substring(startQueryParamIndex)}';
+        }
+      }
+
+      try {
+        await Chrome.start([url]);
+      } catch (e) {
+        print('Unable to launch Chrome: $e\n');
+      }
+    }
+
+    if (enableStdinCommands) {
+      String message = '''Serving DevTools at $devToolsUrl.
+
+          Hit ctrl-c to terminate the server.''';
+      if (!machineMode && debugMode) {
+        // Add bold to help find the correct url to open.
+        message = ConsoleUtils.bold('$message\n');
+      }
+
+      DevToolsUtils.printOutput(
+        message,
+        {
+          'event': 'server.started',
+          // TODO(dantup): Remove this `method` field when we're sure VS Code
+          // users are all on a newer version that uses `event`. We incorrectly
+          // used `method` for the original releases.
+          'method': 'server.started',
+          'params': {
+            'host': _server.address.host,
+            'port': _server.port,
+            'pid': pid,
+            'protocolVersion': protocolVersion,
+          }
+        },
+        machineMode: machineMode,
+      );
+
+      if (machineMode) {
+        _machineModeCommandHandler = MachineModeCommandHandler(server: this);
+        await _machineModeCommandHandler!.initialize(
+          devToolsUrl: devToolsUrl,
+          headlessMode: headlessMode,
+        );
+      }
+    }
+
+    return server;
+  }
+
+  Future<Map<String, dynamic>> launchDevTools(
+      Map<String, dynamic> params,
+      Uri vmServiceUri,
+      String devToolsUrl,
+      bool headlessMode,
+      bool machineMode) async {
+    // First see if we have an existing DevTools client open that we can
+    // reuse.
+    final canReuse =
+        params.containsKey('reuseWindows') && params['reuseWindows'] == true;
+    final shouldNotify =
+        params.containsKey('notify') && params['notify'] == true;
+    final page = params['page'];
+    if (canReuse &&
+        _tryReuseExistingDevToolsInstance(
+          vmServiceUri,
+          page,
+          shouldNotify,
+        )) {
+      _emitLaunchEvent(
+          reused: true,
+          notified: shouldNotify,
+          pid: null,
+          machineMode: machineMode);
+      return {
+        'reused': true,
+        'notified': shouldNotify,
+      };
+    }
+
+    final uriParams = <String, dynamic>{};
+
+    // Copy over queryParams passed by the client
+    params['queryParams']?.forEach((key, value) => uriParams[key] = value);
+
+    // Add the URI to the VM service
+    uriParams['uri'] = vmServiceUri.toString();
+
+    final devToolsUri = Uri.parse(devToolsUrl);
+    final uriToLaunch = _buildUriToLaunch(uriParams, page, devToolsUri);
+
+    // TODO(dantup): When ChromeOS has support for tunneling all ports we can
+    // change this to always use the native browser for ChromeOS and may wish to
+    // handle this inside `browser_launcher`; https://crbug.com/848063.
+    final useNativeBrowser = _isChromeOS &&
+        _isAccessibleToChromeOSNativeBrowser(devToolsUri) &&
+        _isAccessibleToChromeOSNativeBrowser(vmServiceUri);
+    int? browserPid;
+    if (useNativeBrowser) {
+      await Process.start('x-www-browser', [uriToLaunch.toString()]);
+    } else {
+      final args = headlessMode
+          ? [
+              '--headless',
+              // When running headless, Chrome will quit immediately after loading
+              // the page unless we have the debug port open.
+              '--remote-debugging-port=9223',
+              '--disable-gpu',
+              '--no-sandbox',
+            ]
+          : <String>[];
+      final proc = await Chrome.start([uriToLaunch.toString()], args: args);
+      browserPid = proc.pid;
+    }
+    _emitLaunchEvent(
+        reused: false,
+        notified: false,
+        pid: browserPid!,
+        machineMode: machineMode);
+    return {
+      'reused': false,
+      'notified': false,
+      'pid': browserPid,
+    };
+  }
+
+  Future<void> _hookupMemoryProfiling(
+    Uri observatoryUri,
+    String profileFile, [
+    bool verboseMode = false,
+  ]) async {
+    final service = await DevToolsUtils.connectToVmService(observatoryUri);
+    if (service == null) {
+      return;
+    }
+
+    final memoryProfiler = MemoryProfile(service, profileFile, verboseMode);
+    memoryProfiler.startPolling();
+
+    print('Writing memory profile samples to $profileFile...');
+  }
+
+  bool _tryReuseExistingDevToolsInstance(
+    Uri vmServiceUri,
+    String page,
+    bool notifyUser,
+  ) {
+    // First try to find a client that's already connected to this VM service,
+    // and just send the user a notification for that one.
+    final existingClient =
+        clientManager.findExistingConnectedReusableClient(vmServiceUri);
+    if (existingClient != null) {
+      try {
+        existingClient.showPage(page);
+        if (notifyUser) {
+          existingClient.notify();
+        }
+        return true;
+      } catch (e) {
+        print('Failed to reuse existing connected DevTools client');
+        print(e);
+      }
+    }
+
+    final reusableClient = clientManager.findReusableClient();
+    if (reusableClient != null) {
+      try {
+        reusableClient.connectToVmService(vmServiceUri, notifyUser);
+        return true;
+      } catch (e) {
+        print('Failed to reuse existing DevTools client');
+        print(e);
+      }
+    }
+    return false;
+  }
+
+  String _buildUriToLaunch(
+    Map<String, dynamic> uriParams,
+    page,
+    Uri devToolsUri,
+  ) {
+    final queryStringNameValues = [];
+    uriParams.forEach((key, value) => queryStringNameValues.add(
+        '${Uri.encodeQueryComponent(key)}=${Uri.encodeQueryComponent(value)}'));
+
+    if (page != null) {
+      queryStringNameValues.add('page=${Uri.encodeQueryComponent(page)}');
+    }
+
+    return devToolsUri
+        .replace(
+            path: '${devToolsUri.path.isEmpty ? '/' : devToolsUri.path}',
+            fragment: '?${queryStringNameValues.join('&')}')
+        .toString();
+  }
+
+  /// Prints a launch event to stdout so consumers of the DevTools server
+  /// can see when clients are being launched/reused.
+  void _emitLaunchEvent(
+      {required bool reused,
+      required bool notified,
+      required int? pid,
+      required bool machineMode}) {
+    DevToolsUtils.printOutput(
+      null,
+      {
+        'event': 'client.launch',
+        'params': {
+          'reused': reused,
+          'notified': notified,
+          'pid': pid,
+        },
+      },
+      machineMode: machineMode,
+    );
+  }
+
+  bool _isAccessibleToChromeOSNativeBrowser(Uri uri) {
+    const tunneledPorts = {
+      8000,
+      8008,
+      8080,
+      8085,
+      8888,
+      9005,
+      3000,
+      4200,
+      5000
+    };
+    return uri.hasPort && tunneledPorts.contains(uri.port);
+  }
+
+  // TODO(https://github.com/flutter/devtools/issues/3571): move to devtools_shared.
+  // Note: please keep this copy of normalizeVmServiceUri() in sync with the one
+  // in devtools_app.
+  Uri? _normalizeVmServiceUri(String value) {
+    value = value.trim();
+
+    // Cleanup encoded urls likely copied from the uri of an existing running
+    // DevTools app.
+    if (value.contains('%3A%2F%2F')) {
+      value = Uri.decodeFull(value);
+    }
+    final uri = Uri.parse(value.trim()).removeFragment();
+    if (!uri.isAbsolute) {
+      return null;
+    }
+    if (uri.path.endsWith('/')) return uri;
+    return uri.replace(path: uri.path);
+  }
+}
diff --git a/pkg/dds/lib/src/dds_impl.dart b/pkg/dds/lib/src/dds_impl.dart
index 7de1b71..1d76900 100644
--- a/pkg/dds/lib/src/dds_impl.dart
+++ b/pkg/dds/lib/src/dds_impl.dart
@@ -24,7 +24,7 @@
 import 'client.dart';
 import 'client_manager.dart';
 import 'constants.dart';
-import 'devtools/devtools_handler.dart';
+import 'devtools/handler.dart';
 import 'expression_evaluator.dart';
 import 'isolate_manager.dart';
 import 'stream_manager.dart';
@@ -297,7 +297,7 @@
       // the VM service.
       final String buildDir =
           _devToolsConfiguration!.customBuildDirectoryPath.toFilePath();
-      return devtoolsHandler(
+      return defaultHandler(
         dds: this,
         buildDir: buildDir,
         notFoundHandler: proxyHandler(remoteVmServiceUri),
diff --git a/pkg/dds/lib/src/devtools/client.dart b/pkg/dds/lib/src/devtools/client.dart
new file mode 100644
index 0000000..af82b55
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/client.dart
@@ -0,0 +1,220 @@
+// Copyright (c) 2022, 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:collection/collection.dart';
+import 'package:devtools_shared/devtools_server.dart';
+import 'package:json_rpc_2/src/peer.dart' as json_rpc;
+import 'package:meta/meta.dart';
+import 'package:sse/src/server/sse_handler.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+class LoggingMiddlewareSink<S> implements StreamSink<S> {
+  LoggingMiddlewareSink(this.sink);
+
+  @override
+  void add(S event) {
+    print('DevTools SSE response: $event');
+    sink.add(event);
+  }
+
+  @override
+  void addError(Object error, [StackTrace? stackTrace]) {
+    print('DevTools SSE error response: $error');
+    sink.addError(error);
+  }
+
+  @override
+  Future addStream(Stream<S> stream) {
+    return sink.addStream(stream);
+  }
+
+  @override
+  Future close() => sink.close();
+
+  @override
+  Future get done => sink.done;
+
+  final StreamSink sink;
+}
+
+/// A connection between a DevTools front-end app and the DevTools server.
+///
+/// see `packages/devtools_app/lib/src/server_connection.dart`.
+class ClientManager {
+  ClientManager({required this.requestNotificationPermissions});
+
+  /// Whether to immediately request notification permissions when a client connects.
+  /// Otherwise permission will be requested only with the first notification.
+  final bool requestNotificationPermissions;
+  final List<DevToolsClient> _clients = [];
+
+  void acceptClient(SseConnection connection, {bool enableLogging = false}) {
+    final client = DevToolsClient.fromSSEConnection(
+      connection,
+      enableLogging,
+    );
+    if (requestNotificationPermissions) {
+      client.enableNotifications();
+    }
+    _clients.add(client);
+    connection.sink.done.then((_) => _clients.remove(client));
+  }
+
+  /// Finds an active DevTools instance that is not already connecting to
+  /// a VM service that we can reuse (for example if a user stopped debugging
+  /// and it disconnected, then started debugging again, we want to reuse
+  /// the open DevTools window).
+  DevToolsClient? findReusableClient() {
+    return _clients.firstWhereOrNull(
+      (c) => !c.hasConnection && !c.embedded,
+    );
+  }
+
+  /// Finds a client that may already be connected to this VM Service.
+  DevToolsClient? findExistingConnectedReusableClient(Uri vmServiceUri) {
+    // Checking the whole URI will fail if DevTools converted it from HTTP to
+    // WS, so just check the host, port and first segment of path (token).
+    return _clients.firstWhereOrNull(
+      (c) =>
+          c.hasConnection &&
+          !c.embedded &&
+          _areSameVmServices(c.vmServiceUri!, vmServiceUri),
+    );
+  }
+
+  @override
+  String toString() {
+    return _clients.map((c) {
+      return '${c.hasConnection.toString().padRight(5)} '
+          '${c.currentPage?.padRight(12)} ${c.vmServiceUri.toString()}';
+    }).join('\n');
+  }
+
+  Map<String, dynamic> toJson(dynamic id) => {
+        'id': id,
+        'result': {
+          'clients': _clients.map((e) => e.toJson()).toList(),
+        }
+      };
+
+  bool _areSameVmServices(Uri uri1, Uri uri2) {
+    return uri1.host == uri2.host &&
+        uri1.port == uri2.port &&
+        uri1.pathSegments.isNotEmpty &&
+        uri2.pathSegments.isNotEmpty &&
+        uri1.pathSegments[0] == uri2.pathSegments[0];
+  }
+}
+
+/// Represents a DevTools client connection to the DevTools server API.
+class DevToolsClient {
+  factory DevToolsClient.fromSSEConnection(
+    SseConnection sse,
+    bool loggingEnabled,
+  ) {
+    Stream<String> stream = sse.stream;
+    StreamSink sink = sse.sink;
+    return DevToolsClient(
+      stream: stream,
+      sink: sink,
+      loggingEnabled: loggingEnabled,
+    );
+  }
+
+  @visibleForTesting
+  DevToolsClient({
+    required Stream<String> stream,
+    required StreamSink sink,
+    bool loggingEnabled = false,
+  }) {
+    if (loggingEnabled) {
+      stream = stream.map<String>((String e) {
+        print('DevTools SSE request: $e');
+        return e;
+      });
+      sink = LoggingMiddlewareSink(sink);
+    }
+
+    _devToolsPeer = json_rpc.Peer(
+      StreamChannel(stream, sink as StreamSink<String>),
+      strictProtocolChecks: false,
+    );
+    _registerJsonRpcMethods();
+    _devToolsPeer.listen();
+  }
+
+  void _registerJsonRpcMethods() {
+    _devToolsPeer.registerMethod('connected', (parameters) {
+      _vmServiceUri = Uri.parse(parameters['uri'].asString);
+    });
+
+    _devToolsPeer.registerMethod('disconnected', (parameters) {
+      _vmServiceUri = null;
+    });
+
+    _devToolsPeer.registerMethod('currentPage', (parameters) {
+      _currentPage = parameters['id'].asString;
+      _embedded = parameters['embedded'].asBool;
+    });
+
+    _devToolsPeer.registerMethod('getPreferenceValue', (parameters) {
+      final key = parameters['key'].asString;
+      final value = ServerApi.devToolsPreferences.properties[key];
+      return value;
+    });
+
+    _devToolsPeer.registerMethod('setPreferenceValue', (parameters) {
+      final key = parameters['key'].asString;
+      final value = parameters['value'].value;
+      ServerApi.devToolsPreferences.properties[key] = value;
+    });
+  }
+
+  /// Notify the DevTools client to connect to a specific VM service instance.
+  void connectToVmService(Uri uri, bool notifyUser) {
+    _devToolsPeer.sendNotification('connectToVm', {
+      'uri': uri.toString(),
+      'notify': notifyUser,
+    });
+  }
+
+  void notify() => _devToolsPeer.sendNotification('notify');
+
+  /// Enable notifications to the user from this DevTools client.
+  void enableNotifications() =>
+      _devToolsPeer.sendNotification('enableNotifications');
+
+  /// Notify the DevTools client to show a specific page.
+  void showPage(String pageId) {
+    _devToolsPeer.sendNotification('showPage', {
+      'page': pageId,
+    });
+  }
+
+  Map<String, dynamic> toJson() => {
+        'hasConnection': hasConnection,
+        'currentPage': currentPage,
+        'embedded': embedded,
+        'vmServiceUri': vmServiceUri?.toString(),
+      };
+
+  /// The current DevTools page displayed by this client.
+  String? get currentPage => _currentPage;
+  String? _currentPage;
+
+  /// Returns true if this DevTools client is embedded.
+  bool get embedded => _embedded;
+  bool _embedded = false;
+
+  /// Returns the VM service URI that the DevTools client is currently
+  /// connected to. Returns null if the client is not connected to a process.
+  Uri? get vmServiceUri => _vmServiceUri;
+  Uri? _vmServiceUri;
+
+  bool get hasConnection => _vmServiceUri != null;
+
+  late json_rpc.Peer _devToolsPeer;
+}
diff --git a/pkg/dds/lib/src/devtools/devtools_client.dart b/pkg/dds/lib/src/devtools/devtools_client.dart
deleted file mode 100644
index e5ec54e..0000000
--- a/pkg/dds/lib/src/devtools/devtools_client.dart
+++ /dev/null
@@ -1,93 +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:devtools_shared/devtools_server.dart';
-import 'package:json_rpc_2/src/server.dart' as json_rpc;
-import 'package:sse/src/server/sse_handler.dart';
-import 'package:stream_channel/stream_channel.dart';
-
-class LoggingMiddlewareSink<S> implements StreamSink<S> {
-  LoggingMiddlewareSink(this.sink);
-
-  @override
-  void add(S event) {
-    print('DevTools SSE response: $event');
-    sink.add(event);
-  }
-
-  @override
-  void addError(Object error, [StackTrace? stackTrace]) {
-    print('DevTools SSE error response: $error');
-    sink.addError(error);
-  }
-
-  @override
-  Future addStream(Stream<S> stream) {
-    return sink.addStream(stream);
-  }
-
-  @override
-  Future close() => sink.close();
-
-  @override
-  Future get done => sink.done;
-
-  final StreamSink sink;
-}
-
-/// Represents a DevTools client connection to the DevTools server API.
-class DevToolsClient {
-  DevToolsClient.fromSSEConnection(
-    SseConnection sse,
-    bool loggingEnabled,
-  ) {
-    Stream<String> stream = sse.stream;
-    StreamSink sink = sse.sink;
-
-    if (loggingEnabled) {
-      stream = stream.map<String>((String e) {
-        print('DevTools SSE request: $e');
-        return e;
-      });
-      sink = LoggingMiddlewareSink(sink);
-    }
-
-    _server = json_rpc.Server(
-      StreamChannel(stream, sink as StreamSink<String>),
-      strictProtocolChecks: false,
-    );
-    _registerJsonRpcMethods();
-    _server.listen();
-  }
-
-  void _registerJsonRpcMethods() {
-    _server.registerMethod('connected', (parameters) {
-      // Nothing to do here.
-    });
-
-    _server.registerMethod('currentPage', (parameters) {
-      // Nothing to do here.
-    });
-
-    _server.registerMethod('disconnected', (parameters) {
-      // Nothing to do here.
-    });
-
-    _server.registerMethod('getPreferenceValue', (parameters) {
-      final key = parameters['key'].asString;
-      final value = ServerApi.devToolsPreferences.properties[key];
-      return value;
-    });
-
-    _server.registerMethod('setPreferenceValue', (parameters) {
-      final key = parameters['key'].asString;
-      final value = parameters['value'].value;
-      ServerApi.devToolsPreferences.properties[key] = value;
-    });
-  }
-
-  late json_rpc.Server _server;
-}
diff --git a/pkg/dds/lib/src/devtools/devtools_handler.dart b/pkg/dds/lib/src/devtools/handler.dart
similarity index 69%
rename from pkg/dds/lib/src/devtools/devtools_handler.dart
rename to pkg/dds/lib/src/devtools/handler.dart
index ad7067d..d323f9d 100644
--- a/pkg/dds/lib/src/devtools/devtools_handler.dart
+++ b/pkg/dds/lib/src/devtools/handler.dart
@@ -11,19 +11,23 @@
 
 import '../constants.dart';
 import '../dds_impl.dart';
-import 'devtools_client.dart';
+import 'client.dart';
 
 /// Returns a [Handler] which handles serving DevTools and the DevTools server
-/// API under $DDS_URI/devtools/.
+/// API.
 ///
 /// [buildDir] is the path to the pre-compiled DevTools instance to be served.
 ///
 /// [notFoundHandler] is a [Handler] to which requests that could not be handled
 /// by the DevTools handler are forwarded (e.g., a proxy to the VM service).
-FutureOr<Handler> devtoolsHandler({
-  required DartDevelopmentServiceImpl dds,
+///
+/// If [dds] is null, DevTools is not being served by a DDS instance and is
+/// served by a standalone server (see `package:dds/devtools_server.dart`).
+FutureOr<Handler> defaultHandler({
+  DartDevelopmentServiceImpl? dds,
   required String buildDir,
-  required Handler notFoundHandler,
+  ClientManager? clientManager,
+  Handler? notFoundHandler,
 }) {
   // Serves the web assets for DevTools.
   final devtoolsAssetHandler = createStaticHandler(
@@ -35,18 +39,20 @@
   // Note: the handler path needs to match the full *original* path, not the
   // current request URL (we remove '/devtools' in the initial router but we
   // need to include it here).
-  const devToolsSseHandlerPath = '/devtools/api/sse';
+  final devToolsSseHandlerPath = dds != null ? '/devtools/api/sse' : '/api/sse';
   final devToolsApiHandler = SseHandler(
-    dds.authCodesEnabled
-        ? Uri.parse('/${dds.authCode}$devToolsSseHandlerPath')
+    (dds?.authCodesEnabled ?? false)
+        ? Uri.parse('/${dds!.authCode}$devToolsSseHandlerPath')
         : Uri.parse(devToolsSseHandlerPath),
     keepAlive: sseKeepAlive,
   );
 
+  clientManager ??= ClientManager(requestNotificationPermissions: false);
+
   devToolsApiHandler.connections.rest.listen(
-    (sseConnection) => DevToolsClient.fromSSEConnection(
+    (sseConnection) => clientManager!.acceptClient(
       sseConnection,
-      dds.shouldLogRequests,
+      enableLogging: dds?.shouldLogRequests ?? false,
     ),
   );
 
@@ -73,12 +79,14 @@
   };
 
   return (request) {
-    final pathSegments = request.url.pathSegments;
-    if (pathSegments.isEmpty || pathSegments.first != 'devtools') {
-      return notFoundHandler(request);
+    if (notFoundHandler != null) {
+      final pathSegments = request.url.pathSegments;
+      if (pathSegments.isEmpty || pathSegments.first != 'devtools') {
+        return notFoundHandler(request);
+      }
+      // Forward all requests to /devtools/* to the DevTools handler.
+      request = request.change(path: 'devtools');
     }
-    // Forward all requests to /devtools/* to the DevTools handler.
-    request = request.change(path: 'devtools');
     return devtoolsHandler(request);
   };
 }
diff --git a/pkg/dds/lib/src/devtools/machine_mode_command_handler.dart b/pkg/dds/lib/src/devtools/machine_mode_command_handler.dart
new file mode 100644
index 0000000..5b65d4c
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/machine_mode_command_handler.dart
@@ -0,0 +1,429 @@
+// Copyright (c) 2022, 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.
+
+// TODO(https://github.com/dart-lang/sdk/issues/48161): investigate whether or
+// not machine mode is still relevant.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:devtools_shared/devtools_server.dart';
+import 'package:devtools_shared/devtools_shared.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../../devtools_server.dart';
+import 'utils.dart';
+
+class MachineModeCommandHandler {
+  static const launchDevToolsService = 'launchDevTools';
+  static const copyAndCreateDevToolsFile = 'copyAndCreateDevToolsFile';
+  static const restoreDevToolsFile = 'restoreDevToolsFiles';
+  static const errorLaunchingBrowserCode = 500;
+  static const bool machineMode = true;
+
+  MachineModeCommandHandler({required this.server});
+
+  /// The server instance associated with this client.
+  final DevToolsServer server;
+
+  // Only used for testing DevToolsUsage (used by survey).
+  DevToolsUsage? _devToolsUsage;
+
+  File? _devToolsBackup;
+
+  static bool _isValidVmServiceUri(Uri? uri) {
+    // Lots of things are considered valid URIs (including empty strings and
+    // single letters) since they can be relative, so we need to do some extra
+    // checks.
+    return uri != null &&
+        uri.isAbsolute &&
+        (uri.isScheme('ws') ||
+            uri.isScheme('wss') ||
+            uri.isScheme('http') ||
+            uri.isScheme('https'));
+  }
+
+  Future<void> initialize({
+    required String devToolsUrl,
+    required bool headlessMode,
+  }) async {
+    final Stream<Map<String, dynamic>> _stdinCommandStream = stdin
+        .transform<String>(utf8.decoder)
+        .transform<String>(const LineSplitter())
+        .where((String line) => line.startsWith('{') && line.endsWith('}'))
+        .map<Map<String, dynamic>>((String line) {
+      return json.decode(line) as Map<String, dynamic>;
+    });
+
+    // Example input:
+    // {
+    //   "id":0,
+    //   "method":"vm.register",
+    //   "params":{
+    //     "uri":"<vm-service-uri-here>",
+    //   }
+    // }
+    _stdinCommandStream.listen((Map<String, dynamic> json) async {
+      // ID can be String, int or null
+      final dynamic id = json['id'];
+      final Map<String, dynamic> params = json['params'];
+      final method = json['method'];
+      switch (method) {
+        case 'vm.register':
+          await _handleVmRegister(id, params, headlessMode, devToolsUrl);
+          break;
+        case 'devTools.launch':
+          await _handleDevToolsLaunch(id, params, headlessMode, devToolsUrl);
+          break;
+        case 'client.list':
+          _handleClientsList(id, params);
+          break;
+        case 'devTools.survey':
+          _handleDevToolsSurvey(id, params);
+          break;
+        default:
+          DevToolsUtils.printOutput(
+            'Unknown method $method',
+            {
+              'id': id,
+              'error': 'Unknown method $method',
+            },
+            machineMode: machineMode,
+          );
+      }
+    });
+  }
+
+  Future<void> _handleVmRegister(
+    dynamic id,
+    Map<String, dynamic> params,
+    bool headlessMode,
+    String devToolsUrl,
+  ) async {
+    if (!params.containsKey('uri')) {
+      DevToolsUtils.printOutput(
+        'Invalid input: $params does not contain the key \'uri\'',
+        {
+          'id': id,
+          'error': 'Invalid input: $params does not contain the key \'uri\'',
+        },
+        machineMode: machineMode,
+      );
+    }
+
+    // params['uri'] should contain a vm service uri.
+    final uri = Uri.tryParse(params['uri']);
+
+    if (_isValidVmServiceUri(uri)) {
+      await registerLaunchDevToolsService(
+        uri!,
+        id,
+        devToolsUrl,
+        headlessMode,
+      );
+    } else {
+      DevToolsUtils.printOutput(
+        'Uri must be absolute with a http, https, ws or wss scheme',
+        {
+          'id': id,
+          'error': 'Uri must be absolute with a http, https, ws or wss scheme',
+        },
+        machineMode: machineMode,
+      );
+    }
+  }
+
+  Future<void> _handleDevToolsLaunch(
+    dynamic id,
+    Map<String, dynamic> params,
+    bool headlessMode,
+    String devToolsUrl,
+  ) async {
+    if (!params.containsKey('vmServiceUri')) {
+      DevToolsUtils.printOutput(
+        "Invalid input: $params does not contain the key 'vmServiceUri'",
+        {
+          'id': id,
+          'error':
+              "Invalid input: $params does not contain the key e'vmServiceUri'",
+        },
+        machineMode: machineMode,
+      );
+    }
+
+    // params['vmServiceUri'] should contain a vm service uri.
+    final vmServiceUri = Uri.tryParse(params['vmServiceUri'])!;
+
+    if (_isValidVmServiceUri(vmServiceUri)) {
+      try {
+        final result = await server.launchDevTools(
+          params,
+          vmServiceUri,
+          devToolsUrl,
+          headlessMode,
+          machineMode,
+        );
+        DevToolsUtils.printOutput(
+          'DevTools launched',
+          {'id': id, 'result': result},
+          machineMode: machineMode,
+        );
+      } catch (e, s) {
+        DevToolsUtils.printOutput(
+          'Failed to launch browser: $e\n$s',
+          {'id': id, 'error': 'Failed to launch browser: $e\n$s'},
+          machineMode: machineMode,
+        );
+      }
+    } else {
+      DevToolsUtils.printOutput(
+        'VM Service URI must be absolute with a http, https, ws or wss scheme',
+        {
+          'id': id,
+          'error':
+              'VM Service Uri must be absolute with a http, https, ws or wss scheme',
+        },
+        machineMode: machineMode,
+      );
+    }
+  }
+
+  void _handleClientsList(dynamic id, Map<String, dynamic> params) {
+    DevToolsUtils.printOutput(
+      server.clientManager.toString(),
+      server.clientManager.toJson(id),
+      machineMode: machineMode,
+    );
+  }
+
+  void _handleDevToolsSurvey(dynamic id, Map<String, dynamic> params) {
+    _devToolsUsage ??= DevToolsUsage();
+    final String surveyRequest = params['surveyRequest'];
+    final String value = params['value'];
+
+    switch (surveyRequest) {
+      case copyAndCreateDevToolsFile:
+        // Backup and delete ~/.devtools file.
+        if (backupAndCreateDevToolsStore()) {
+          _devToolsUsage = null;
+          DevToolsUtils.printOutput(
+            'DevTools Survey',
+            {
+              'id': id,
+              'result': {
+                // TODO(bkonyi): fix incorrect spelling of "success" here and
+                // below once we figure out the impact of changing this key.
+                'success': true,
+              },
+            },
+            machineMode: machineMode,
+          );
+        }
+        break;
+      case restoreDevToolsFile:
+        _devToolsUsage = null;
+        final content = restoreDevToolsStore();
+        if (content != null) {
+          DevToolsUtils.printOutput(
+            'DevTools Survey',
+            {
+              'id': id,
+              'result': {
+                'success': true,
+                'content': content,
+              },
+            },
+            machineMode: machineMode,
+          );
+
+          _devToolsUsage = null;
+        }
+        break;
+      case apiSetActiveSurvey:
+        _devToolsUsage!.activeSurvey = value;
+        DevToolsUtils.printOutput(
+          'DevTools Survey',
+          {
+            'id': id,
+            'result': {
+              'success': _devToolsUsage!.activeSurvey == value,
+              'activeSurvey': _devToolsUsage!.activeSurvey,
+            },
+          },
+          machineMode: machineMode,
+        );
+        break;
+      case apiGetSurveyActionTaken:
+        DevToolsUtils.printOutput(
+          'DevTools Survey',
+          {
+            'id': id,
+            'result': {
+              'activeSurvey': _devToolsUsage!.activeSurvey,
+              'surveyActionTaken': _devToolsUsage!.surveyActionTaken,
+            },
+          },
+          machineMode: machineMode,
+        );
+        break;
+      case apiSetSurveyActionTaken:
+        _devToolsUsage!.surveyActionTaken = jsonDecode(value);
+        DevToolsUtils.printOutput(
+          'DevTools Survey',
+          {
+            'id': id,
+            'result': {
+              'activeSurvey': _devToolsUsage!.activeSurvey,
+              'surveyActionTaken': _devToolsUsage!.surveyActionTaken,
+            },
+          },
+          machineMode: machineMode,
+        );
+        break;
+      case apiGetSurveyShownCount:
+        DevToolsUtils.printOutput(
+          'DevTools Survey',
+          {
+            'id': id,
+            'result': {
+              'activeSurvey': _devToolsUsage!.activeSurvey,
+              'surveyShownCount': _devToolsUsage!.surveyShownCount,
+            },
+          },
+          machineMode: machineMode,
+        );
+        break;
+      case apiIncrementSurveyShownCount:
+        _devToolsUsage!.incrementSurveyShownCount();
+        DevToolsUtils.printOutput(
+          'DevTools Survey',
+          {
+            'id': id,
+            'result': {
+              'activeSurvey': _devToolsUsage!.activeSurvey,
+              'surveyShownCount': _devToolsUsage!.surveyShownCount,
+            },
+          },
+          machineMode: machineMode,
+        );
+        break;
+      default:
+        DevToolsUtils.printOutput(
+          'Unknown DevTools Survey Request $surveyRequest',
+          {
+            'id': id,
+            'result': {
+              'activeSurvey': _devToolsUsage!.activeSurvey,
+              'surveyActionTaken': _devToolsUsage!.surveyActionTaken,
+              'surveyShownCount': _devToolsUsage!.surveyShownCount,
+            },
+          },
+          machineMode: machineMode,
+        );
+    }
+  }
+
+  bool backupAndCreateDevToolsStore() {
+    assert(_devToolsBackup == null);
+    final devToolsStore = File(
+      LocalFileSystem.devToolsStoreLocation(),
+    );
+    if (devToolsStore.existsSync()) {
+      _devToolsBackup = devToolsStore
+          .copySync('${LocalFileSystem.devToolsDir()}/.devtools_backup_test');
+      devToolsStore.deleteSync();
+    }
+    return true;
+  }
+
+  String? restoreDevToolsStore() {
+    if (_devToolsBackup != null) {
+      // Read the current ~/.devtools file
+      LocalFileSystem.maybeMoveLegacyDevToolsStore();
+
+      final devToolsStore = File(LocalFileSystem.devToolsStoreLocation());
+      final content = devToolsStore.readAsStringSync();
+
+      // Delete the temporary ~/.devtools file
+      devToolsStore.deleteSync();
+      if (_devToolsBackup!.existsSync()) {
+        // Restore the backup ~/.devtools file we created in
+        // backupAndCreateDevToolsStore.
+        _devToolsBackup!.copySync(LocalFileSystem.devToolsStoreLocation());
+        _devToolsBackup!.deleteSync();
+        _devToolsBackup = null;
+      }
+      return content;
+    }
+    return null;
+  }
+
+  Future<void> registerLaunchDevToolsService(
+    Uri vmServiceUri,
+    dynamic id,
+    String devToolsUrl,
+    bool headlessMode,
+  ) async {
+    try {
+      // Connect to the vm service and register a method to launch DevTools in
+      // chrome.
+      final service = await DevToolsUtils.connectToVmService(vmServiceUri);
+      if (service == null) return;
+
+      service.registerServiceCallback(
+        launchDevToolsService,
+        (params) async {
+          try {
+            await server.launchDevTools(
+              params,
+              vmServiceUri,
+              devToolsUrl,
+              headlessMode,
+              machineMode,
+            );
+            return {
+              'result': Success().toJson(),
+            };
+          } catch (e, s) {
+            // Note: It's critical that we return responses in exactly the right format
+            // or the VM will unregister the service. The objects must match JSON-RPC
+            // however a successful response must also have a "type" field in its result.
+            // Otherwise, we can return an error object (instead of result) that includes
+            // code + message.
+            return {
+              'error': {
+                'code': errorLaunchingBrowserCode,
+                'message': 'Failed to launch browser: $e\n$s',
+              },
+            };
+          }
+        },
+      );
+
+      final registerServiceMethodName = 'registerService';
+      await service.callMethod(registerServiceMethodName, args: {
+        'service': launchDevToolsService,
+        'alias': 'DevTools Server',
+      });
+
+      DevToolsUtils.printOutput(
+        'Successfully registered launchDevTools service',
+        {
+          'id': id,
+          'result': {'success': true},
+        },
+        machineMode: machineMode,
+      );
+    } catch (e) {
+      DevToolsUtils.printOutput(
+        'Unable to connect to VM service at $vmServiceUri: $e',
+        {
+          'id': id,
+          'error': 'Unable to connect to VM service at $vmServiceUri: $e',
+        },
+        machineMode: machineMode,
+      );
+    }
+  }
+}
diff --git a/pkg/dds/lib/src/devtools/memory_profile.dart b/pkg/dds/lib/src/devtools/memory_profile.dart
new file mode 100644
index 0000000..f99caf4
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/memory_profile.dart
@@ -0,0 +1,399 @@
+// Copyright (c) 2022, 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.
+
+// TODO(https://github.com/dart-lang/sdk/issues/48161) investigate whether we
+// can delete this file or not.
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:devtools_shared/devtools_shared.dart';
+import 'package:vm_service/vm_service.dart';
+
+import 'service_registrations.dart' as service_registrations;
+
+class MemoryProfile {
+  MemoryProfile(this.service, String profileFilename, this._verboseMode) {
+    onConnectionClosed.listen(_handleConnectionStop);
+
+    service!.onEvent('Service').listen(handleServiceEvent);
+
+    _jsonFile = MemoryJsonFile.create(profileFilename);
+
+    _hookUpEvents();
+  }
+
+  late MemoryJsonFile _jsonFile;
+
+  final bool _verboseMode;
+
+  void _hookUpEvents() async {
+    final streamIds = [
+      EventStreams.kExtension,
+      EventStreams.kGC,
+      EventStreams.kIsolate,
+      EventStreams.kLogging,
+      EventStreams.kStderr,
+      EventStreams.kStdout,
+      // TODO(Kenzi): Collect timeline data too.
+      // EventStreams.kTimeline,
+      EventStreams.kVM,
+      EventStreams.kService,
+    ];
+
+    await Future.wait(streamIds.map((String id) async {
+      try {
+        await service!.streamListen(id);
+      } catch (e) {
+        if (id.endsWith('Logging')) {
+          // Don't complain about '_Logging' or 'Logging' events (new VMs don't
+          // have the private names, and older ones don't have the public ones).
+        } else {
+          print("Service client stream not supported: '$id'\n  $e");
+        }
+      }
+    }));
+  }
+
+  bool get hasConnection => service != null;
+
+  void handleServiceEvent(Event e) {
+    if (e.kind == EventKind.kServiceRegistered) {
+      final serviceName = e.service!;
+      _registeredMethodsForService
+          .putIfAbsent(serviceName, () => [])
+          .add(e.method!);
+    }
+
+    if (e.kind == EventKind.kServiceUnregistered) {
+      final serviceName = e.service!;
+      _registeredMethodsForService.remove(serviceName);
+    }
+  }
+
+  late IsolateRef _selectedIsolate;
+
+  Future<Response?> getAdbMemoryInfo() async {
+    return await callService(
+      service_registrations.flutterMemory.service,
+      isolateId: _selectedIsolate.id,
+    );
+  }
+
+  /// Call a service that is registered by exactly one client.
+  Future<Response?> callService(
+    String name, {
+    String? isolateId,
+    Map<String, dynamic>? args,
+  }) async {
+    final registered = _registeredMethodsForService[name] ?? const [];
+    if (registered.isEmpty) {
+      throw Exception('There are no registered methods for service "$name"');
+    }
+    return service!.callMethod(
+      registered.first,
+      isolateId: isolateId,
+      args: args,
+    );
+  }
+
+  Map<String, List<String>> get registeredMethodsForService =>
+      _registeredMethodsForService;
+  final _registeredMethodsForService = <String, List<String>>{};
+
+  static const Duration updateDelay = Duration(milliseconds: 500);
+
+  VmService? service;
+
+  late Timer _pollingTimer;
+
+  /// Polled VM current RSS.
+  int? processRss;
+
+  final Map<String, List<HeapSpace>> isolateHeaps = <String, List<HeapSpace>>{};
+
+  final List<HeapSample> samples = <HeapSample>[];
+
+  AdbMemoryInfo? adbMemoryInfo;
+
+  late EventSample eventSample;
+
+  RasterCache? rasterCache;
+
+  late int heapMax;
+
+  Stream<void> get onConnectionClosed => _connectionClosedController.stream;
+  final _connectionClosedController = StreamController<void>.broadcast();
+
+  void _handleConnectionStop(dynamic event) {
+    // TODO(terry): Gracefully handle connection loss.
+  }
+
+  // TODO(terry): Investigate moving code from this point through end of class to devtools_shared.
+  void startPolling() {
+    _pollingTimer = Timer(updateDelay, _pollMemory);
+    service!.onGCEvent.listen(_handleGCEvent);
+  }
+
+  void _handleGCEvent(Event event) {
+    //final bool ignore = event.json['reason'] == 'compact';
+    final json = event.json!;
+    final List<HeapSpace> heaps = <HeapSpace>[
+      HeapSpace.parse(json['new'])!,
+      HeapSpace.parse(json['old'])!
+    ];
+    _updateGCEvent(event.isolate!.id!, heaps);
+    // TODO(terry): expose when GC occured as markers in memory timeline.
+  }
+
+  void stopPolling() {
+    _pollingTimer.cancel();
+    service = null;
+  }
+
+  Future<void> _pollMemory() async {
+    final service = this.service!;
+    final VM vm = await service.getVM();
+
+    // TODO(terry): Need to handle a possible Sentinel being returned.
+    final List<Isolate?> isolates =
+        await Future.wait(vm.isolates!.map((IsolateRef ref) async {
+      try {
+        return await service.getIsolate(ref.id!);
+      } catch (e) {
+        // TODO(terry): Seem to sometimes get a sentinel not sure how? VM issue?
+        // Unhandled Exception: type 'Sentinel' is not a subtype of type 'FutureOr<Isolate>'
+        print('Error [MEMORY_PROTOCOL]: $e');
+        return Future<Isolate?>.value();
+      }
+    }));
+
+    // Polls for current Android meminfo using:
+    //    > adb shell dumpsys meminfo -d <package_name>
+    final isolate = isolates[0]!;
+    _selectedIsolate = IsolateRef(
+      id: isolate.id,
+      name: isolate.name,
+      number: isolate.number,
+      isSystemIsolate: isolate.isSystemIsolate,
+    );
+
+    if (hasConnection && vm.operatingSystem == 'android') {
+      // Poll ADB meminfo
+      adbMemoryInfo = await _fetchAdbInfo();
+    } else {
+      // TODO(terry): TBD alternative for iOS memory info - all values zero.
+      adbMemoryInfo = AdbMemoryInfo.empty();
+    }
+
+    // Query the engine's rasterCache estimate.
+    rasterCache = await _fetchRasterCacheInfo(_selectedIsolate);
+
+    // TODO(terry): There are no user interactions.  However, might be nice to
+    //              record VM GC's on the timeline.
+    eventSample = EventSample.empty();
+
+    // Polls for current RSS size.
+    _update(vm, isolates);
+
+    _pollingTimer = Timer(updateDelay, _pollMemory);
+  }
+
+  /// Poll ADB meminfo
+  Future<AdbMemoryInfo?> _fetchAdbInfo() async {
+    final adbMemInfo = await getAdbMemoryInfo();
+    if (adbMemInfo?.json != null) {
+      return AdbMemoryInfo.fromJsonInKB(adbMemInfo!.json!);
+    }
+    return null;
+  }
+
+  /// Poll Fultter engine's Raster Cache metrics.
+  /// @returns engine's rasterCache estimates or null.
+  Future<RasterCache?> _fetchRasterCacheInfo(IsolateRef selectedIsolate) async {
+    final response = await getRasterCacheMetrics(selectedIsolate);
+    return RasterCache.parse(response?.json);
+  }
+
+  /// @returns view id of selected isolate's 'FlutterView'.
+  /// @throws Exception if no 'FlutterView'.
+  Future<String?> getFlutterViewId(IsolateRef selectedIsolate) async {
+    final flutterViewListResponse = await service!.callServiceExtension(
+      service_registrations.flutterListViews,
+      isolateId: selectedIsolate.id,
+    );
+    final List<dynamic> views =
+        flutterViewListResponse.json!['views'].cast<Map<String, dynamic>>();
+
+    // Each isolate should only have one FlutterView.
+    final flutterView = views.firstWhere(
+      (view) => view['type'] == 'FlutterView',
+      orElse: () => null,
+    );
+
+    if (flutterView == null) {
+      final message =
+          'No Flutter Views to query: ${flutterViewListResponse.json}';
+      print('ERROR: $message');
+      throw Exception(message);
+    }
+
+    final String flutterViewId = flutterView['id']!;
+    return flutterViewId;
+  }
+
+  /// Flutter engine returns estimate how much memory is used by layer/picture raster
+  /// cache entries in bytes.
+  ///
+  /// Call to returns JSON payload 'EstimateRasterCacheMemory' with two entries:
+  ///   layerBytes - layer raster cache entries in bytes
+  ///   pictureBytes - picture raster cache entries in bytes
+  Future<Response?> getRasterCacheMetrics(IsolateRef selectedIsolate) async {
+    final viewId = await getFlutterViewId(selectedIsolate);
+
+    return await service!.callServiceExtension(
+      service_registrations.flutterEngineRasterCache,
+      args: {'viewId': viewId},
+      isolateId: selectedIsolate.id,
+    );
+  }
+
+  void _update(VM vm, List<Isolate?> isolates) {
+    processRss = vm.json!['_currentRSS'];
+
+    isolateHeaps.clear();
+
+    for (Isolate? isolate in isolates) {
+      if (isolate != null) {
+        isolateHeaps[isolate.id!] = getHeaps(isolate);
+      }
+    }
+
+    _recalculate();
+  }
+
+  void _updateGCEvent(String id, List<HeapSpace> heaps) {
+    isolateHeaps[id] = heaps;
+    _recalculate(true);
+  }
+
+  void _recalculate([bool fromGC = false]) {
+    int total = 0;
+
+    int used = 0;
+    int capacity = 0;
+    int external = 0;
+    for (List<HeapSpace> heaps in isolateHeaps.values) {
+      used += heaps.fold<int>(0, (i, heap) => i + heap.used!);
+      capacity += heaps.fold<int>(0, (i, heap) => i + heap.capacity!);
+      external += heaps.fold<int>(0, (i, heap) => i + heap.external!);
+
+      capacity += external;
+
+      total +=
+          heaps.fold<int>(0, (i, heap) => i + heap.capacity! + heap.external!);
+    }
+
+    heapMax = total;
+
+    final time = DateTime.now().millisecondsSinceEpoch;
+    final sample = HeapSample(
+      time,
+      processRss ?? -1,
+      capacity,
+      used,
+      external,
+      fromGC,
+      adbMemoryInfo,
+      eventSample,
+      rasterCache,
+    );
+
+    if (_verboseMode) {
+      final timeCollected = _formatTime(
+        DateTime.fromMillisecondsSinceEpoch(time),
+      );
+
+      print(' Collected Sample: [$timeCollected] capacity=$capacity, '
+          'ADB MemoryInfo total=${adbMemoryInfo!.total}${fromGC ? ' [GC]' : ''}');
+    }
+
+    _jsonFile.writeSample(sample);
+  }
+
+  static List<HeapSpace> getHeaps(Isolate isolate) {
+    final Map<String, dynamic> heaps = isolate.json!['_heaps'];
+    final heapList = <HeapSpace>[];
+    for (final heapJson in heaps.values) {
+      final heap = HeapSpace.parse(heapJson);
+      if (heap != null) {
+        heapList.add(heap);
+      }
+    }
+    return heapList;
+  }
+
+  static String _formatTime(DateTime value) {
+    String toStringLength(int value, int length) {
+      final result = '$value';
+      assert(length >= result.length);
+      return '0' * (length - result.length) + result;
+    }
+
+    return toStringLength(value.hour, 2) +
+        ':' +
+        toStringLength(value.minute, 2) +
+        ':' +
+        toStringLength(value.second, 2) +
+        '.' +
+        toStringLength(value.millisecond, 3);
+  }
+}
+
+class MemoryJsonFile {
+  MemoryJsonFile.create(this._absoluteFileName) {
+    _open();
+  }
+
+  final String _absoluteFileName;
+  late io.File _fs;
+  late io.RandomAccessFile _raFile;
+  bool _multipleSamples = false;
+
+  void _open() {
+    _fs = io.File(_absoluteFileName);
+    _raFile = _fs.openSync(mode: io.FileMode.writeOnly);
+
+    _populateJsonHeader();
+  }
+
+  void _populateJsonHeader() {
+    final payload = '${SamplesMemoryJson.header}${MemoryJson.trailer}';
+    _raFile.writeStringSync(payload);
+    _raFile.flushSync();
+  }
+
+  void _setPositionToWriteSample() {
+    // Set the file position to the data array field contents - inside of [].
+    final filePosition = _raFile.positionSync();
+    _raFile.setPositionSync(filePosition - MemoryJson.trailer.length);
+  }
+
+  void writeSample(HeapSample sample) {
+    _setPositionToWriteSample();
+
+    String encodedSample;
+    if (_multipleSamples) {
+      encodedSample = SamplesMemoryJson().encodeAnother(sample);
+    } else {
+      encodedSample = SamplesMemoryJson().encode(sample);
+    }
+
+    _raFile.writeStringSync('$encodedSample${MemoryJson.trailer}');
+
+    _raFile.flushSync();
+
+    _multipleSamples = true;
+  }
+}
diff --git a/pkg/dds/lib/src/devtools/service_registrations.dart b/pkg/dds/lib/src/devtools/service_registrations.dart
new file mode 100644
index 0000000..10b35ea
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/service_registrations.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2022, 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.
+
+// TODO(https://github.com/flutter/devtools/issues/3571): move to devtools_shared.
+class RegisteredServiceDescription {
+  const RegisteredServiceDescription._({
+    required this.service,
+    required this.title,
+  });
+
+  final String service;
+  final String title;
+}
+
+/// Flutter memory service registered by Flutter Tools.
+///
+/// We call this service to get version information about the Flutter Android memory info
+/// using Android's ADB.
+const flutterMemory = RegisteredServiceDescription._(
+  service: 'flutterMemoryInfo',
+  title: 'Flutter Memory Info',
+);
+
+const flutterListViews = '_flutter.listViews';
+
+/// Flutter engine returns estimate how much memory is used by layer/picture raster
+/// cache entries in bytes.
+const flutterEngineRasterCache = '_flutter.estimateRasterCacheMemory';
diff --git a/pkg/dds/lib/src/devtools/utils.dart b/pkg/dds/lib/src/devtools/utils.dart
new file mode 100644
index 0000000..b4c732e
--- /dev/null
+++ b/pkg/dds/lib/src/devtools/utils.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2022, 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:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+import 'package:vm_service/utils.dart';
+import 'package:vm_service/vm_service.dart';
+
+abstract class DevToolsUtils {
+  static void printOutput(
+    String? message,
+    Object json, {
+    required bool machineMode,
+  }) {
+    final output = machineMode ? jsonEncode(json) : message;
+    if (output != null) {
+      print(output);
+    }
+  }
+
+  static Future<VmService?> connectToVmService(Uri theUri) async {
+    // Fix up the various acceptable URI formats into a WebSocket URI to connect.
+    final uri = convertToWebSocketUrl(serviceProtocolUrl: theUri);
+
+    try {
+      final WebSocket ws = await WebSocket.connect(uri.toString());
+
+      final VmService service = VmService(
+        ws.asBroadcastStream(),
+        (String message) => ws.add(message),
+      );
+
+      return service;
+    } catch (_) {
+      print('ERROR: Unable to connect to VMService $theUri');
+      return null;
+    }
+  }
+
+  static Future<String> getVersion(String devToolsDir) async {
+    try {
+      final versionFile = File(path.join(devToolsDir, 'version.json'));
+      final decoded = jsonDecode(await versionFile.readAsString());
+      return decoded['version'] ?? 'unknown';
+    } on FileSystemException {
+      return 'unknown';
+    }
+  }
+}
diff --git a/pkg/dds/lib/src/utils/console.dart b/pkg/dds/lib/src/utils/console.dart
new file mode 100644
index 0000000..10c302b
--- /dev/null
+++ b/pkg/dds/lib/src/utils/console.dart
@@ -0,0 +1,12 @@
+// Copyright (c) 2022, 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.
+
+abstract class ConsoleUtils {
+  /// Make [contents] bold when printed to the terminal.
+  ///
+  /// This is a no-op on Windows.
+  static String bold(String contents) {
+    return '\u001b[1m$contents\u001b[0m';
+  }
+}
diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml
index 51e3d8d..22e251c 100644
--- a/pkg/dds/pubspec.yaml
+++ b/pkg/dds/pubspec.yaml
@@ -3,7 +3,7 @@
   A library used to spawn the Dart Developer Service, used to communicate with
   a Dart VM Service instance.
 
-version: 2.1.7
+version: 2.2.0
 
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/dds
 
@@ -12,9 +12,11 @@
 
 dependencies:
   async: ^2.4.1
+  browser_launcher: ^1.0.0
   collection: ^1.15.0
   dds_service_extensions: ^1.3.0
   devtools_shared: ^2.3.0
+  http_multi_server: ^3.0.0
   json_rpc_2: ^3.0.0
   meta: ^1.1.8
   path: ^1.8.0
diff --git a/pkg/dds/test/devtools_server/devtools_client_test.dart b/pkg/dds/test/devtools_server/devtools_client_test.dart
new file mode 100644
index 0000000..4a163b0
--- /dev/null
+++ b/pkg/dds/test/devtools_server/devtools_client_test.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2022, 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:dds/src/devtools/client.dart';
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late json_rpc.Peer client;
+  late DevToolsClient devToolsClient;
+  setUp(() {
+    final requestController = StreamController<String>();
+    final requestStream = requestController.stream;
+    final requestSink = requestController.sink;
+
+    final responseController = StreamController<String>();
+    final responseStream = responseController.stream;
+    final responseSink = responseController.sink;
+    client = json_rpc.Peer(StreamChannel(responseStream, requestSink));
+    unawaited(client.listen());
+
+    devToolsClient = DevToolsClient(
+      stream: requestStream,
+      sink: responseSink,
+    );
+  });
+
+  test('DevToolsClient API', () async {
+    var response = await client.sendRequest('connected', {
+      'uri': 'http://127.0.0.1:8181',
+    });
+    expect(response, isNull);
+    expect(devToolsClient.hasConnection, true);
+    expect(devToolsClient.vmServiceUri, Uri.parse('http://127.0.0.1:8181'));
+    expect(devToolsClient.embedded, false);
+    expect(devToolsClient.currentPage, isNull);
+
+    response = await client.sendRequest('disconnected');
+    expect(response, isNull);
+    expect(devToolsClient.hasConnection, false);
+    expect(devToolsClient.vmServiceUri, isNull);
+    expect(devToolsClient.embedded, false);
+    expect(devToolsClient.currentPage, isNull);
+
+    response = await client.sendRequest('currentPage', {
+      'id': 'foo',
+      'embedded': true,
+    });
+
+    expect(response, isNull);
+    expect(devToolsClient.hasConnection, false);
+    expect(devToolsClient.vmServiceUri, isNull);
+    expect(devToolsClient.embedded, true);
+    expect(devToolsClient.currentPage, 'foo');
+
+    // TODO: add tests for package:devtools_shared/devtools_server.dart
+  });
+
+  test('DevToolsClient notifications', () async {
+    final enableNotifications = Completer<void>();
+    client.registerMethod(
+      'enableNotifications',
+      (_) => enableNotifications.complete(),
+    );
+    devToolsClient.enableNotifications();
+    await enableNotifications.future;
+
+    final showPage = Completer<void>();
+    String? pageId;
+    client.registerMethod('showPage', (parameters) {
+      pageId = parameters['page'].asString;
+      showPage.complete();
+    });
+    devToolsClient.showPage('foo');
+    await showPage.future;
+    expect(pageId, 'foo');
+
+    final connectToVmService = Completer<void>();
+    String? uri;
+    bool notifyUser = false;
+    client.registerMethod('connectToVm', (parameters) {
+      uri = parameters['uri'].asString;
+      notifyUser = parameters['notify'].asBool;
+      connectToVmService.complete();
+    });
+    devToolsClient.connectToVmService(Uri.parse('http://127.0.0.1:8181'), true);
+    await connectToVmService.future;
+    expect(uri, 'http://127.0.0.1:8181');
+    expect(notifyUser, true);
+
+    final notify = Completer<void>();
+    client.registerMethod('notify', (_) => notify.complete());
+    devToolsClient.notify();
+    await notify.future;
+  });
+}
diff --git a/pkg/front_end/testcases/strong.status b/pkg/front_end/testcases/strong.status
index de5dad0..f57bbdf 100644
--- a/pkg/front_end/testcases/strong.status
+++ b/pkg/front_end/testcases/strong.status
@@ -15,7 +15,6 @@
 
 constructor_tearoffs/call_instantiation: TypeCheckError
 constructor_tearoffs/lowering/invalid_redirect: VerificationError
-enhanced_enums/enum_as_supertype: RuntimeError
 enhanced_enums/simple_mixins: RuntimeError
 extension_types/access_setter_as_getter: ExpectationFileMismatchSerialized # Expected.
 extension_types/call_not_get: ExpectationFileMismatchSerialized # Expected.
diff --git a/pkg/front_end/testcases/text_serialization.status b/pkg/front_end/testcases/text_serialization.status
index 3630e3a..5a838fa 100644
--- a/pkg/front_end/testcases/text_serialization.status
+++ b/pkg/front_end/testcases/text_serialization.status
@@ -8,7 +8,6 @@
 
 constructor_tearoffs/call_instantiation: TypeCheckError
 constructor_tearoffs/lowering/invalid_redirect: VerificationError
-enhanced_enums/enum_as_supertype: RuntimeError
 enhanced_enums/simple_mixins: RuntimeError
 extension_types/access_setter_as_getter: ExpectationFileMismatchSerialized # Expected.
 extension_types/call_not_get: ExpectationFileMismatchSerialized # Expected.
diff --git a/pkg/front_end/testcases/weak.status b/pkg/front_end/testcases/weak.status
index c71436c..f75594a 100644
--- a/pkg/front_end/testcases/weak.status
+++ b/pkg/front_end/testcases/weak.status
@@ -22,7 +22,6 @@
 
 constructor_tearoffs/call_instantiation: TypeCheckError
 constructor_tearoffs/lowering/invalid_redirect: VerificationError
-enhanced_enums/enum_as_supertype: RuntimeError
 enhanced_enums/simple_mixins: RuntimeError
 extension_types/access_setter_as_getter: ExpectationFileMismatchSerialized # Expected.
 extension_types/call_not_get: ExpectationFileMismatchSerialized # Expected.
diff --git a/runtime/include/dart_api.h b/runtime/include/dart_api.h
index 0ac05bf..61c56f6 100644
--- a/runtime/include/dart_api.h
+++ b/runtime/include/dart_api.h
@@ -1252,18 +1252,6 @@
 DART_EXPORT void Dart_KillIsolate(Dart_Isolate isolate);
 
 /**
- * Notifies the VM that the embedder expects |size| bytes of memory have become
- * unreachable. The VM may use this hint to adjust the garbage collector's
- * growth policy.
- *
- * Multiple calls are interpreted as increasing, not replacing, the estimate of
- * unreachable memory.
- *
- * Requires there to be a current isolate.
- */
-DART_EXPORT void Dart_HintFreed(intptr_t size);
-
-/**
  * Notifies the VM that the embedder expects to be idle until |deadline|. The VM
  * may use this time to perform garbage collection or other tasks to avoid
  * delays during execution of Dart code in the future.
diff --git a/runtime/vm/class_finalizer.cc b/runtime/vm/class_finalizer.cc
index 9326873..587b538 100644
--- a/runtime/vm/class_finalizer.cc
+++ b/runtime/vm/class_finalizer.cc
@@ -1223,6 +1223,7 @@
 void ClassFinalizer::AllocateEnumValues(const Class& enum_cls) {
   Thread* thread = Thread::Current();
   Zone* zone = thread->zone();
+  ObjectStore* object_store = thread->isolate_group()->object_store();
 
   const auto& values_field =
       Field::Handle(zone, enum_cls.LookupStaticField(Symbols::Values()));
@@ -1237,16 +1238,11 @@
     ASSERT(values.IsArray());
   }
 
-  // The enum_cls is the actual declared class.
-  // The shared super-class holds the fields for index and name.
-  const auto& super_cls = Class::Handle(zone, enum_cls.SuperClass());
-
   const auto& index_field =
-      Field::Handle(zone, super_cls.LookupInstanceField(Symbols::Index()));
+      Field::Handle(zone, object_store->enum_index_field());
   ASSERT(!index_field.IsNull());
 
-  const auto& name_field = Field::Handle(
-      zone, super_cls.LookupInstanceFieldAllowPrivate(Symbols::_name()));
+  const auto& name_field = Field::Handle(zone, object_store->enum_name_field());
   ASSERT(!name_field.IsNull());
 
   const auto& enum_name = String::Handle(zone, enum_cls.ScrubbedName());
diff --git a/runtime/vm/compiler/runtime_offsets_extracted.h b/runtime/vm/compiler/runtime_offsets_extracted.h
index 4784b36..fccacc2 100644
--- a/runtime/vm/compiler/runtime_offsets_extracted.h
+++ b/runtime/vm/compiler/runtime_offsets_extracted.h
@@ -219,11 +219,12 @@
     12;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    152;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 108;
+    160;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 116;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    172;
-static constexpr dart::compiler::target::word ObjectStore_type_type_offset = 96;
+    180;
+static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
+    104;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 12;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 4;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset = 8;
@@ -784,12 +785,12 @@
     24;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    304;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 216;
+    320;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 232;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    344;
+    360;
 static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 16;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 8;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset =
@@ -1355,11 +1356,12 @@
     12;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    152;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 108;
+    160;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 116;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    172;
-static constexpr dart::compiler::target::word ObjectStore_type_type_offset = 96;
+    180;
+static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
+    104;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 12;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 4;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset = 8;
@@ -1917,12 +1919,12 @@
     24;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    304;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 216;
+    320;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 232;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    344;
+    360;
 static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 16;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 8;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset =
@@ -2489,12 +2491,12 @@
     24;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    304;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 216;
+    320;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 232;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    344;
+    360;
 static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 16;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 8;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset =
@@ -3060,12 +3062,12 @@
     24;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    304;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 216;
+    320;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 232;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    344;
+    360;
 static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 16;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 8;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset =
@@ -3628,11 +3630,12 @@
     12;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    152;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 108;
+    160;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 116;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    172;
-static constexpr dart::compiler::target::word ObjectStore_type_type_offset = 96;
+    180;
+static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
+    104;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 12;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 4;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset = 8;
@@ -4187,12 +4190,12 @@
     24;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    304;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 216;
+    320;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 232;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    344;
+    360;
 static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 16;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 8;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset =
@@ -4752,11 +4755,12 @@
     12;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    152;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 108;
+    160;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 116;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    172;
-static constexpr dart::compiler::target::word ObjectStore_type_type_offset = 96;
+    180;
+static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
+    104;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 12;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 4;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset = 8;
@@ -5308,12 +5312,12 @@
     24;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    304;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 216;
+    320;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 232;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    344;
+    360;
 static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 16;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 8;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset =
@@ -5874,12 +5878,12 @@
     24;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    304;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 216;
+    320;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 232;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    344;
+    360;
 static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 16;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 8;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset =
@@ -6439,12 +6443,12 @@
     24;
 static constexpr dart::compiler::target::word NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word ObjectStore_double_type_offset =
-    304;
-static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 216;
+    320;
+static constexpr dart::compiler::target::word ObjectStore_int_type_offset = 232;
 static constexpr dart::compiler::target::word ObjectStore_string_type_offset =
-    344;
+    360;
 static constexpr dart::compiler::target::word ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word OneByteString_data_offset = 16;
 static constexpr dart::compiler::target::word PointerBase_data_field_offset = 8;
 static constexpr dart::compiler::target::word Pointer_type_arguments_offset =
@@ -7037,13 +7041,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 152;
+    AOT_ObjectStore_double_type_offset = 160;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    108;
+    116;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 172;
+    AOT_ObjectStore_string_type_offset = 180;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    96;
+    104;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     12;
 static constexpr dart::compiler::target::word
@@ -7669,13 +7673,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 304;
+    AOT_ObjectStore_double_type_offset = 320;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    216;
+    232;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 344;
+    AOT_ObjectStore_string_type_offset = 360;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     16;
 static constexpr dart::compiler::target::word
@@ -8307,13 +8311,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 304;
+    AOT_ObjectStore_double_type_offset = 320;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    216;
+    232;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 344;
+    AOT_ObjectStore_string_type_offset = 360;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     16;
 static constexpr dart::compiler::target::word
@@ -8942,13 +8946,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 304;
+    AOT_ObjectStore_double_type_offset = 320;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    216;
+    232;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 344;
+    AOT_ObjectStore_string_type_offset = 360;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     16;
 static constexpr dart::compiler::target::word
@@ -9576,13 +9580,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 304;
+    AOT_ObjectStore_double_type_offset = 320;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    216;
+    232;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 344;
+    AOT_ObjectStore_string_type_offset = 360;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     16;
 static constexpr dart::compiler::target::word
@@ -10206,13 +10210,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 152;
+    AOT_ObjectStore_double_type_offset = 160;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    108;
+    116;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 172;
+    AOT_ObjectStore_string_type_offset = 180;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    96;
+    104;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     12;
 static constexpr dart::compiler::target::word
@@ -10831,13 +10835,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 304;
+    AOT_ObjectStore_double_type_offset = 320;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    216;
+    232;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 344;
+    AOT_ObjectStore_string_type_offset = 360;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     16;
 static constexpr dart::compiler::target::word
@@ -11462,13 +11466,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 304;
+    AOT_ObjectStore_double_type_offset = 320;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    216;
+    232;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 344;
+    AOT_ObjectStore_string_type_offset = 360;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     16;
 static constexpr dart::compiler::target::word
@@ -12090,13 +12094,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 304;
+    AOT_ObjectStore_double_type_offset = 320;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    216;
+    232;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 344;
+    AOT_ObjectStore_string_type_offset = 360;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     16;
 static constexpr dart::compiler::target::word
@@ -12717,13 +12721,13 @@
 static constexpr dart::compiler::target::word
     AOT_NativeArguments_thread_offset = 0;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_double_type_offset = 304;
+    AOT_ObjectStore_double_type_offset = 320;
 static constexpr dart::compiler::target::word AOT_ObjectStore_int_type_offset =
-    216;
+    232;
 static constexpr dart::compiler::target::word
-    AOT_ObjectStore_string_type_offset = 344;
+    AOT_ObjectStore_string_type_offset = 360;
 static constexpr dart::compiler::target::word AOT_ObjectStore_type_type_offset =
-    192;
+    208;
 static constexpr dart::compiler::target::word AOT_OneByteString_data_offset =
     16;
 static constexpr dart::compiler::target::word
diff --git a/runtime/vm/dart_api_impl.cc b/runtime/vm/dart_api_impl.cc
index 8b8468d..7106d5b 100644
--- a/runtime/vm/dart_api_impl.cc
+++ b/runtime/vm/dart_api_impl.cc
@@ -1811,17 +1811,6 @@
   return Api::NewHandle(T, I->sticky_error());
 }
 
-DART_EXPORT void Dart_HintFreed(intptr_t size) {
-  if (size < 0) {
-    FATAL1("%s requires a non-negative size", CURRENT_FUNC);
-  }
-  Thread* T = Thread::Current();
-  CHECK_ISOLATE(T->isolate());
-  API_TIMELINE_BEGIN_END(T);
-  TransitionNativeToVM transition(T);
-  T->heap()->HintFreed(size);
-}
-
 DART_EXPORT void Dart_NotifyIdle(int64_t deadline) {
   Thread* T = Thread::Current();
   CHECK_ISOLATE(T->isolate());
diff --git a/runtime/vm/dart_api_impl_test.cc b/runtime/vm/dart_api_impl_test.cc
index c9851a6..5691670 100644
--- a/runtime/vm/dart_api_impl_test.cc
+++ b/runtime/vm/dart_api_impl_test.cc
@@ -9525,39 +9525,6 @@
   }
 }
 
-static void HintFreedNative(Dart_NativeArguments args) {
-  int64_t size = 0;
-  EXPECT_VALID(Dart_GetNativeIntegerArgument(args, 0, &size));
-  Dart_HintFreed(size);
-}
-
-static Dart_NativeFunction HintFreed_native_lookup(Dart_Handle name,
-                                                   int argument_count,
-                                                   bool* auto_setup_scope) {
-  return HintFreedNative;
-}
-
-TEST_CASE(DartAPI_HintFreed) {
-  const char* kScriptChars = R"(
-@pragma("vm:external-name", "Test_nativeFunc")
-external void hintFreed(int size);
-void main() {
-  var v;
-  for (var i = 0; i < 100; i++) {
-    var t = [];
-    for (var j = 0; j < 10000; j++) {
-      t.add(List.filled(100, null));
-    }
-    v = t;
-    hintFreed(100 * 10000 * 4);
-  }
-})";
-  Dart_Handle lib =
-      TestCase::LoadTestScript(kScriptChars, &HintFreed_native_lookup);
-  Dart_Handle result = Dart_Invoke(lib, NewString("main"), 0, NULL);
-  EXPECT_VALID(result);
-}
-
 static void NotifyIdleShortNative(Dart_NativeArguments args) {
   Dart_NotifyIdle(Dart_TimelineGetMicros() + 10 * kMicrosecondsPerMillisecond);
 }
diff --git a/runtime/vm/heap/heap.cc b/runtime/vm/heap/heap.cc
index fe3956d..b226659 100644
--- a/runtime/vm/heap/heap.cc
+++ b/runtime/vm/heap/heap.cc
@@ -356,10 +356,6 @@
   return raw_obj;
 }
 
-void Heap::HintFreed(intptr_t size) {
-  old_space_.HintFreed(size);
-}
-
 void Heap::NotifyIdle(int64_t deadline) {
   Thread* thread = Thread::Current();
   TIMELINE_FUNCTION_GC_DURATION(thread, "NotifyIdle");
diff --git a/runtime/vm/heap/heap.h b/runtime/vm/heap/heap.h
index 2360700..121e075 100644
--- a/runtime/vm/heap/heap.h
+++ b/runtime/vm/heap/heap.h
@@ -104,7 +104,6 @@
   ObjectPtr FindNewObject(FindObjectVisitor* visitor);
   ObjectPtr FindObject(FindObjectVisitor* visitor);
 
-  void HintFreed(intptr_t size);
   void NotifyIdle(int64_t deadline);
   void NotifyLowMemory();
 
diff --git a/runtime/vm/heap/pages.cc b/runtime/vm/heap/pages.cc
index f17e8e6..39b1603 100644
--- a/runtime/vm/heap/pages.cc
+++ b/runtime/vm/heap/pages.cc
@@ -1692,17 +1692,6 @@
   }
 }
 
-void PageSpaceController::HintFreed(intptr_t size) {
-  intptr_t size_in_words = size << kWordSizeLog2;
-  if (size_in_words > idle_gc_threshold_in_words_) {
-    idle_gc_threshold_in_words_ = 0;
-  } else {
-    idle_gc_threshold_in_words_ -= size_in_words;
-  }
-
-  // TODO(rmacnak): Hasten the soft threshold at some discount?
-}
-
 void PageSpaceGarbageCollectionHistory::AddGarbageCollectionTime(int64_t start,
                                                                  int64_t end) {
   Entry entry;
diff --git a/runtime/vm/heap/pages.h b/runtime/vm/heap/pages.h
index 9d039e1..b8e209b 100644
--- a/runtime/vm/heap/pages.h
+++ b/runtime/vm/heap/pages.h
@@ -265,7 +265,6 @@
                                  int64_t start,
                                  int64_t end);
   void EvaluateAfterLoading(SpaceUsage after);
-  void HintFreed(intptr_t size);
 
   void set_last_usage(SpaceUsage current) { last_usage_ = current; }
 
@@ -359,7 +358,6 @@
   void EvaluateAfterLoading() {
     page_space_controller_.EvaluateAfterLoading(usage_);
   }
-  void HintFreed(intptr_t size) { page_space_controller_.HintFreed(size); }
 
   int64_t UsedInWords() const { return usage_.used_in_words; }
   int64_t CapacityInWords() const {
diff --git a/runtime/vm/object_reload.cc b/runtime/vm/object_reload.cc
index 9fdbffd..87874b8 100644
--- a/runtime/vm/object_reload.cc
+++ b/runtime/vm/object_reload.cc
@@ -302,10 +302,11 @@
   ASSERT(is_finalized());
   ASSERT(old_enum.is_finalized());
 
-  Zone* zone = Thread::Current()->zone();
+  Thread* thread = Thread::Current();
+  Zone* zone = thread->zone();
+  ObjectStore* object_store = thread->isolate_group()->object_store();
 
   Field& field = Field::Handle(zone);
-  Class& cls = Class::Handle(zone);
   String& enum_ident = String::Handle();
   Instance& old_enum_value = Instance::Handle(zone);
   Instance& enum_value = Instance::Handle(zone);
@@ -338,8 +339,7 @@
     old_deleted_enum_sentinel ^= field.StaticConstFieldValue();
     ASSERT(!old_deleted_enum_sentinel.IsNull());
 
-    cls = old_enum.SuperClass();
-    field = cls.LookupInstanceFieldAllowPrivate(Symbols::_name());
+    field = object_store->enum_name_field();
     ASSERT(!field.IsNull());
 
     UnorderedHashMap<EnumMapTraits> enum_map(enum_map_storage.ptr());
@@ -374,8 +374,7 @@
     deleted_enum_sentinel ^= field.StaticConstFieldValue();
     ASSERT(!deleted_enum_sentinel.IsNull());
 
-    cls = SuperClass();
-    field = cls.LookupInstanceFieldAllowPrivate(Symbols::_name());
+    field = object_store->enum_name_field();
     ASSERT(!field.IsNull());
 
     UnorderedHashMap<EnumMapTraits> enum_map(enum_map_storage.ptr());
diff --git a/runtime/vm/object_store.cc b/runtime/vm/object_store.cc
index b7c1cb3..8ce45e3 100644
--- a/runtime/vm/object_store.cc
+++ b/runtime/vm/object_store.cc
@@ -361,6 +361,8 @@
   if (list_class_.load() == Type::null()) {
     ASSERT(non_nullable_list_rare_type_.load() == Type::null());
     ASSERT(non_nullable_map_rare_type_.load() == Type::null());
+    ASSERT(enum_index_field_.load() == Field::null());
+    ASSERT(enum_name_field_.load() == Field::null());
     ASSERT(_object_equals_function_.load() == Function::null());
     ASSERT(_object_hash_code_function_.load() == Function::null());
     ASSERT(_object_to_string_function_.load() == Function::null());
@@ -382,6 +384,21 @@
     type ^= cls.RareType();
     non_nullable_map_rare_type_.store(type.ptr());
 
+    auto& field = Field::Handle(zone);
+
+    cls = core_lib.LookupClassAllowPrivate(Symbols::_Enum());
+    ASSERT(!cls.IsNull());
+    const auto& error = cls.EnsureIsFinalized(thread);
+    ASSERT(error == Error::null());
+
+    field = cls.LookupInstanceField(Symbols::Index());
+    ASSERT(!field.IsNull());
+    enum_index_field_.store(field.ptr());
+
+    field = cls.LookupInstanceFieldAllowPrivate(Symbols::_name());
+    ASSERT(!field.IsNull());
+    enum_name_field_.store(field.ptr());
+
     auto& function = Function::Handle(zone);
 
     function = core_lib.LookupFunctionAllowPrivate(Symbols::_objectHashCode());
diff --git a/runtime/vm/object_store.h b/runtime/vm/object_store.h
index 19689f0..3d77c61 100644
--- a/runtime/vm/object_store.h
+++ b/runtime/vm/object_store.h
@@ -47,6 +47,8 @@
   LAZY_CORE(Class, list_class)                                                 \
   LAZY_CORE(Type, non_nullable_list_rare_type)                                 \
   LAZY_CORE(Type, non_nullable_map_rare_type)                                  \
+  LAZY_CORE(Field, enum_index_field)                                           \
+  LAZY_CORE(Field, enum_name_field)                                            \
   LAZY_CORE(Function, _object_equals_function)                                 \
   LAZY_CORE(Function, _object_hash_code_function)                              \
   LAZY_CORE(Function, _object_to_string_function)                              \
diff --git a/runtime/vm/symbols.h b/runtime/vm/symbols.h
index 75bb968..5c8db08 100644
--- a/runtime/vm/symbols.h
+++ b/runtime/vm/symbols.h
@@ -293,6 +293,7 @@
   V(_DeletedEnumPrefix, "Deleted enum value from ")                            \
   V(_DeletedEnumSentinel, "_deleted_enum_sentinel")                            \
   V(_Double, "_Double")                                                        \
+  V(_Enum, "_Enum")                                                            \
   V(_ExternalFloat32Array, "_ExternalFloat32Array")                            \
   V(_ExternalFloat32x4Array, "_ExternalFloat32x4Array")                        \
   V(_ExternalFloat64Array, "_ExternalFloat64Array")                            \
diff --git a/tools/VERSION b/tools/VERSION
index bb3a9c1..03fbd69 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 17
 PATCH 0
-PRERELEASE 29
+PRERELEASE 30
 PRERELEASE_PATCH 0
\ No newline at end of file