Version 2.14.0-378.0.dev

Merge commit '9fced4c389504c0dd8ff1a2c29d5958f2f23ca54' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index faad814..d40b3a4 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -11,7 +11,7 @@
     "constraint, update this by running tools/generate_package_config.dart."
   ],
   "configVersion": 2,
-  "generated": "2021-07-27T19:27:52.638315",
+  "generated": "2021-07-30T20:37:49.663747",
   "generator": "tools/generate_package_config.dart",
   "packages": [
     {
@@ -205,6 +205,12 @@
       "languageVersion": "2.3"
     },
     {
+      "name": "dart2js_runtime_metrics",
+      "rootUri": "../pkg/dart2js_runtime_metrics",
+      "packageUri": "lib/",
+      "languageVersion": "2.14"
+    },
+    {
       "name": "dart2js_tools",
       "rootUri": "../pkg/dart2js_tools",
       "packageUri": "lib/",
@@ -244,7 +250,7 @@
       "name": "dds",
       "rootUri": "../pkg/dds",
       "packageUri": "lib/",
-      "languageVersion": "2.12"
+      "languageVersion": "2.14"
     },
     {
       "name": "dev_compiler",
diff --git a/.packages b/.packages
index 2bd3c0a..764746a 100644
--- a/.packages
+++ b/.packages
@@ -30,6 +30,7 @@
 crypto:third_party/pkg/crypto/lib
 csslib:third_party/pkg/csslib/lib
 dart2js_info:third_party/pkg/dart2js_info/lib
+dart2js_runtime_metrics:pkg/dart2js_runtime_metrics/lib
 dart2js_tools:pkg/dart2js_tools/lib
 dart2native:pkg/dart2native/lib
 dart_internal:pkg/dart_internal/lib
diff --git a/pkg/_fe_analyzer_shared/lib/src/util/options.dart b/pkg/_fe_analyzer_shared/lib/src/util/options.dart
new file mode 100644
index 0000000..bf26a66
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/lib/src/util/options.dart
@@ -0,0 +1,310 @@
+// 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 '../messages/codes.dart';
+import 'resolve_input_uri.dart';
+
+class CommandLineProblem {
+  final Message message;
+
+  CommandLineProblem(this.message);
+
+  CommandLineProblem.deprecated(String message)
+      : this(templateUnspecified.withArguments(message));
+
+  String toString() => message.message;
+}
+
+class ParsedOptions {
+  final Map<String, dynamic> options = <String, dynamic>{};
+  final List<String> arguments = <String>[];
+  final Map<String, String> defines = <String, String>{};
+
+  String toString() => "ParsedArguments($options, $arguments)";
+
+  /// Returns arguments stored as line separated text.
+  static List<String> readOptionsFile(String optionsFile) {
+    return optionsFile
+        .split('\n')
+        .map((String line) => line.trim())
+        .where((line) => line.isNotEmpty)
+        .toList();
+  }
+
+  /// Parses a list of command-line [arguments] into options and arguments.
+  ///
+  /// An /option/ is something that, normally, starts with `-` or `--` (one or
+  /// two dashes). However, as a special case `/?` and `/h` are also recognized
+  /// as options for increased compatibility with Windows. An option can have a
+  /// value.
+  ///
+  /// An /argument/ is something that isn't an option, for example, a file name.
+  ///
+  /// The specification is a map of options to one of the following values:
+  /// * the type literal `Uri`, representing an option value of type [Uri],
+  /// * the type literal `int`, representing an option value of type [int],
+  /// * the bool literal `false`, representing a boolean option that is turned
+  ///   off by default,
+  /// * the bool literal `true, representing a boolean option that is turned on
+  ///   by default,
+  /// * or the string literal `","`, representing a comma-separated list of
+  ///   values.
+  ///
+  /// If [arguments] contains `"--"`, anything before is parsed as options, and
+  /// arguments; anything following is treated as arguments (even if starting
+  /// with, for example, a `-`).
+  ///
+  /// If an option isn't found in [specification], an error is thrown.
+  ///
+  /// Boolean options do not require an option value, but an optional value can
+  /// be provided using the forms `--option=value` where `value` can be `true`
+  /// or `yes` to turn on the option, or `false` or `no` to turn it off.  If no
+  /// option value is specified, a boolean option is turned on.
+  ///
+  /// All other options require an option value, either on the form `--option
+  /// value` or `--option=value`.
+  static ParsedOptions parse(List<String> arguments, List<Option>? options) {
+    options ??= [];
+    Map<String, ValueSpecification>? specification = {};
+    void addSpec(String flag, ValueSpecification spec) {
+      if (specification.containsKey(flag)) {
+        throw new CommandLineProblem.deprecated("Duplicate option '${flag}'.");
+      }
+      specification[flag] = spec;
+    }
+
+    for (Option option in options) {
+      addSpec(option.flag, option.spec);
+      for (String alias in option.aliases) {
+        addSpec(alias, new AliasValue(option.flag));
+      }
+    }
+    ParsedOptions result = new ParsedOptions();
+    int index = arguments.indexOf("--");
+    Iterable<String> nonOptions = const <String>[];
+    Iterator<String> iterator = arguments.iterator;
+    if (index != -1) {
+      nonOptions = arguments.skip(index + 1);
+      iterator = arguments.take(index).iterator;
+    }
+    while (iterator.moveNext()) {
+      String argument = iterator.current;
+      if (argument.startsWith("-") || argument == "/?" || argument == "/h") {
+        String? value;
+        if (argument.startsWith("-D")) {
+          value = argument.substring("-D".length);
+          argument = "-D";
+        } else {
+          index = argument.indexOf("=");
+          if (index != -1) {
+            value = argument.substring(index + 1);
+            argument = argument.substring(0, index);
+          }
+        }
+        ValueSpecification? valueSpecification = specification[argument];
+        if (valueSpecification == null) {
+          throw new CommandLineProblem.deprecated(
+              "Unknown option '$argument'.");
+        }
+        String canonicalArgument = argument;
+        if (valueSpecification.alias != null) {
+          canonicalArgument = valueSpecification.alias as String;
+          valueSpecification = specification[valueSpecification.alias];
+        }
+        if (valueSpecification == null) {
+          throw new CommandLineProblem.deprecated(
+              "Unknown option alias '$canonicalArgument'.");
+        }
+        final bool requiresValue = valueSpecification.requiresValue;
+        if (requiresValue && value == null) {
+          if (!iterator.moveNext()) {
+            throw new CommandLineProblem(
+                templateFastaCLIArgumentRequired.withArguments(argument));
+          }
+          value = iterator.current;
+        }
+        valueSpecification.processValue(
+            result, canonicalArgument, argument, value);
+      } else {
+        result.arguments.add(argument);
+      }
+    }
+    specification.forEach((String key, ValueSpecification value) {
+      if (value.defaultValue != null) {
+        result.options[key] ??= value.defaultValue;
+      }
+    });
+    result.arguments.addAll(nonOptions);
+    return result;
+  }
+}
+
+abstract class ValueSpecification<T> {
+  const ValueSpecification();
+
+  String? get alias => null;
+
+  T? get defaultValue => null;
+
+  bool get requiresValue => true;
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value);
+}
+
+class AliasValue<T> extends ValueSpecification<T> {
+  final String alias;
+
+  const AliasValue(this.alias);
+
+  bool get requiresValue =>
+      throw new UnsupportedError("AliasValue.requiresValue");
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value) {
+    throw new UnsupportedError("AliasValue.processValue");
+  }
+}
+
+class UriValue extends ValueSpecification<Uri?> {
+  const UriValue();
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value) {
+    if (result.options.containsKey(canonicalArgument)) {
+      throw new CommandLineProblem.deprecated(
+          "Multiple values for '$argument': "
+          "'${result.options[canonicalArgument]}' and '$value'.");
+    }
+    // TODO(ahe): resolve Uris lazily, so that schemes provided by
+    // other flags can be used for parsed command-line arguments too.
+    result.options[canonicalArgument] = resolveInputUri(value!);
+  }
+}
+
+class StringValue extends ValueSpecification<String?> {
+  final String? defaultValue;
+
+  const StringValue({this.defaultValue});
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value) {
+    if (result.options.containsKey(canonicalArgument)) {
+      throw new CommandLineProblem.deprecated(
+          "Multiple values for '$argument': "
+          "'${result.options[canonicalArgument]}' and '$value'.");
+    }
+    result.options[canonicalArgument] = value!;
+  }
+}
+
+class BoolValue extends ValueSpecification<bool> {
+  final bool defaultValue;
+
+  const BoolValue(this.defaultValue);
+
+  bool get requiresValue => false;
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value) {
+    if (result.options.containsKey(canonicalArgument)) {
+      throw new CommandLineProblem.deprecated(
+          "Multiple values for '$argument': "
+          "'${result.options[canonicalArgument]}' and '$value'.");
+    }
+    bool parsedValue;
+    if (value == null || value == "true" || value == "yes") {
+      parsedValue = true;
+    } else if (value == "false" || value == "no") {
+      parsedValue = false;
+    } else {
+      throw new CommandLineProblem.deprecated(
+          "Value for '$argument' is '$value', "
+          "but expected one of: 'true', 'false', 'yes', or 'no'.");
+    }
+    result.options[canonicalArgument] = parsedValue;
+  }
+}
+
+class IntValue extends ValueSpecification<int?> {
+  final int? defaultValue;
+  final int? noArgValue;
+
+  const IntValue({this.defaultValue, this.noArgValue});
+
+  bool get requiresValue => noArgValue == null;
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value) {
+    if (result.options.containsKey(canonicalArgument)) {
+      throw new CommandLineProblem.deprecated(
+          "Multiple values for '$argument': "
+          "'${result.options[canonicalArgument]}' and '$value'.");
+    }
+    int? parsedValue = noArgValue;
+    if (value != null) {
+      parsedValue = int.tryParse(value);
+    }
+    if (parsedValue == null) {
+      throw new CommandLineProblem.deprecated(
+          "Value for '$argument', '$value', isn't an int.");
+    }
+    result.options[canonicalArgument] = parsedValue;
+  }
+}
+
+class DefineValue extends ValueSpecification<Map<String, String>> {
+  const DefineValue();
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value) {
+    int index = value!.indexOf('=');
+    String name;
+    String expression;
+    if (index != -1) {
+      name = value.substring(0, index);
+      expression = value.substring(index + 1);
+    } else {
+      name = value;
+      expression = value;
+    }
+    result.defines[name] = expression;
+  }
+}
+
+class StringListValue extends ValueSpecification<List<String>?> {
+  const StringListValue();
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value) {
+    List<String> values = result.options[canonicalArgument] ??= <String>[];
+    values.addAll(value!.split(","));
+  }
+}
+
+class UriListValue extends ValueSpecification<List<Uri>?> {
+  const UriListValue();
+
+  void processValue(ParsedOptions result, String canonicalArgument,
+      String argument, String? value) {
+    List<Uri> values = result.options[canonicalArgument] ??= <Uri>[];
+    values.addAll(value!.split(",").map(resolveInputUri));
+  }
+}
+
+class Option<T> {
+  final String flag;
+
+  final ValueSpecification<T?> spec;
+
+  final bool isDefines;
+
+  final List<String> aliases;
+
+  const Option(this.flag, this.spec,
+      {this.isDefines: false, this.aliases: const []});
+
+  T read(ParsedOptions parsedOptions) =>
+      (isDefines ? parsedOptions.defines : parsedOptions.options[flag]) as T;
+}
diff --git a/pkg/front_end/lib/src/fasta/resolve_input_uri.dart b/pkg/_fe_analyzer_shared/lib/src/util/resolve_input_uri.dart
similarity index 93%
rename from pkg/front_end/lib/src/fasta/resolve_input_uri.dart
rename to pkg/_fe_analyzer_shared/lib/src/util/resolve_input_uri.dart
index 6f0dba3..2600d6d 100644
--- a/pkg/front_end/lib/src/fasta/resolve_input_uri.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/util/resolve_input_uri.dart
@@ -2,7 +2,7 @@
 // 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:_fe_analyzer_shared/src/util/relativize.dart';
+import 'relativize.dart';
 
 Uri resolveInputUri(String path) {
   Uri uri;
diff --git a/pkg/_fe_analyzer_shared/test/resolve_input_uri_test.dart b/pkg/_fe_analyzer_shared/test/resolve_input_uri_test.dart
new file mode 100644
index 0000000..b1da4e1
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/resolve_input_uri_test.dart
@@ -0,0 +1,41 @@
+// Copyright (c) 2018, 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:_fe_analyzer_shared/src/util/relativize.dart';
+import 'package:_fe_analyzer_shared/src/util/resolve_input_uri.dart';
+
+test() {
+  // data URI scheme is supported by default'.
+  expect('data', resolveInputUri('data:,foo').scheme);
+
+  // Custom Dart schemes are recognized by default.
+  expect('dart', resolveInputUri('dart:foo').scheme);
+  expect('package', resolveInputUri('package:foo').scheme);
+
+  // Unknown schemes are recognized by default.
+  expect(isWindows ? 'file' : 'c', resolveInputUri('c:/foo').scheme);
+  expect('test', resolveInputUri('test:foo').scheme);
+  expect('org-dartlang-foo', resolveInputUri('org-dartlang-foo:bar').scheme);
+  expect('test', resolveInputUri('test:/foo').scheme);
+  expect('org-dartlang-foo', resolveInputUri('org-dartlang-foo:/bar').scheme);
+  expect(
+      "${Uri.base.resolve('file.txt')}", "${resolveInputUri('file:file.txt')}");
+}
+
+main() {
+  // Test platform default.
+  test();
+  // Test non-Windows behavior.
+  isWindows = false;
+  test();
+  // Test Windows behavior.
+  isWindows = true;
+  test();
+}
+
+void expect(expected, actual) {
+  if (expected != actual) {
+    throw 'Expected $expected, actual $actual';
+  }
+}
diff --git a/pkg/analyzer/lib/error/error.dart b/pkg/analyzer/lib/error/error.dart
index 0a2b2c8..7d50e7e 100644
--- a/pkg/analyzer/lib/error/error.dart
+++ b/pkg/analyzer/lib/error/error.dart
@@ -604,6 +604,7 @@
   HintCode.TYPE_CHECK_IS_NOT_NULL,
   HintCode.TYPE_CHECK_IS_NULL,
   HintCode.UNDEFINED_HIDDEN_NAME,
+  HintCode.UNDEFINED_REFERENCED_PARAMETER,
   HintCode.UNDEFINED_SHOWN_NAME,
   HintCode.UNIGNORABLE_IGNORE,
   HintCode.UNNECESSARY_CAST,
diff --git a/pkg/analyzer/lib/src/dart/error/hint_codes.dart b/pkg/analyzer/lib/src/dart/error/hint_codes.dart
index 4030bff..afaad1f 100644
--- a/pkg/analyzer/lib/src/dart/error/hint_codes.dart
+++ b/pkg/analyzer/lib/src/dart/error/hint_codes.dart
@@ -2878,6 +2878,18 @@
       hasPublishedDocs: true);
 
   /**
+   * This hint is generated when an `@UnusedResult.unless` annotation
+   * references an undefined parameter.
+   *
+   * Parameters:
+   * 0: the name of the undefined parameter
+   * 1: the name of the targeted member
+   */
+  static const HintCode UNDEFINED_REFERENCED_PARAMETER = HintCode(
+      'UNDEFINED_REFERENCED_PARAMETER',
+      "The parameter '{0}' is not defined by '{1}'.");
+
+  /**
    * Parameters:
    * 0: the name of the library being imported
    * 1: the name in the show clause that isn't defined in the library
diff --git a/pkg/analyzer/lib/src/error/best_practices_verifier.dart b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
index bfa745d..5425f5e 100644
--- a/pkg/analyzer/lib/src/error/best_practices_verifier.dart
+++ b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
@@ -12,6 +12,7 @@
 import 'package:analyzer/dart/element/nullability_suffix.dart';
 import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/src/dart/ast/ast.dart';
 import 'package:analyzer/src/dart/ast/extensions.dart';
 import 'package:analyzer/src/dart/element/element.dart';
 import 'package:analyzer/src/dart/element/extensions.dart';
@@ -285,6 +286,30 @@
         // visibleForTemplate or visibleForTesting, so leave it alone for now.
       }
     }
+
+    // Check for a reference to an undefined parameter in a `@UseResult.unless`
+    // annotation.
+    if (element.isUseResult) {
+      var undefinedParam = _findUndefinedUseResultParam(element, node, parent);
+      if (undefinedParam != null) {
+        String? name;
+        if (parent is FunctionDeclaration) {
+          name = parent.name.name;
+        } else if (parent is MethodDeclaration) {
+          name = parent.name.name;
+        }
+        if (name != null) {
+          var paramName = undefinedParam is SimpleStringLiteral
+              ? undefinedParam.value
+              : undefinedParam.staticParameterElement?.name;
+          _errorReporter.reportErrorForNode(
+              HintCode.UNDEFINED_REFERENCED_PARAMETER,
+              undefinedParam,
+              [paramName ?? undefinedParam, name]);
+        }
+      }
+    }
+
     var kinds = _targetKindsFor(element);
     if (kinds.isNotEmpty) {
       if (!_isValidTarget(parent, kinds)) {
@@ -1538,6 +1563,62 @@
     }
   }
 
+  Expression? _findUndefinedUseResultParam(
+      ElementAnnotation element, Annotation node, AstNode parent) {
+    var constructorName = node.name;
+    if (constructorName is! PrefixedIdentifier ||
+        constructorName.identifier.name != 'unless') {
+      return null;
+    }
+
+    var unlessParam = element
+        .computeConstantValue()
+        ?.getField('parameterDefined')
+        ?.toStringValue();
+    if (unlessParam == null) {
+      return null;
+    }
+
+    Expression? checkParams(FormalParameterList? parameterList) {
+      if (parameterList == null) {
+        return null;
+      }
+
+      for (var param in parameterList.parameters) {
+        if (param is FormalParameter) {
+          // Param is defined.
+          if (param.identifier?.name == unlessParam) {
+            return null;
+          }
+        }
+      }
+
+      // Find and return the parameter value node.
+      var arguments = node.arguments?.arguments;
+      if (arguments == null) {
+        return null;
+      }
+
+      for (var arg in arguments) {
+        if (arg is NamedExpression &&
+            arg.name.label.name == 'parameterDefined') {
+          return arg.expression;
+        }
+      }
+
+      return null;
+    }
+
+    if (parent is FunctionDeclarationImpl) {
+      return checkParams(parent.functionExpression.parameters);
+    }
+    if (parent is MethodDeclarationImpl) {
+      return checkParams(parent.parameters);
+    }
+
+    return null;
+  }
+
   /// Return subexpressions that are marked `@doNotStore`, as a map so that
   /// corresponding elements can be used in the diagnostic message.
   Map<Expression, Element> _getSubExpressionsMarkedDoNotStore(
diff --git a/pkg/analyzer/lib/src/error/duplicate_definition_verifier.dart b/pkg/analyzer/lib/src/error/duplicate_definition_verifier.dart
index a762a29..0947fbe 100644
--- a/pkg/analyzer/lib/src/error/duplicate_definition_verifier.dart
+++ b/pkg/analyzer/lib/src/error/duplicate_definition_verifier.dart
@@ -253,15 +253,18 @@
 
   /// Check that there are no members with the same name.
   void _checkClassMembers(ClassElement element, List<ClassMember> members) {
-    Set<String> constructorNames = HashSet<String>();
-    Map<String, Element> instanceGetters = HashMap<String, Element>();
-    Map<String, Element> instanceSetters = HashMap<String, Element>();
-    Map<String, Element> staticGetters = HashMap<String, Element>();
-    Map<String, Element> staticSetters = HashMap<String, Element>();
+    var constructorNames = HashSet<String>();
+    var instanceGetters = HashMap<String, Element>();
+    var instanceSetters = HashMap<String, Element>();
+    var staticGetters = HashMap<String, Element>();
+    var staticSetters = HashMap<String, Element>();
 
     for (ClassMember member in members) {
       if (member is ConstructorDeclaration) {
         var name = member.name?.name ?? '';
+        if (name == 'new') {
+          name = '';
+        }
         if (!constructorNames.add(name)) {
           if (name.isEmpty) {
             _errorReporter.reportErrorForName(
diff --git a/pkg/analyzer/lib/src/error/use_result_verifier.dart b/pkg/analyzer/lib/src/error/use_result_verifier.dart
index c96ee1d..cc2d3bf 100644
--- a/pkg/analyzer/lib/src/error/use_result_verifier.dart
+++ b/pkg/analyzer/lib/src/error/use_result_verifier.dart
@@ -5,6 +5,7 @@
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/element/element.dart';
 import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/src/dart/ast/ast.dart';
 import 'package:analyzer/src/dart/error/hint_codes.dart';
 import 'package:collection/collection.dart';
 
@@ -56,6 +57,10 @@
       return;
     }
 
+    if (_passesUsingParam(node, annotation)) {
+      return;
+    }
+
     if (_isUsed(node)) {
       return;
     }
@@ -72,6 +77,32 @@
     }
   }
 
+  bool _passesUsingParam(AstNode node, ElementAnnotation annotation) {
+    if (node is! MethodInvocation) {
+      return false;
+    }
+
+    var unlessParam = _getUseResultUnlessParam(annotation);
+    if (unlessParam == null) {
+      return false;
+    }
+
+    var argumentList = node.argumentList as ArgumentListImpl;
+    var parameters = argumentList.correspondingStaticParameters;
+    if (parameters == null) {
+      return false;
+    }
+
+    for (var param in parameters) {
+      var name = param?.name;
+      if (unlessParam == name) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
   static AstNode _getNodeToAnnotate(AstNode node) {
     if (node is MethodInvocation) {
       return node.methodName;
@@ -98,6 +129,11 @@
     return element.metadata.firstWhereOrNull((e) => e.isUseResult);
   }
 
+  static String? _getUseResultUnlessParam(ElementAnnotation annotation) {
+    var constantValue = annotation.computeConstantValue();
+    return constantValue?.getField('parameterDefined')?.toStringValue();
+  }
+
   static bool _isUsed(AstNode node) {
     var parent = node.parent;
     if (parent == null) {
diff --git a/pkg/analyzer/lib/src/test_utilities/mock_packages.dart b/pkg/analyzer/lib/src/test_utilities/mock_packages.dart
index 226370a..84ee9ef 100644
--- a/pkg/analyzer/lib/src/test_utilities/mock_packages.dart
+++ b/pkg/analyzer/lib/src/test_utilities/mock_packages.dart
@@ -86,8 +86,10 @@
   const _Sealed();
 }
 class UseResult {
+  final String? parameterDefined;
   final String reason;
   const UseResult([this.reason = '']);
+  const UseResult.unless({required this.parameterDefined, this.reason = ''});
 }
 class _VisibleForOverriding {
   const _VisibleForOverriding();
diff --git a/pkg/analyzer/test/src/dart/resolution/class_test.dart b/pkg/analyzer/test/src/dart/resolution/class_test.dart
index 96b3160..d5bb61e 100644
--- a/pkg/analyzer/test/src/dart/resolution/class_test.dart
+++ b/pkg/analyzer/test/src/dart/resolution/class_test.dart
@@ -338,6 +338,28 @@
     ]);
   }
 
+  test_error_duplicateConstructorDefault_bothNew() async {
+    await assertErrorsInCode(r'''
+class C {
+  C.new();
+  C.new();
+}
+''', [
+      error(CompileTimeErrorCode.DUPLICATE_CONSTRUCTOR_DEFAULT, 23, 5),
+    ]);
+  }
+
+  test_error_duplicateConstructorDefault_oneNew() async {
+    await assertErrorsInCode(r'''
+class C {
+  C();
+  C.new();
+}
+''', [
+      error(CompileTimeErrorCode.DUPLICATE_CONSTRUCTOR_DEFAULT, 19, 5),
+    ]);
+  }
+
   test_error_duplicateConstructorName() async {
     await assertErrorsInCode(r'''
 class C {
diff --git a/pkg/analyzer/test/src/diagnostics/test_all.dart b/pkg/analyzer/test/src/diagnostics/test_all.dart
index 49fbffe..176cb48 100644
--- a/pkg/analyzer/test/src/diagnostics/test_all.dart
+++ b/pkg/analyzer/test/src/diagnostics/test_all.dart
@@ -667,6 +667,8 @@
 import 'undefined_named_parameter_test.dart' as undefined_named_parameter;
 import 'undefined_operator_test.dart' as undefined_operator;
 import 'undefined_prefixed_name_test.dart' as undefined_prefixed_name;
+import 'undefined_referenced_parameter_test.dart'
+    as undefined_referenced_parameter;
 import 'undefined_setter_test.dart' as undefined_setter;
 import 'undefined_shown_name_test.dart' as undefined_shown_name;
 import 'unignorable_ignore_test.dart' as unignorable_ignore;
@@ -1158,6 +1160,7 @@
     undefined_named_parameter.main();
     undefined_operator.main();
     undefined_prefixed_name.main();
+    undefined_referenced_parameter.main();
     undefined_setter.main();
     undefined_shown_name.main();
     unignorable_ignore.main();
diff --git a/pkg/analyzer/test/src/diagnostics/undefined_referenced_parameter_test.dart b/pkg/analyzer/test/src/diagnostics/undefined_referenced_parameter_test.dart
new file mode 100644
index 0000000..c34634c
--- /dev/null
+++ b/pkg/analyzer/test/src/diagnostics/undefined_referenced_parameter_test.dart
@@ -0,0 +1,58 @@
+// 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 'package:analyzer/src/error/codes.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../dart/resolution/context_collection_resolution.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(UndefinedReferencedParameterTest);
+  });
+}
+
+@reflectiveTest
+class UndefinedReferencedParameterTest extends PubPackageResolutionTest {
+  @override
+  void setUp() {
+    super.setUp();
+    writeTestPackageConfigWithMeta();
+  }
+
+  test_method() async {
+    await assertErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+class Foo {
+  @UseResult.unless(parameterDefined: 'undef')
+  int foo([int? value]) => value ?? 0;
+}
+''', [
+      error(HintCode.UNDEFINED_REFERENCED_PARAMETER, 84, 7),
+    ]);
+  }
+
+  test_method_parameterDefined() async {
+    await assertNoErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+class Foo {
+  @UseResult.unless(parameterDefined: 'value')
+  int foo([int? value]) => value ?? 0;
+}
+''');
+  }
+
+  test_topLevelFunction() async {
+    await assertErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+@UseResult.unless(parameterDefined: 'undef')
+int foo([int? value]) => value ?? 0;
+''', [
+      error(HintCode.UNDEFINED_REFERENCED_PARAMETER, 70, 7),
+    ]);
+  }
+}
diff --git a/pkg/analyzer/test/src/diagnostics/unused_result_test.dart b/pkg/analyzer/test/src/diagnostics/unused_result_test.dart
index 0a0a134..02a7888 100644
--- a/pkg/analyzer/test/src/diagnostics/unused_result_test.dart
+++ b/pkg/analyzer/test/src/diagnostics/unused_result_test.dart
@@ -553,6 +553,21 @@
     ]);
   }
 
+  test_method_result_unassigned_parameterDefined() async {
+    await assertNoErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+class A {
+  @UseResult.unless(parameterDefined: 'value')
+  int foo([int? value]) => value ?? 0;
+}
+
+void main() {
+  A().foo(3);
+}
+''');
+  }
+
   test_topLevelFunction_result_assigned() async {
     await assertNoErrorsInCode(r'''
 import 'package:meta/meta.dart';
@@ -581,6 +596,19 @@
 ''');
   }
 
+  test_topLevelFunction_result_optionNamedParam_unassigned_parameterDefined() async {
+    await assertNoErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+@UseResult.unless(parameterDefined: 'value')
+int foo({int? value}) => value ?? 0;
+
+void main() {
+  foo(value: 3);
+}
+''');
+  }
+
   test_topLevelFunction_result_passed() async {
     await assertNoErrorsInCode(r'''
 import 'package:meta/meta.dart';
@@ -654,6 +682,49 @@
     ]);
   }
 
+  test_topLevelFunction_result_unassigned_parameterDefined() async {
+    await assertNoErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+@UseResult.unless(parameterDefined: 'value')
+int foo([int? value]) => value ?? 0;
+
+void main() {
+  foo(3);
+}
+''');
+  }
+
+  test_topLevelFunction_result_unassigned_parameterUnDefined() async {
+    await assertErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+@UseResult.unless(parameterDefined: 'value')
+int foo([int? value]) => value ?? 0;
+
+void main() {
+  foo();
+}
+''', [
+      error(HintCode.UNUSED_RESULT, 133, 3),
+    ]);
+  }
+
+  test_topLevelFunction_result_unassigned_parameterUnDefined2() async {
+    await assertErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+@UseResult.unless(parameterDefined: 'value')
+int foo([String? msg, int? value]) => value ?? 0;
+
+void main() {
+  foo('none');
+}
+''', [
+      error(HintCode.UNUSED_RESULT, 146, 3),
+    ]);
+  }
+
   test_topLevelVariable_assigned() async {
     await assertNoErrorsInCode(r'''
 import 'package:meta/meta.dart';
diff --git a/pkg/compiler/lib/src/backend_strategy.dart b/pkg/compiler/lib/src/backend_strategy.dart
index add5771..a8f2d9f 100644
--- a/pkg/compiler/lib/src/backend_strategy.dart
+++ b/pkg/compiler/lib/src/backend_strategy.dart
@@ -7,7 +7,7 @@
 import 'common.dart';
 import 'common/codegen.dart';
 import 'common/tasks.dart';
-import 'deferred_load/deferred_load.dart' show OutputUnitData;
+import 'deferred_load/output_unit.dart' show OutputUnitData;
 import 'enqueue.dart';
 import 'elements/entities.dart';
 import 'inferrer/types.dart';
diff --git a/pkg/compiler/lib/src/common/codegen.dart b/pkg/compiler/lib/src/common/codegen.dart
index bec3d57..57b488b 100644
--- a/pkg/compiler/lib/src/common/codegen.dart
+++ b/pkg/compiler/lib/src/common/codegen.dart
@@ -9,7 +9,7 @@
 import '../common.dart';
 import '../common_elements.dart';
 import '../constants/values.dart';
-import '../deferred_load/deferred_load.dart';
+import '../deferred_load/output_unit.dart';
 import '../elements/entities.dart';
 import '../elements/types.dart' show DartType, InterfaceType;
 import '../inferrer/abstract_value_domain.dart';
diff --git a/pkg/compiler/lib/src/common_elements.dart b/pkg/compiler/lib/src/common_elements.dart
index 8fdefc4..64a6537 100644
--- a/pkg/compiler/lib/src/common_elements.dart
+++ b/pkg/compiler/lib/src/common_elements.dart
@@ -388,6 +388,9 @@
   /// Holds the method "requiresPreamble" in _js_helper.
   FunctionEntity get requiresPreambleMarker;
 
+  /// Holds the method "_rawStartupMetrics" in _js_helper.
+  FunctionEntity get rawStartupMetrics;
+
   FunctionEntity get loadLibraryWrapper;
 
   FunctionEntity get loadDeferredLibrary;
@@ -1665,6 +1668,11 @@
   FunctionEntity get requiresPreambleMarker =>
       _requiresPreambleMarker ??= _findHelperFunction('requiresPreamble');
 
+  FunctionEntity _rawStartupMetrics;
+  @override
+  FunctionEntity get rawStartupMetrics =>
+      _rawStartupMetrics ??= _findHelperFunction('rawStartupMetrics');
+
   @override
   FunctionEntity get loadLibraryWrapper =>
       _findHelperFunction("_loadLibraryWrapper");
diff --git a/pkg/compiler/lib/src/compiler.dart b/pkg/compiler/lib/src/compiler.dart
index d9303a6..b3f137b 100644
--- a/pkg/compiler/lib/src/compiler.dart
+++ b/pkg/compiler/lib/src/compiler.dart
@@ -18,7 +18,8 @@
 import 'common/work.dart' show WorkItem;
 import 'common.dart';
 import 'common_elements.dart' show ElementEnvironment;
-import 'deferred_load/deferred_load.dart' show DeferredLoadTask, OutputUnitData;
+import 'deferred_load/deferred_load.dart' show DeferredLoadTask;
+import 'deferred_load/output_unit.dart' show OutputUnitData;
 import 'diagnostics/code_location.dart';
 import 'diagnostics/messages.dart' show Message, MessageTemplate;
 import 'dump_info.dart' show DumpInfoTask;
diff --git a/pkg/compiler/lib/src/constants/values.dart b/pkg/compiler/lib/src/constants/values.dart
index 4ef3a07..45c6f40 100644
--- a/pkg/compiler/lib/src/constants/values.dart
+++ b/pkg/compiler/lib/src/constants/values.dart
@@ -8,7 +8,7 @@
 import '../common_elements.dart';
 import '../elements/entities.dart';
 import '../elements/types.dart';
-import '../deferred_load/deferred_load.dart' show OutputUnit;
+import '../deferred_load/output_unit.dart' show OutputUnit;
 import '../js/js.dart' as js;
 import '../util/util.dart';
 
diff --git a/pkg/compiler/lib/src/deferred_load/deferred_load.dart b/pkg/compiler/lib/src/deferred_load/deferred_load.dart
index bf7a202..22d19c6 100644
--- a/pkg/compiler/lib/src/deferred_load/deferred_load.dart
+++ b/pkg/compiler/lib/src/deferred_load/deferred_load.dart
@@ -6,9 +6,11 @@
 
 import 'dart:collection' show Queue;
 
-import 'package:front_end/src/api_unstable/dart2js.dart' as fe;
 import 'package:kernel/ast.dart' as ir;
-import 'package:kernel/type_environment.dart' as ir;
+
+import 'dependencies.dart';
+import 'import_set.dart';
+import 'output_unit.dart';
 
 import '../../compiler_new.dart' show OutputType;
 import '../common/metrics.dart' show Metric, Metrics, CountMetric, DurationMetric;
@@ -20,68 +22,20 @@
     show
         ConstantValue,
         ConstructedConstantValue,
-        DeferredGlobalConstantValue,
         InstantiationConstantValue;
 import '../elements/types.dart';
 import '../elements/entities.dart';
-import '../ir/util.dart';
 import '../kernel/kelements.dart' show KLocalFunction;
 import '../kernel/element_map.dart';
-import '../serialization/serialization.dart';
-import '../options.dart';
 import '../universe/use.dart';
 import '../universe/world_impact.dart'
     show ImpactUseCase, WorldImpact, WorldImpactVisitorImpl;
-import '../util/maplet.dart';
 import '../util/util.dart' show makeUnique;
 import '../world.dart' show KClosedWorld;
 
-/// A "hunk" of the program that will be loaded whenever one of its [imports]
-/// are loaded.
-///
-/// Elements that are only used in one deferred import, is in an OutputUnit with
-/// the deferred import as single element in the [imports] set.
-///
-/// Whenever a deferred Element is shared between several deferred imports it is
-/// in an output unit with those imports in the [imports] Set.
-///
-/// We never create two OutputUnits sharing the same set of [imports].
-class OutputUnit implements Comparable<OutputUnit> {
-  /// `true` if this output unit is for the main output file.
-  final bool isMainOutput;
-
-  /// A unique name representing this [OutputUnit].
-  final String name;
-
-  /// The deferred imports that use the elements in this output unit.
-  final Set<ImportEntity> imports;
-
-  OutputUnit(this.isMainOutput, this.name, this.imports);
-
-  @override
-  int compareTo(OutputUnit other) {
-    if (identical(this, other)) return 0;
-    if (isMainOutput && !other.isMainOutput) return -1;
-    if (!isMainOutput && other.isMainOutput) return 1;
-    var size = imports.length;
-    var otherSize = other.imports.length;
-    if (size != otherSize) return size.compareTo(otherSize);
-    var thisImports = imports.toList();
-    var otherImports = other.imports.toList();
-    for (var i = 0; i < size; i++) {
-      var cmp = _compareImportEntities(thisImports[i], otherImports[i]);
-      if (cmp != 0) return cmp;
-    }
-    // TODO(sigmund): make compare stable.  If we hit this point, all imported
-    // libraries are the same, however [this] and [other] use different deferred
-    // imports in the program. We can make this stable if we sort based on the
-    // deferred imports themselves (e.g. their declaration location).
-    return name.compareTo(other.name);
-  }
-
-  @override
-  String toString() => "OutputUnit($name, $imports)";
-}
+// TODO(joshualitt): Refactor logic out of DeferredLoadTask so work_queue.dart
+// can be its own independent library.
+part 'work_queue.dart';
 
 class _DeferredLoadTaskMetrics implements Metrics {
   @override
@@ -650,7 +604,7 @@
     void addUnit(ImportSet importSet) {
       if (importSet.unit != null) return;
       var unit = OutputUnit(false, '$counter',
-          importSet._collectImports().map((i) => i.declaration).toSet());
+          importSet.collectImports().map((i) => i.declaration).toSet());
       counter++;
       importSet.unit = unit;
       _allOutputUnits.add(unit);
@@ -676,7 +630,7 @@
     var allDeferredImports = _allDeferredImports.toList();
     if (useIds) {
       // Sort for a canonical order of [ImportEntity]s.
-      allDeferredImports.sort(_compareImportEntities);
+      allDeferredImports.sort(compareImportEntities);
     }
     int nextDeferId = 0;
     Set<String> usedImportNames = {};
@@ -838,7 +792,7 @@
     int id = 0;
     Map<ImportEntity, int> importMap = {};
     var entities = _deferredImportDescriptions.keys.toList();
-    entities.sort(_compareImportEntities);
+    entities.sort(compareImportEntities);
     entities = entities.reversed.toList();
     for (var key in entities) {
       importMap[key] = id++;
@@ -1003,914 +957,3 @@
     return sb.toString();
   }
 }
-
-class ImportDescription {
-  /// Relative uri to the importing library.
-  final String importingUri;
-
-  /// The prefix this import is imported as.
-  final String prefix;
-
-  final LibraryEntity importingLibrary;
-
-  ImportDescription.internal(
-      this.importingUri, this.prefix, this.importingLibrary);
-
-  ImportDescription(
-      ImportEntity import, LibraryEntity importingLibrary, Uri mainLibraryUri)
-      : this.internal(
-            fe.relativizeUri(
-                mainLibraryUri, importingLibrary.canonicalUri, false),
-            import.name,
-            importingLibrary);
-}
-
-/// Indirectly represents a deferred import in an [ImportSet].
-///
-/// We could directly store the [declaration] in [ImportSet], but adding this
-/// class makes some of the import set operations more efficient.
-class _DeferredImport {
-  final ImportEntity declaration;
-
-  /// Canonical index associated with [declaration]. This is used to efficiently
-  /// implement [ImportSetLattice.union].
-  final int index;
-
-  _DeferredImport(this.declaration, this.index);
-}
-
-/// A compact lattice representation of import sets and subsets.
-///
-/// We use a graph of nodes to represent elements of the lattice, but only
-/// create new nodes on-demand as they are needed by the deferred loading
-/// algorithm.
-///
-/// The constructions of nodes is carefully done by storing imports in a
-/// specific order. This ensures that we have a unique and canonical
-/// representation for each subset.
-class ImportSetLattice {
-  /// Index of deferred imports that defines the canonical order used by the
-  /// operations below.
-  Map<ImportEntity, _DeferredImport> _importIndex = {};
-
-  /// The canonical instance representing the empty import set.
-  ImportSet _emptySet = ImportSet.empty();
-
-  /// The import set representing the main output unit, which happens to be
-  /// implemented as an empty set in our algorithm.
-  ImportSet get mainSet => _emptySet;
-
-  /// Get the singleton import set that only contains [import].
-  ImportSet singleton(ImportEntity import) {
-    // Ensure we have import in the index.
-    return _emptySet._add(_wrap(import));
-  }
-
-  /// Get the import set that includes the union of [a] and [b].
-  ImportSet union(ImportSet a, ImportSet b) {
-    if (a == null || a.isEmpty) return b;
-    if (b == null || b.isEmpty) return a;
-
-    // Create the union by merging the imports in canonical order. The sets are
-    // basically lists linked by the `_previous` field in reverse order. We do a
-    // merge-like scan 'backwards' removing the biggest element until we hit an
-    // empty set or a common prefix, and the add the 'merge-sorted' elements
-    // back onto the prefix.
-    ImportSet result;
-    // 'removed' imports in decreasing canonical order.
-    List<_DeferredImport> imports = [];
-
-    while (true) {
-      if (a.isEmpty) {
-        result = b;
-        break;
-      }
-      if (b.isEmpty || identical(a, b)) {
-        result = a;
-        break;
-      }
-      if (a._import.index > b._import.index) {
-        imports.add(a._import);
-        a = a._previous;
-      } else if (b._import.index > a._import.index) {
-        imports.add(b._import);
-        b = b._previous;
-      } else {
-        assert(identical(a._import, b._import));
-        imports.add(a._import);
-        a = a._previous;
-        b = b._previous;
-      }
-    }
-
-    // Add merged elements back in reverse order. It is tempting to pop them off
-    // with `removeLast()` but that causes measurable shrinking reallocations.
-    for (int i = imports.length - 1; i >= 0; i--) {
-      result = result._add(imports[i]);
-    }
-    return result;
-  }
-
-  /// Get the index for an [import] according to the canonical order.
-  _DeferredImport _wrap(ImportEntity import) {
-    return _importIndex[import] ??=
-        _DeferredImport(import, _importIndex.length);
-  }
-}
-
-/// A canonical set of deferred imports.
-class ImportSet {
-  /// Last element added to set.
-  ///
-  /// This set comprises [_import] appended onto [_previous]. *Note*: [_import]
-  /// is the last element in the set in the canonical order imposed by
-  /// [ImportSetLattice].
-  final _DeferredImport _import; // `null` for empty ImportSet
-  /// The set containing all previous elements.
-  final ImportSet _previous;
-  final int length;
-
-  bool get isEmpty => _import == null;
-  bool get isNotEmpty => _import != null;
-
-  /// Returns an iterable over the imports in this set in canonical order.
-  Iterable<_DeferredImport> _collectImports() {
-    List<_DeferredImport> result = [];
-    ImportSet current = this;
-    while (current.isNotEmpty) {
-      result.add(current._import);
-      current = current._previous;
-    }
-    assert(result.length == this.length);
-    return result.reversed;
-  }
-
-  /// Links to other import sets in the lattice by adding one import.
-  final Map<_DeferredImport, ImportSet> _transitions = Maplet();
-
-  ImportSet.empty()
-      : _import = null,
-        _previous = null,
-        length = 0;
-
-  ImportSet(this._import, this._previous, this.length);
-
-  /// The output unit corresponding to this set of imports, if any.
-  OutputUnit unit;
-
-  /// Create an import set that adds [import] to all the imports on this set.
-  /// This assumes that import's canonical order comes after all imports in
-  /// this current set. This should only be called from [ImportSetLattice],
-  /// since it is where we preserve this invariant.
-  ImportSet _add(_DeferredImport import) {
-    assert(_import == null || import.index > _import.index);
-    return _transitions[import] ??= ImportSet(import, this, length + 1);
-  }
-
-  @override
-  String toString() {
-    StringBuffer sb = StringBuffer();
-    sb.write('ImportSet(size: $length, ');
-    for (var import in _collectImports()) {
-      sb.write('${import.declaration.name} ');
-    }
-    sb.write(')');
-    return '$sb';
-  }
-}
-
-/// The algorithm work queue.
-class WorkQueue {
-  /// The actual queue of work that needs to be done.
-  final Queue<WorkItem> queue = Queue();
-
-  /// An index to find work items in the queue corresponding to a class.
-  final Map<ClassEntity, WorkItem> pendingClasses = {};
-
-  /// An index to find work items in the queue corresponding to an
-  /// [InterfaceType] represented here by its [ClassEntitiy].
-  final Map<ClassEntity, WorkItem> pendingClassType = {};
-
-  /// An index to find work items in the queue corresponding to a member.
-  final Map<MemberEntity, WorkItem> pendingMembers = {};
-
-  /// An index to find work items in the queue corresponding to a constant.
-  final Map<ConstantValue, WorkItem> pendingConstants = {};
-
-  /// Lattice used to compute unions of [ImportSet]s.
-  final ImportSetLattice _importSets;
-
-  WorkQueue(this._importSets);
-
-  /// Whether there are no more work items in the queue.
-  bool get isNotEmpty => queue.isNotEmpty;
-
-  /// Pop the next element in the queue.
-  WorkItem nextItem() {
-    assert(isNotEmpty);
-    return queue.removeFirst();
-  }
-
-  /// Add to the queue that [element] should be updated to include all imports
-  /// in [importSet]. If there is already a work item in the queue for
-  /// [element], this makes sure that the work item now includes the union of
-  /// [importSet] and the existing work item's import set.
-  void addClass(ClassEntity element, ImportSet importSet) {
-    var item = pendingClasses[element];
-    if (item == null) {
-      item = ClassWorkItem(element, importSet);
-      pendingClasses[element] = item;
-      queue.add(item);
-    } else {
-      item.importsToAdd = _importSets.union(item.importsToAdd, importSet);
-    }
-  }
-
-  /// Add to the queue that class type (represented by [element]) should be
-  /// updated to include all imports in [importSet]. If there is already a
-  /// work item in the queue for [element], this makes sure that the work
-  /// item now includes the union of [importSet] and the existing work
-  /// item's import set.
-  void addClassType(ClassEntity element, ImportSet importSet) {
-    var item = pendingClassType[element];
-    if (item == null) {
-      item = ClassTypeWorkItem(element, importSet);
-      pendingClassType[element] = item;
-      queue.add(item);
-    } else {
-      item.importsToAdd = _importSets.union(item.importsToAdd, importSet);
-    }
-  }
-
-  /// Add to the queue that [element] should be updated to include all imports
-  /// in [importSet]. If there is already a work item in the queue for
-  /// [element], this makes sure that the work item now includes the union of
-  /// [importSet] and the existing work item's import set.
-  void addMember(MemberEntity element, ImportSet importSet) {
-    var item = pendingMembers[element];
-    if (item == null) {
-      item = MemberWorkItem(element, importSet);
-      pendingMembers[element] = item;
-      queue.add(item);
-    } else {
-      item.importsToAdd = _importSets.union(item.importsToAdd, importSet);
-    }
-  }
-
-  /// Add to the queue that [constant] should be updated to include all imports
-  /// in [importSet]. If there is already a work item in the queue for
-  /// [constant], this makes sure that the work item now includes the union of
-  /// [importSet] and the existing work item's import set.
-  void addConstant(ConstantValue constant, ImportSet importSet) {
-    var item = pendingConstants[constant];
-    if (item == null) {
-      item = ConstantWorkItem(constant, importSet);
-      pendingConstants[constant] = item;
-      queue.add(item);
-    } else {
-      item.importsToAdd = _importSets.union(item.importsToAdd, importSet);
-    }
-  }
-}
-
-/// Summary of the work that needs to be done on a class, member, or constant.
-abstract class WorkItem {
-  /// Additional imports that use [element] or [value] and need to be added by
-  /// the algorithm.
-  ///
-  /// This is non-final in case we add more deferred imports to the set before
-  /// the work item is applied (see [WorkQueue.addElement] and
-  /// [WorkQueue.addConstant]).
-  ImportSet importsToAdd;
-
-  WorkItem(this.importsToAdd);
-
-  void update(DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue);
-}
-
-/// Summary of the work that needs to be done on a class.
-class ClassWorkItem extends WorkItem {
-  /// Class to be recursively updated.
-  final ClassEntity cls;
-
-  ClassWorkItem(this.cls, ImportSet newSet) : super(newSet);
-
-  @override
-  void update(
-      DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue) {
-    queue.pendingClasses.remove(cls);
-    ImportSet oldSet = task._classToSet[cls];
-    ImportSet newSet = task.importSets.union(oldSet, importsToAdd);
-    task._updateClassRecursive(closedWorld, cls, oldSet, newSet, queue);
-  }
-}
-
-/// Summary of the work that needs to be done on a class.
-class ClassTypeWorkItem extends WorkItem {
-  /// Class to be recursively updated.
-  final ClassEntity cls;
-
-  ClassTypeWorkItem(this.cls, ImportSet newSet) : super(newSet);
-
-  @override
-  void update(
-      DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue) {
-    queue.pendingClassType.remove(cls);
-    ImportSet oldSet = task._classTypeToSet[cls];
-    ImportSet newSet = task.importSets.union(oldSet, importsToAdd);
-    task._updateClassTypeRecursive(closedWorld, cls, oldSet, newSet, queue);
-  }
-}
-
-/// Summary of the work that needs to be done on a member.
-class MemberWorkItem extends WorkItem {
-  /// Member to be recursively updated.
-  final MemberEntity member;
-
-  MemberWorkItem(this.member, ImportSet newSet) : super(newSet);
-
-  @override
-  void update(
-      DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue) {
-    queue.pendingMembers.remove(member);
-    ImportSet oldSet = task._memberToSet[member];
-    ImportSet newSet = task.importSets.union(oldSet, importsToAdd);
-    task._updateMemberRecursive(closedWorld, member, oldSet, newSet, queue);
-  }
-}
-
-/// Summary of the work that needs to be done on a constant.
-class ConstantWorkItem extends WorkItem {
-  /// Constant to be recursively updated.
-  final ConstantValue constant;
-
-  ConstantWorkItem(this.constant, ImportSet newSet) : super(newSet);
-
-  @override
-  void update(
-      DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue) {
-    queue.pendingConstants.remove(constant);
-    ImportSet oldSet = task._constantToSet[constant];
-    ImportSet newSet = task.importSets.union(oldSet, importsToAdd);
-    task._updateConstantRecursive(closedWorld, constant, oldSet, newSet, queue);
-  }
-}
-
-/// Interface for updating an [OutputUnitData] object with data for late
-/// members, that is, members created on demand during code generation.
-class LateOutputUnitDataBuilder {
-  final OutputUnitData _outputUnitData;
-
-  LateOutputUnitDataBuilder(this._outputUnitData);
-
-  /// Registers [newEntity] to be emitted in the same output unit as
-  /// [existingEntity];
-  void registerColocatedMembers(
-      MemberEntity existingEntity, MemberEntity newEntity) {
-    assert(_outputUnitData._memberToUnit[newEntity] == null);
-    _outputUnitData._memberToUnit[newEntity] =
-        _outputUnitData.outputUnitForMember(existingEntity);
-  }
-}
-
-/// Results of the deferred loading algorithm.
-///
-/// Provides information about the output unit associated with entities and
-/// constants, as well as other helper methods.
-// TODO(sigmund): consider moving here every piece of data used as a result of
-// deferred loading (including hunksToLoad, etc).
-class OutputUnitData {
-  /// Tag used for identifying serialized [OutputUnitData] objects in a
-  /// debugging data stream.
-  static const String tag = 'output-unit-data';
-
-  final bool isProgramSplit;
-  final OutputUnit mainOutputUnit;
-  final Map<ClassEntity, OutputUnit> _classToUnit;
-  final Map<ClassEntity, OutputUnit> _classTypeToUnit;
-  final Map<MemberEntity, OutputUnit> _memberToUnit;
-  final Map<Local, OutputUnit> _localFunctionToUnit;
-  final Map<ConstantValue, OutputUnit> _constantToUnit;
-  final List<OutputUnit> outputUnits;
-  final Map<ImportEntity, String> importDeferName;
-
-  /// Because the token-stream is forgotten later in the program, we cache a
-  /// description of each deferred import.
-  final Map<ImportEntity, ImportDescription> deferredImportDescriptions;
-
-  OutputUnitData(
-      this.isProgramSplit,
-      this.mainOutputUnit,
-      this._classToUnit,
-      this._classTypeToUnit,
-      this._memberToUnit,
-      this._localFunctionToUnit,
-      this._constantToUnit,
-      this.outputUnits,
-      this.importDeferName,
-      this.deferredImportDescriptions);
-
-  // Creates J-world data from the K-world data.
-  factory OutputUnitData.from(
-      OutputUnitData other,
-      LibraryEntity convertLibrary(LibraryEntity library),
-      Map<ClassEntity, OutputUnit> Function(
-              Map<ClassEntity, OutputUnit>, Map<Local, OutputUnit>)
-          convertClassMap,
-      Map<MemberEntity, OutputUnit> Function(
-              Map<MemberEntity, OutputUnit>, Map<Local, OutputUnit>)
-          convertMemberMap,
-      Map<ConstantValue, OutputUnit> Function(Map<ConstantValue, OutputUnit>)
-          convertConstantMap) {
-    Map<ClassEntity, OutputUnit> classToUnit =
-        convertClassMap(other._classToUnit, other._localFunctionToUnit);
-    Map<ClassEntity, OutputUnit> classTypeToUnit =
-        convertClassMap(other._classTypeToUnit, other._localFunctionToUnit);
-    Map<MemberEntity, OutputUnit> memberToUnit =
-        convertMemberMap(other._memberToUnit, other._localFunctionToUnit);
-    Map<ConstantValue, OutputUnit> constantToUnit =
-        convertConstantMap(other._constantToUnit);
-    Map<ImportEntity, ImportDescription> deferredImportDescriptions = {};
-    other.deferredImportDescriptions
-        .forEach((ImportEntity import, ImportDescription description) {
-      deferredImportDescriptions[import] = ImportDescription.internal(
-          description.importingUri,
-          description.prefix,
-          convertLibrary(description.importingLibrary));
-    });
-
-    return OutputUnitData(
-        other.isProgramSplit,
-        other.mainOutputUnit,
-        classToUnit,
-        classTypeToUnit,
-        memberToUnit,
-        // Local functions only make sense in the K-world model.
-        const {},
-        constantToUnit,
-        other.outputUnits,
-        other.importDeferName,
-        deferredImportDescriptions);
-  }
-
-  /// Deserializes an [OutputUnitData] object from [source].
-  factory OutputUnitData.readFromDataSource(DataSource source) {
-    source.begin(tag);
-    bool isProgramSplit = source.readBool();
-    List<OutputUnit> outputUnits = source.readList(() {
-      bool isMainOutput = source.readBool();
-      String name = source.readString();
-      Set<ImportEntity> importSet = source.readImports().toSet();
-      return OutputUnit(isMainOutput, name, importSet);
-    });
-    OutputUnit mainOutputUnit = outputUnits[source.readInt()];
-
-    Map<ClassEntity, OutputUnit> classToUnit = source.readClassMap(() {
-      return outputUnits[source.readInt()];
-    });
-    Map<ClassEntity, OutputUnit> classTypeToUnit = source.readClassMap(() {
-      return outputUnits[source.readInt()];
-    });
-    Map<MemberEntity, OutputUnit> memberToUnit =
-        source.readMemberMap((MemberEntity member) {
-      return outputUnits[source.readInt()];
-    });
-    Map<ConstantValue, OutputUnit> constantToUnit = source.readConstantMap(() {
-      return outputUnits[source.readInt()];
-    });
-    Map<ImportEntity, String> importDeferName =
-        source.readImportMap(source.readString);
-    Map<ImportEntity, ImportDescription> deferredImportDescriptions =
-        source.readImportMap(() {
-      String importingUri = source.readString();
-      String prefix = source.readString();
-      LibraryEntity importingLibrary = source.readLibrary();
-      return ImportDescription.internal(importingUri, prefix, importingLibrary);
-    });
-    source.end(tag);
-    return OutputUnitData(
-        isProgramSplit,
-        mainOutputUnit,
-        classToUnit,
-        classTypeToUnit,
-        memberToUnit,
-        // Local functions only make sense in the K-world model.
-        const {},
-        constantToUnit,
-        outputUnits,
-        importDeferName,
-        deferredImportDescriptions);
-  }
-
-  /// Serializes this [OutputUnitData] to [sink].
-  void writeToDataSink(DataSink sink) {
-    sink.begin(tag);
-    sink.writeBool(isProgramSplit);
-    Map<OutputUnit, int> outputUnitIndices = {};
-    sink.writeList(outputUnits, (OutputUnit outputUnit) {
-      outputUnitIndices[outputUnit] = outputUnitIndices.length;
-      sink.writeBool(outputUnit.isMainOutput);
-      sink.writeString(outputUnit.name);
-      sink.writeImports(outputUnit.imports);
-    });
-    sink.writeInt(outputUnitIndices[mainOutputUnit]);
-    sink.writeClassMap(_classToUnit, (OutputUnit outputUnit) {
-      sink.writeInt(outputUnitIndices[outputUnit]);
-    });
-    sink.writeClassMap(_classTypeToUnit, (OutputUnit outputUnit) {
-      sink.writeInt(outputUnitIndices[outputUnit]);
-    });
-    sink.writeMemberMap(_memberToUnit,
-        (MemberEntity member, OutputUnit outputUnit) {
-      sink.writeInt(outputUnitIndices[outputUnit]);
-    });
-    sink.writeConstantMap(_constantToUnit, (OutputUnit outputUnit) {
-      sink.writeInt(outputUnitIndices[outputUnit]);
-    });
-    sink.writeImportMap(importDeferName, sink.writeString);
-    sink.writeImportMap(deferredImportDescriptions,
-        (ImportDescription importDescription) {
-      sink.writeString(importDescription.importingUri);
-      sink.writeString(importDescription.prefix);
-      sink.writeLibrary(importDescription.importingLibrary);
-    });
-    sink.end(tag);
-  }
-
-  /// Returns the [OutputUnit] where [cls] belongs.
-  // TODO(johnniwinther): Remove the need for [allowNull]. Dump-info currently
-  // needs it.
-  OutputUnit outputUnitForClass(ClassEntity cls, {bool allowNull = false}) {
-    if (!isProgramSplit) return mainOutputUnit;
-    OutputUnit unit = _classToUnit[cls];
-    assert(allowNull || unit != null, 'No output unit for class $cls');
-    return unit ?? mainOutputUnit;
-  }
-
-  OutputUnit outputUnitForClassForTesting(ClassEntity cls) => _classToUnit[cls];
-
-  /// Returns the [OutputUnit] where [cls]'s type belongs.
-  // TODO(joshualitt): see above TODO regarding allowNull.
-  OutputUnit outputUnitForClassType(ClassEntity cls, {bool allowNull = false}) {
-    if (!isProgramSplit) return mainOutputUnit;
-    OutputUnit unit = _classTypeToUnit[cls];
-    assert(allowNull || unit != null, 'No output unit for type $cls');
-    return unit ?? mainOutputUnit;
-  }
-
-  OutputUnit outputUnitForClassTypeForTesting(ClassEntity cls) =>
-      _classTypeToUnit[cls];
-
-  /// Returns the [OutputUnit] where [member] belongs.
-  OutputUnit outputUnitForMember(MemberEntity member) {
-    if (!isProgramSplit) return mainOutputUnit;
-    OutputUnit unit = _memberToUnit[member];
-    assert(unit != null, 'No output unit for member $member');
-    return unit ?? mainOutputUnit;
-  }
-
-  OutputUnit outputUnitForMemberForTesting(MemberEntity member) =>
-      _memberToUnit[member];
-
-  /// Direct access to the output-unit to constants map used for testing.
-  Iterable<ConstantValue> get constantsForTesting => _constantToUnit.keys;
-
-  /// Returns the [OutputUnit] where [constant] belongs.
-  OutputUnit outputUnitForConstant(ConstantValue constant) {
-    if (!isProgramSplit) return mainOutputUnit;
-    OutputUnit unit = _constantToUnit[constant];
-    // TODO(sigmund): enforce unit is not null: it is sometimes null on some
-    // corner cases on internal apps.
-    return unit ?? mainOutputUnit;
-  }
-
-  OutputUnit outputUnitForConstantForTesting(ConstantValue constant) =>
-      _constantToUnit[constant];
-
-  /// Indicates whether [element] is deferred.
-  bool isDeferredClass(ClassEntity element) {
-    return outputUnitForClass(element) != mainOutputUnit;
-  }
-
-  /// Returns `true` if element [to] is reachable from element [from] without
-  /// crossing a deferred import.
-  ///
-  /// For example, if we have two deferred libraries `A` and `B` that both
-  /// import a library `C`, then even though elements from `A` and `C` end up in
-  /// different output units, there is a non-deferred path between `A` and `C`.
-  bool hasOnlyNonDeferredImportPaths(MemberEntity from, MemberEntity to) {
-    OutputUnit outputUnitFrom = outputUnitForMember(from);
-    OutputUnit outputUnitTo = outputUnitForMember(to);
-    if (outputUnitTo == mainOutputUnit) return true;
-    if (outputUnitFrom == mainOutputUnit) return false;
-    return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
-  }
-
-  /// Returns `true` if constant [to] is reachable from element [from] without
-  /// crossing a deferred import.
-  ///
-  /// For example, if we have two deferred libraries `A` and `B` that both
-  /// import a library `C`, then even though elements from `A` and `C` end up in
-  /// different output units, there is a non-deferred path between `A` and `C`.
-  bool hasOnlyNonDeferredImportPathsToConstant(
-      MemberEntity from, ConstantValue to) {
-    OutputUnit outputUnitFrom = outputUnitForMember(from);
-    OutputUnit outputUnitTo = outputUnitForConstant(to);
-    if (outputUnitTo == mainOutputUnit) return true;
-    if (outputUnitFrom == mainOutputUnit) return false;
-    return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
-  }
-
-  /// Returns `true` if class [to] is reachable from element [from] without
-  /// crossing a deferred import.
-  ///
-  /// For example, if we have two deferred libraries `A` and `B` that both
-  /// import a library `C`, then even though elements from `A` and `C` end up in
-  /// different output units, there is a non-deferred path between `A` and `C`.
-  bool hasOnlyNonDeferredImportPathsToClass(MemberEntity from, ClassEntity to) {
-    OutputUnit outputUnitFrom = outputUnitForMember(from);
-    OutputUnit outputUnitTo = outputUnitForClass(to);
-    if (outputUnitTo == mainOutputUnit) return true;
-    if (outputUnitFrom == mainOutputUnit) return false;
-    return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
-  }
-
-  /// Registers that a constant is used in the same deferred output unit as
-  /// [field].
-  void registerConstantDeferredUse(DeferredGlobalConstantValue constant) {
-    if (!isProgramSplit) return;
-    OutputUnit unit = constant.unit;
-    assert(
-        _constantToUnit[constant] == null || _constantToUnit[constant] == unit);
-    _constantToUnit[constant] = unit;
-  }
-
-  /// Returns the unique name for the given deferred [import].
-  String getImportDeferName(Spannable node, ImportEntity import) {
-    String name = importDeferName[import];
-    if (name == null) {
-      throw SpannableAssertionFailure(node, "No deferred name for $import.");
-    }
-    return name;
-  }
-
-  /// Returns the names associated with each deferred import in [unit].
-  Iterable<String> getImportNames(OutputUnit unit) {
-    return unit.imports.map((i) => importDeferName[i]);
-  }
-}
-
-/// Returns the filename for the output-unit named [name].
-///
-/// The filename is of the form "<main output file>_<name>.part.js".
-/// If [addExtension] is false, the ".part.js" suffix is left out.
-String deferredPartFileName(CompilerOptions options, String name,
-    {bool addExtension = true}) {
-  assert(name != "");
-  String outPath = options.outputUri != null ? options.outputUri.path : "out";
-  String outName = outPath.substring(outPath.lastIndexOf('/') + 1);
-  String extension = addExtension ? ".part.js" : "";
-  return "${outName}_$name$extension";
-}
-
-class Dependencies {
-  final Map<ClassEntity, DependencyInfo> classes = {};
-  final Map<ClassEntity, DependencyInfo> classType = {};
-  final Map<MemberEntity, DependencyInfo> members = {};
-  final Set<Local> localFunctions = {};
-  final Map<ConstantValue, DependencyInfo> constants = {};
-
-  void addClass(ClassEntity cls, [ImportEntity import]) {
-    (classes[cls] ??= DependencyInfo()).registerImport(import);
-
-    // Add a classType dependency as well just in case we optimize out
-    // the class later.
-    addClassType(cls, import);
-  }
-
-  void addClassType(ClassEntity cls, [ImportEntity import]) {
-    (classType[cls] ??= DependencyInfo()).registerImport(import);
-  }
-
-  void addMember(MemberEntity m, [ImportEntity import]) {
-    (members[m] ??= DependencyInfo()).registerImport(import);
-  }
-
-  void addConstant(ConstantValue c, [ImportEntity import]) {
-    (constants[c] ??= DependencyInfo()).registerImport(import);
-  }
-}
-
-class DependencyInfo {
-  bool isDeferred = true;
-  List<ImportEntity> imports;
-
-  registerImport(ImportEntity import) {
-    if (!isDeferred) return;
-    // A null import represents a direct non-deferred dependency.
-    if (import != null) {
-      (imports ??= []).add(import);
-    } else {
-      imports = null;
-      isDeferred = false;
-    }
-  }
-}
-
-class TypeDependencyVisitor implements DartTypeVisitor<void, Null> {
-  final Dependencies _dependencies;
-  final ImportEntity _import;
-  final CommonElements _commonElements;
-
-  TypeDependencyVisitor(this._dependencies, this._import, this._commonElements);
-
-  @override
-  void visit(DartType type, [_]) {
-    type.accept(this, null);
-  }
-
-  void visitList(List<DartType> types) {
-    types.forEach(visit);
-  }
-
-  @override
-  void visitLegacyType(LegacyType type, Null argument) {
-    visit(type.baseType);
-  }
-
-  @override
-  void visitNullableType(NullableType type, Null argument) {
-    visit(type.baseType);
-  }
-
-  @override
-  void visitFutureOrType(FutureOrType type, Null argument) {
-    _dependencies.addClassType(_commonElements.futureClass);
-    visit(type.typeArgument);
-  }
-
-  @override
-  void visitNeverType(NeverType type, Null argument) {
-    // Nothing to add.
-  }
-
-  @override
-  void visitDynamicType(DynamicType type, Null argument) {
-    // Nothing to add.
-  }
-
-  @override
-  void visitErasedType(ErasedType type, Null argument) {
-    // Nothing to add.
-  }
-
-  @override
-  void visitAnyType(AnyType type, Null argument) {
-    // Nothing to add.
-  }
-
-  @override
-  void visitInterfaceType(InterfaceType type, Null argument) {
-    visitList(type.typeArguments);
-    _dependencies.addClassType(type.element, _import);
-  }
-
-  @override
-  void visitFunctionType(FunctionType type, Null argument) {
-    for (FunctionTypeVariable typeVariable in type.typeVariables) {
-      visit(typeVariable.bound);
-    }
-    visitList(type.parameterTypes);
-    visitList(type.optionalParameterTypes);
-    visitList(type.namedParameterTypes);
-    visit(type.returnType);
-  }
-
-  @override
-  void visitFunctionTypeVariable(FunctionTypeVariable type, Null argument) {
-    // Nothing to add. Handled in [visitFunctionType].
-  }
-
-  @override
-  void visitTypeVariableType(TypeVariableType type, Null argument) {
-    // TODO(johnniwinther): Do we need to collect the bound?
-  }
-
-  @override
-  void visitVoidType(VoidType type, Null argument) {
-    // Nothing to add.
-  }
-}
-
-class ConstantCollector extends ir.RecursiveVisitor {
-  final KernelToElementMap elementMap;
-  final Dependencies dependencies;
-  final ir.StaticTypeContext staticTypeContext;
-
-  ConstantCollector(this.elementMap, this.staticTypeContext, this.dependencies);
-
-  CommonElements get commonElements => elementMap.commonElements;
-
-  void add(ir.Expression node, {bool required = true}) {
-    ConstantValue constant = elementMap
-        .getConstantValue(staticTypeContext, node, requireConstant: required);
-    if (constant != null) {
-      dependencies.addConstant(
-          constant, elementMap.getImport(getDeferredImport(node)));
-    }
-  }
-
-  @override
-  void visitIntLiteral(ir.IntLiteral literal) {}
-
-  @override
-  void visitDoubleLiteral(ir.DoubleLiteral literal) {}
-
-  @override
-  void visitBoolLiteral(ir.BoolLiteral literal) {}
-
-  @override
-  void visitStringLiteral(ir.StringLiteral literal) {}
-
-  @override
-  void visitSymbolLiteral(ir.SymbolLiteral literal) => add(literal);
-
-  @override
-  void visitNullLiteral(ir.NullLiteral literal) {}
-
-  @override
-  void visitListLiteral(ir.ListLiteral literal) {
-    if (literal.isConst) {
-      add(literal);
-    } else {
-      super.visitListLiteral(literal);
-    }
-  }
-
-  @override
-  void visitSetLiteral(ir.SetLiteral literal) {
-    if (literal.isConst) {
-      add(literal);
-    } else {
-      super.visitSetLiteral(literal);
-    }
-  }
-
-  @override
-  void visitMapLiteral(ir.MapLiteral literal) {
-    if (literal.isConst) {
-      add(literal);
-    } else {
-      super.visitMapLiteral(literal);
-    }
-  }
-
-  @override
-  void visitConstructorInvocation(ir.ConstructorInvocation node) {
-    if (node.isConst) {
-      add(node);
-    } else {
-      super.visitConstructorInvocation(node);
-    }
-  }
-
-  @override
-  void visitTypeParameter(ir.TypeParameter node) {
-    // We avoid visiting metadata on the type parameter declaration. The bound
-    // cannot hold constants so we skip that as well.
-  }
-
-  @override
-  void visitVariableDeclaration(ir.VariableDeclaration node) {
-    // We avoid visiting metadata on the parameter declaration by only visiting
-    // the initializer. The type cannot hold constants so can kan skip that
-    // as well.
-    node.initializer?.accept(this);
-  }
-
-  @override
-  void visitTypeLiteral(ir.TypeLiteral node) {
-    if (node.type is! ir.TypeParameterType) add(node);
-  }
-
-  @override
-  void visitInstantiation(ir.Instantiation node) {
-    // TODO(johnniwinther): The CFE should mark constant instantiations as
-    // constant.
-    add(node, required: false);
-    super.visitInstantiation(node);
-  }
-
-  @override
-  void visitConstantExpression(ir.ConstantExpression node) {
-    add(node);
-  }
-}
-
-int _compareImportEntities(ImportEntity a, ImportEntity b) {
-  if (a == b) {
-    return 0;
-  } else {
-    return a.uri.path.compareTo(b.uri.path);
-  }
-}
diff --git a/pkg/compiler/lib/src/deferred_load/dependencies.dart b/pkg/compiler/lib/src/deferred_load/dependencies.dart
new file mode 100644
index 0000000..c4579a5
--- /dev/null
+++ b/pkg/compiler/lib/src/deferred_load/dependencies.dart
@@ -0,0 +1,250 @@
+// 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 'package:kernel/ast.dart' as ir;
+import 'package:kernel/type_environment.dart' as ir;
+
+import '../common_elements.dart' show CommonElements;
+import '../constants/values.dart'
+    show
+        ConstantValue;
+import '../elements/types.dart';
+import '../elements/entities.dart';
+import '../ir/util.dart';
+import '../kernel/element_map.dart';
+
+/// [Dependencies] is a helper for collecting per [Entity] [DependencyInfo].
+class Dependencies {
+  final Map<ClassEntity, DependencyInfo> classes = {};
+  final Map<ClassEntity, DependencyInfo> classType = {};
+  final Map<MemberEntity, DependencyInfo> members = {};
+  final Set<Local> localFunctions = {};
+  final Map<ConstantValue, DependencyInfo> constants = {};
+
+  void addClass(ClassEntity cls, [ImportEntity import]) {
+    (classes[cls] ??= DependencyInfo()).registerImport(import);
+
+    // Add a classType dependency as well just in case we optimize out
+    // the class later.
+    addClassType(cls, import);
+  }
+
+  void addClassType(ClassEntity cls, [ImportEntity import]) {
+    (classType[cls] ??= DependencyInfo()).registerImport(import);
+  }
+
+  void addMember(MemberEntity m, [ImportEntity import]) {
+    (members[m] ??= DependencyInfo()).registerImport(import);
+  }
+
+  void addConstant(ConstantValue c, [ImportEntity import]) {
+    (constants[c] ??= DependencyInfo()).registerImport(import);
+  }
+}
+
+class DependencyInfo {
+  bool isDeferred = true;
+  List<ImportEntity> imports;
+
+  registerImport(ImportEntity import) {
+    if (!isDeferred) return;
+    // A null import represents a direct non-deferred dependency.
+    if (import != null) {
+      (imports ??= []).add(import);
+    } else {
+      imports = null;
+      isDeferred = false;
+    }
+  }
+}
+
+class TypeDependencyVisitor implements DartTypeVisitor<void, Null> {
+  final Dependencies _dependencies;
+  final ImportEntity _import;
+  final CommonElements _commonElements;
+
+  TypeDependencyVisitor(this._dependencies, this._import, this._commonElements);
+
+  @override
+  void visit(DartType type, [_]) {
+    type.accept(this, null);
+  }
+
+  void visitList(List<DartType> types) {
+    types.forEach(visit);
+  }
+
+  @override
+  void visitLegacyType(LegacyType type, Null argument) {
+    visit(type.baseType);
+  }
+
+  @override
+  void visitNullableType(NullableType type, Null argument) {
+    visit(type.baseType);
+  }
+
+  @override
+  void visitFutureOrType(FutureOrType type, Null argument) {
+    _dependencies.addClassType(_commonElements.futureClass);
+    visit(type.typeArgument);
+  }
+
+  @override
+  void visitNeverType(NeverType type, Null argument) {
+    // Nothing to add.
+  }
+
+  @override
+  void visitDynamicType(DynamicType type, Null argument) {
+    // Nothing to add.
+  }
+
+  @override
+  void visitErasedType(ErasedType type, Null argument) {
+    // Nothing to add.
+  }
+
+  @override
+  void visitAnyType(AnyType type, Null argument) {
+    // Nothing to add.
+  }
+
+  @override
+  void visitInterfaceType(InterfaceType type, Null argument) {
+    visitList(type.typeArguments);
+    _dependencies.addClassType(type.element, _import);
+  }
+
+  @override
+  void visitFunctionType(FunctionType type, Null argument) {
+    for (FunctionTypeVariable typeVariable in type.typeVariables) {
+      visit(typeVariable.bound);
+    }
+    visitList(type.parameterTypes);
+    visitList(type.optionalParameterTypes);
+    visitList(type.namedParameterTypes);
+    visit(type.returnType);
+  }
+
+  @override
+  void visitFunctionTypeVariable(FunctionTypeVariable type, Null argument) {
+    // Nothing to add. Handled in [visitFunctionType].
+  }
+
+  @override
+  void visitTypeVariableType(TypeVariableType type, Null argument) {
+    // TODO(johnniwinther): Do we need to collect the bound?
+  }
+
+  @override
+  void visitVoidType(VoidType type, Null argument) {
+    // Nothing to add.
+  }
+}
+
+class ConstantCollector extends ir.RecursiveVisitor {
+  final KernelToElementMap elementMap;
+  final Dependencies dependencies;
+  final ir.StaticTypeContext staticTypeContext;
+
+  ConstantCollector(this.elementMap, this.staticTypeContext, this.dependencies);
+
+  CommonElements get commonElements => elementMap.commonElements;
+
+  void add(ir.Expression node, {bool required = true}) {
+    ConstantValue constant = elementMap
+        .getConstantValue(staticTypeContext, node, requireConstant: required);
+    if (constant != null) {
+      dependencies.addConstant(
+          constant, elementMap.getImport(getDeferredImport(node)));
+    }
+  }
+
+  @override
+  void visitIntLiteral(ir.IntLiteral literal) {}
+
+  @override
+  void visitDoubleLiteral(ir.DoubleLiteral literal) {}
+
+  @override
+  void visitBoolLiteral(ir.BoolLiteral literal) {}
+
+  @override
+  void visitStringLiteral(ir.StringLiteral literal) {}
+
+  @override
+  void visitSymbolLiteral(ir.SymbolLiteral literal) => add(literal);
+
+  @override
+  void visitNullLiteral(ir.NullLiteral literal) {}
+
+  @override
+  void visitListLiteral(ir.ListLiteral literal) {
+    if (literal.isConst) {
+      add(literal);
+    } else {
+      super.visitListLiteral(literal);
+    }
+  }
+
+  @override
+  void visitSetLiteral(ir.SetLiteral literal) {
+    if (literal.isConst) {
+      add(literal);
+    } else {
+      super.visitSetLiteral(literal);
+    }
+  }
+
+  @override
+  void visitMapLiteral(ir.MapLiteral literal) {
+    if (literal.isConst) {
+      add(literal);
+    } else {
+      super.visitMapLiteral(literal);
+    }
+  }
+
+  @override
+  void visitConstructorInvocation(ir.ConstructorInvocation node) {
+    if (node.isConst) {
+      add(node);
+    } else {
+      super.visitConstructorInvocation(node);
+    }
+  }
+
+  @override
+  void visitTypeParameter(ir.TypeParameter node) {
+    // We avoid visiting metadata on the type parameter declaration. The bound
+    // cannot hold constants so we skip that as well.
+  }
+
+  @override
+  void visitVariableDeclaration(ir.VariableDeclaration node) {
+    // We avoid visiting metadata on the parameter declaration by only visiting
+    // the initializer. The type cannot hold constants so can kan skip that
+    // as well.
+    node.initializer?.accept(this);
+  }
+
+  @override
+  void visitTypeLiteral(ir.TypeLiteral node) {
+    if (node.type is! ir.TypeParameterType) add(node);
+  }
+
+  @override
+  void visitInstantiation(ir.Instantiation node) {
+    // TODO(johnniwinther): The CFE should mark constant instantiations as
+    // constant.
+    add(node, required: false);
+    super.visitInstantiation(node);
+  }
+
+  @override
+  void visitConstantExpression(ir.ConstantExpression node) {
+    add(node);
+  }
+}
diff --git a/pkg/compiler/lib/src/deferred_load/import_set.dart b/pkg/compiler/lib/src/deferred_load/import_set.dart
new file mode 100644
index 0000000..2d24a747
--- /dev/null
+++ b/pkg/compiler/lib/src/deferred_load/import_set.dart
@@ -0,0 +1,162 @@
+// 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 'output_unit.dart';
+
+import '../elements/entities.dart';
+import '../util/maplet.dart';
+
+/// Indirectly represents a deferred import in an [ImportSet].
+///
+/// We could directly store the [declaration] in [ImportSet], but adding this
+/// class makes some of the import set operations more efficient.
+class _DeferredImport {
+  final ImportEntity declaration;
+
+  /// Canonical index associated with [declaration]. This is used to efficiently
+  /// implement [ImportSetLattice.union].
+  final int index;
+
+  _DeferredImport(this.declaration, this.index);
+}
+
+/// A compact lattice representation of import sets and subsets.
+///
+/// We use a graph of nodes to represent elements of the lattice, but only
+/// create new nodes on-demand as they are needed by the deferred loading
+/// algorithm.
+///
+/// The constructions of nodes is carefully done by storing imports in a
+/// specific order. This ensures that we have a unique and canonical
+/// representation for each subset.
+class ImportSetLattice {
+  /// Index of deferred imports that defines the canonical order used by the
+  /// operations below.
+  Map<ImportEntity, _DeferredImport> _importIndex = {};
+
+  /// The canonical instance representing the empty import set.
+  ImportSet _emptySet = ImportSet.empty();
+
+  /// The import set representing the main output unit, which happens to be
+  /// implemented as an empty set in our algorithm.
+  ImportSet get mainSet => _emptySet;
+
+  /// Get the singleton import set that only contains [import].
+  ImportSet singleton(ImportEntity import) {
+    // Ensure we have import in the index.
+    return _emptySet._add(_wrap(import));
+  }
+
+  /// Get the import set that includes the union of [a] and [b].
+  ImportSet union(ImportSet a, ImportSet b) {
+    if (a == null || a.isEmpty) return b;
+    if (b == null || b.isEmpty) return a;
+
+    // Create the union by merging the imports in canonical order. The sets are
+    // basically lists linked by the `_previous` field in reverse order. We do a
+    // merge-like scan 'backwards' removing the biggest element until we hit an
+    // empty set or a common prefix, and the add the 'merge-sorted' elements
+    // back onto the prefix.
+    ImportSet result;
+    // 'removed' imports in decreasing canonical order.
+    List<_DeferredImport> imports = [];
+
+    while (true) {
+      if (a.isEmpty) {
+        result = b;
+        break;
+      }
+      if (b.isEmpty || identical(a, b)) {
+        result = a;
+        break;
+      }
+      if (a._import.index > b._import.index) {
+        imports.add(a._import);
+        a = a._previous;
+      } else if (b._import.index > a._import.index) {
+        imports.add(b._import);
+        b = b._previous;
+      } else {
+        assert(identical(a._import, b._import));
+        imports.add(a._import);
+        a = a._previous;
+        b = b._previous;
+      }
+    }
+
+    // Add merged elements back in reverse order. It is tempting to pop them off
+    // with `removeLast()` but that causes measurable shrinking reallocations.
+    for (int i = imports.length - 1; i >= 0; i--) {
+      result = result._add(imports[i]);
+    }
+    return result;
+  }
+
+  /// Get the index for an [import] according to the canonical order.
+  _DeferredImport _wrap(ImportEntity import) {
+    return _importIndex[import] ??=
+        _DeferredImport(import, _importIndex.length);
+  }
+}
+
+/// A canonical set of deferred imports.
+class ImportSet {
+  /// Last element added to set.
+  ///
+  /// This set comprises [_import] appended onto [_previous]. *Note*: [_import]
+  /// is the last element in the set in the canonical order imposed by
+  /// [ImportSetLattice].
+  final _DeferredImport _import; // `null` for empty ImportSet
+  /// The set containing all previous elements.
+  final ImportSet _previous;
+  final int length;
+
+  bool get isEmpty => _import == null;
+  bool get isNotEmpty => _import != null;
+
+  /// Returns an iterable over the imports in this set in canonical order.
+  Iterable<_DeferredImport> collectImports() {
+    List<_DeferredImport> result = [];
+    ImportSet current = this;
+    while (current.isNotEmpty) {
+      result.add(current._import);
+      current = current._previous;
+    }
+    assert(result.length == this.length);
+    return result.reversed;
+  }
+
+  /// Links to other import sets in the lattice by adding one import.
+  final Map<_DeferredImport, ImportSet> _transitions = Maplet();
+
+  ImportSet.empty()
+      : _import = null,
+        _previous = null,
+        length = 0;
+
+  ImportSet(this._import, this._previous, this.length);
+
+  /// The output unit corresponding to this set of imports, if any.
+  OutputUnit unit;
+
+  /// Create an import set that adds [import] to all the imports on this set.
+  /// This assumes that import's canonical order comes after all imports in
+  /// this current set. This should only be called from [ImportSetLattice],
+  /// since it is where we preserve this invariant.
+  ImportSet _add(_DeferredImport import) {
+    assert(_import == null || import.index > _import.index);
+    return _transitions[import] ??= ImportSet(import, this, length + 1);
+  }
+
+  @override
+  String toString() {
+    StringBuffer sb = StringBuffer();
+    sb.write('ImportSet(size: $length, ');
+    for (var import in collectImports()) {
+      sb.write('${import.declaration.name} ');
+    }
+    sb.write(')');
+    return '$sb';
+  }
+}
diff --git a/pkg/compiler/lib/src/deferred_load/output_unit.dart b/pkg/compiler/lib/src/deferred_load/output_unit.dart
new file mode 100644
index 0000000..1b9f93a
--- /dev/null
+++ b/pkg/compiler/lib/src/deferred_load/output_unit.dart
@@ -0,0 +1,407 @@
+// 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 'package:front_end/src/api_unstable/dart2js.dart' as fe;
+
+import '../common.dart';
+import '../constants/values.dart'
+    show
+        ConstantValue,
+        DeferredGlobalConstantValue;
+import '../elements/entities.dart';
+import '../serialization/serialization.dart';
+import '../options.dart';
+
+/// A "hunk" of the program that will be loaded whenever one of its [imports]
+/// are loaded.
+///
+/// Elements that are only used in one deferred import, is in an OutputUnit with
+/// the deferred import as single element in the [imports] set.
+///
+/// Whenever a deferred Element is shared between several deferred imports it is
+/// in an output unit with those imports in the [imports] Set.
+///
+/// We never create two OutputUnits sharing the same set of [imports].
+class OutputUnit implements Comparable<OutputUnit> {
+  /// `true` if this output unit is for the main output file.
+  final bool isMainOutput;
+
+  /// A unique name representing this [OutputUnit].
+  final String name;
+
+  /// The deferred imports that use the elements in this output unit.
+  final Set<ImportEntity> imports;
+
+  OutputUnit(this.isMainOutput, this.name, this.imports);
+
+  @override
+  int compareTo(OutputUnit other) {
+    if (identical(this, other)) return 0;
+    if (isMainOutput && !other.isMainOutput) return -1;
+    if (!isMainOutput && other.isMainOutput) return 1;
+    var size = imports.length;
+    var otherSize = other.imports.length;
+    if (size != otherSize) return size.compareTo(otherSize);
+    var thisImports = imports.toList();
+    var otherImports = other.imports.toList();
+    for (var i = 0; i < size; i++) {
+      var cmp = compareImportEntities(thisImports[i], otherImports[i]);
+      if (cmp != 0) return cmp;
+    }
+    // TODO(sigmund): make compare stable.  If we hit this point, all imported
+    // libraries are the same, however [this] and [other] use different deferred
+    // imports in the program. We can make this stable if we sort based on the
+    // deferred imports themselves (e.g. their declaration location).
+    return name.compareTo(other.name);
+  }
+
+  @override
+  String toString() => "OutputUnit($name, $imports)";
+}
+
+/// Interface for updating an [OutputUnitData] object with data for late
+/// members, that is, members created on demand during code generation.
+class LateOutputUnitDataBuilder {
+  final OutputUnitData _outputUnitData;
+
+  LateOutputUnitDataBuilder(this._outputUnitData);
+
+  /// Registers [newEntity] to be emitted in the same output unit as
+  /// [existingEntity];
+  void registerColocatedMembers(
+      MemberEntity existingEntity, MemberEntity newEntity) {
+    assert(_outputUnitData._memberToUnit[newEntity] == null);
+    _outputUnitData._memberToUnit[newEntity] =
+        _outputUnitData.outputUnitForMember(existingEntity);
+  }
+}
+
+/// Results of the deferred loading algorithm.
+///
+/// Provides information about the output unit associated with entities and
+/// constants, as well as other helper methods.
+// TODO(sigmund): consider moving here every piece of data used as a result of
+// deferred loading (including hunksToLoad, etc).
+class OutputUnitData {
+  /// Tag used for identifying serialized [OutputUnitData] objects in a
+  /// debugging data stream.
+  static const String tag = 'output-unit-data';
+
+  final bool isProgramSplit;
+  final OutputUnit mainOutputUnit;
+  final Map<ClassEntity, OutputUnit> _classToUnit;
+  final Map<ClassEntity, OutputUnit> _classTypeToUnit;
+  final Map<MemberEntity, OutputUnit> _memberToUnit;
+  final Map<Local, OutputUnit> _localFunctionToUnit;
+  final Map<ConstantValue, OutputUnit> _constantToUnit;
+  final List<OutputUnit> outputUnits;
+  final Map<ImportEntity, String> importDeferName;
+
+  /// Because the token-stream is forgotten later in the program, we cache a
+  /// description of each deferred import.
+  final Map<ImportEntity, ImportDescription> deferredImportDescriptions;
+
+  OutputUnitData(
+      this.isProgramSplit,
+      this.mainOutputUnit,
+      this._classToUnit,
+      this._classTypeToUnit,
+      this._memberToUnit,
+      this._localFunctionToUnit,
+      this._constantToUnit,
+      this.outputUnits,
+      this.importDeferName,
+      this.deferredImportDescriptions);
+
+  // Creates J-world data from the K-world data.
+  factory OutputUnitData.from(
+      OutputUnitData other,
+      LibraryEntity convertLibrary(LibraryEntity library),
+      Map<ClassEntity, OutputUnit> Function(
+              Map<ClassEntity, OutputUnit>, Map<Local, OutputUnit>)
+          convertClassMap,
+      Map<MemberEntity, OutputUnit> Function(
+              Map<MemberEntity, OutputUnit>, Map<Local, OutputUnit>)
+          convertMemberMap,
+      Map<ConstantValue, OutputUnit> Function(Map<ConstantValue, OutputUnit>)
+          convertConstantMap) {
+    Map<ClassEntity, OutputUnit> classToUnit =
+        convertClassMap(other._classToUnit, other._localFunctionToUnit);
+    Map<ClassEntity, OutputUnit> classTypeToUnit =
+        convertClassMap(other._classTypeToUnit, other._localFunctionToUnit);
+    Map<MemberEntity, OutputUnit> memberToUnit =
+        convertMemberMap(other._memberToUnit, other._localFunctionToUnit);
+    Map<ConstantValue, OutputUnit> constantToUnit =
+        convertConstantMap(other._constantToUnit);
+    Map<ImportEntity, ImportDescription> deferredImportDescriptions = {};
+    other.deferredImportDescriptions
+        .forEach((ImportEntity import, ImportDescription description) {
+      deferredImportDescriptions[import] = ImportDescription.internal(
+          description.importingUri,
+          description.prefix,
+          convertLibrary(description.importingLibrary));
+    });
+
+    return OutputUnitData(
+        other.isProgramSplit,
+        other.mainOutputUnit,
+        classToUnit,
+        classTypeToUnit,
+        memberToUnit,
+        // Local functions only make sense in the K-world model.
+        const {},
+        constantToUnit,
+        other.outputUnits,
+        other.importDeferName,
+        deferredImportDescriptions);
+  }
+
+  /// Deserializes an [OutputUnitData] object from [source].
+  factory OutputUnitData.readFromDataSource(DataSource source) {
+    source.begin(tag);
+    bool isProgramSplit = source.readBool();
+    List<OutputUnit> outputUnits = source.readList(() {
+      bool isMainOutput = source.readBool();
+      String name = source.readString();
+      Set<ImportEntity> importSet = source.readImports().toSet();
+      return OutputUnit(isMainOutput, name, importSet);
+    });
+    OutputUnit mainOutputUnit = outputUnits[source.readInt()];
+
+    Map<ClassEntity, OutputUnit> classToUnit = source.readClassMap(() {
+      return outputUnits[source.readInt()];
+    });
+    Map<ClassEntity, OutputUnit> classTypeToUnit = source.readClassMap(() {
+      return outputUnits[source.readInt()];
+    });
+    Map<MemberEntity, OutputUnit> memberToUnit =
+        source.readMemberMap((MemberEntity member) {
+      return outputUnits[source.readInt()];
+    });
+    Map<ConstantValue, OutputUnit> constantToUnit = source.readConstantMap(() {
+      return outputUnits[source.readInt()];
+    });
+    Map<ImportEntity, String> importDeferName =
+        source.readImportMap(source.readString);
+    Map<ImportEntity, ImportDescription> deferredImportDescriptions =
+        source.readImportMap(() {
+      String importingUri = source.readString();
+      String prefix = source.readString();
+      LibraryEntity importingLibrary = source.readLibrary();
+      return ImportDescription.internal(importingUri, prefix, importingLibrary);
+    });
+    source.end(tag);
+    return OutputUnitData(
+        isProgramSplit,
+        mainOutputUnit,
+        classToUnit,
+        classTypeToUnit,
+        memberToUnit,
+        // Local functions only make sense in the K-world model.
+        const {},
+        constantToUnit,
+        outputUnits,
+        importDeferName,
+        deferredImportDescriptions);
+  }
+
+  /// Serializes this [OutputUnitData] to [sink].
+  void writeToDataSink(DataSink sink) {
+    sink.begin(tag);
+    sink.writeBool(isProgramSplit);
+    Map<OutputUnit, int> outputUnitIndices = {};
+    sink.writeList(outputUnits, (OutputUnit outputUnit) {
+      outputUnitIndices[outputUnit] = outputUnitIndices.length;
+      sink.writeBool(outputUnit.isMainOutput);
+      sink.writeString(outputUnit.name);
+      sink.writeImports(outputUnit.imports);
+    });
+    sink.writeInt(outputUnitIndices[mainOutputUnit]);
+    sink.writeClassMap(_classToUnit, (OutputUnit outputUnit) {
+      sink.writeInt(outputUnitIndices[outputUnit]);
+    });
+    sink.writeClassMap(_classTypeToUnit, (OutputUnit outputUnit) {
+      sink.writeInt(outputUnitIndices[outputUnit]);
+    });
+    sink.writeMemberMap(_memberToUnit,
+        (MemberEntity member, OutputUnit outputUnit) {
+      sink.writeInt(outputUnitIndices[outputUnit]);
+    });
+    sink.writeConstantMap(_constantToUnit, (OutputUnit outputUnit) {
+      sink.writeInt(outputUnitIndices[outputUnit]);
+    });
+    sink.writeImportMap(importDeferName, sink.writeString);
+    sink.writeImportMap(deferredImportDescriptions,
+        (ImportDescription importDescription) {
+      sink.writeString(importDescription.importingUri);
+      sink.writeString(importDescription.prefix);
+      sink.writeLibrary(importDescription.importingLibrary);
+    });
+    sink.end(tag);
+  }
+
+  /// Returns the [OutputUnit] where [cls] belongs.
+  // TODO(johnniwinther): Remove the need for [allowNull]. Dump-info currently
+  // needs it.
+  OutputUnit outputUnitForClass(ClassEntity cls, {bool allowNull = false}) {
+    if (!isProgramSplit) return mainOutputUnit;
+    OutputUnit unit = _classToUnit[cls];
+    assert(allowNull || unit != null, 'No output unit for class $cls');
+    return unit ?? mainOutputUnit;
+  }
+
+  OutputUnit outputUnitForClassForTesting(ClassEntity cls) => _classToUnit[cls];
+
+  /// Returns the [OutputUnit] where [cls]'s type belongs.
+  // TODO(joshualitt): see above TODO regarding allowNull.
+  OutputUnit outputUnitForClassType(ClassEntity cls, {bool allowNull = false}) {
+    if (!isProgramSplit) return mainOutputUnit;
+    OutputUnit unit = _classTypeToUnit[cls];
+    assert(allowNull || unit != null, 'No output unit for type $cls');
+    return unit ?? mainOutputUnit;
+  }
+
+  OutputUnit outputUnitForClassTypeForTesting(ClassEntity cls) =>
+      _classTypeToUnit[cls];
+
+  /// Returns the [OutputUnit] where [member] belongs.
+  OutputUnit outputUnitForMember(MemberEntity member) {
+    if (!isProgramSplit) return mainOutputUnit;
+    OutputUnit unit = _memberToUnit[member];
+    assert(unit != null, 'No output unit for member $member');
+    return unit ?? mainOutputUnit;
+  }
+
+  OutputUnit outputUnitForMemberForTesting(MemberEntity member) =>
+      _memberToUnit[member];
+
+  /// Direct access to the output-unit to constants map used for testing.
+  Iterable<ConstantValue> get constantsForTesting => _constantToUnit.keys;
+
+  /// Returns the [OutputUnit] where [constant] belongs.
+  OutputUnit outputUnitForConstant(ConstantValue constant) {
+    if (!isProgramSplit) return mainOutputUnit;
+    OutputUnit unit = _constantToUnit[constant];
+    // TODO(sigmund): enforce unit is not null: it is sometimes null on some
+    // corner cases on internal apps.
+    return unit ?? mainOutputUnit;
+  }
+
+  OutputUnit outputUnitForConstantForTesting(ConstantValue constant) =>
+      _constantToUnit[constant];
+
+  /// Indicates whether [element] is deferred.
+  bool isDeferredClass(ClassEntity element) {
+    return outputUnitForClass(element) != mainOutputUnit;
+  }
+
+  /// Returns `true` if element [to] is reachable from element [from] without
+  /// crossing a deferred import.
+  ///
+  /// For example, if we have two deferred libraries `A` and `B` that both
+  /// import a library `C`, then even though elements from `A` and `C` end up in
+  /// different output units, there is a non-deferred path between `A` and `C`.
+  bool hasOnlyNonDeferredImportPaths(MemberEntity from, MemberEntity to) {
+    OutputUnit outputUnitFrom = outputUnitForMember(from);
+    OutputUnit outputUnitTo = outputUnitForMember(to);
+    if (outputUnitTo == mainOutputUnit) return true;
+    if (outputUnitFrom == mainOutputUnit) return false;
+    return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
+  }
+
+  /// Returns `true` if constant [to] is reachable from element [from] without
+  /// crossing a deferred import.
+  ///
+  /// For example, if we have two deferred libraries `A` and `B` that both
+  /// import a library `C`, then even though elements from `A` and `C` end up in
+  /// different output units, there is a non-deferred path between `A` and `C`.
+  bool hasOnlyNonDeferredImportPathsToConstant(
+      MemberEntity from, ConstantValue to) {
+    OutputUnit outputUnitFrom = outputUnitForMember(from);
+    OutputUnit outputUnitTo = outputUnitForConstant(to);
+    if (outputUnitTo == mainOutputUnit) return true;
+    if (outputUnitFrom == mainOutputUnit) return false;
+    return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
+  }
+
+  /// Returns `true` if class [to] is reachable from element [from] without
+  /// crossing a deferred import.
+  ///
+  /// For example, if we have two deferred libraries `A` and `B` that both
+  /// import a library `C`, then even though elements from `A` and `C` end up in
+  /// different output units, there is a non-deferred path between `A` and `C`.
+  bool hasOnlyNonDeferredImportPathsToClass(MemberEntity from, ClassEntity to) {
+    OutputUnit outputUnitFrom = outputUnitForMember(from);
+    OutputUnit outputUnitTo = outputUnitForClass(to);
+    if (outputUnitTo == mainOutputUnit) return true;
+    if (outputUnitFrom == mainOutputUnit) return false;
+    return outputUnitTo.imports.containsAll(outputUnitFrom.imports);
+  }
+
+  /// Registers that a constant is used in the same deferred output unit as
+  /// [field].
+  void registerConstantDeferredUse(DeferredGlobalConstantValue constant) {
+    if (!isProgramSplit) return;
+    OutputUnit unit = constant.unit;
+    assert(
+        _constantToUnit[constant] == null || _constantToUnit[constant] == unit);
+    _constantToUnit[constant] = unit;
+  }
+
+  /// Returns the unique name for the given deferred [import].
+  String getImportDeferName(Spannable node, ImportEntity import) {
+    String name = importDeferName[import];
+    if (name == null) {
+      throw SpannableAssertionFailure(node, "No deferred name for $import.");
+    }
+    return name;
+  }
+
+  /// Returns the names associated with each deferred import in [unit].
+  Iterable<String> getImportNames(OutputUnit unit) {
+    return unit.imports.map((i) => importDeferName[i]);
+  }
+}
+
+class ImportDescription {
+  /// Relative uri to the importing library.
+  final String importingUri;
+
+  /// The prefix this import is imported as.
+  final String prefix;
+
+  final LibraryEntity importingLibrary;
+
+  ImportDescription.internal(
+      this.importingUri, this.prefix, this.importingLibrary);
+
+  ImportDescription(
+      ImportEntity import, LibraryEntity importingLibrary, Uri mainLibraryUri)
+      : this.internal(
+            fe.relativizeUri(
+                mainLibraryUri, importingLibrary.canonicalUri, false),
+            import.name,
+            importingLibrary);
+}
+
+/// Returns the filename for the output-unit named [name].
+///
+/// The filename is of the form "<main output file>_<name>.part.js".
+/// If [addExtension] is false, the ".part.js" suffix is left out.
+String deferredPartFileName(CompilerOptions options, String name,
+    {bool addExtension = true}) {
+  assert(name != "");
+  String outPath = options.outputUri != null ? options.outputUri.path : "out";
+  String outName = outPath.substring(outPath.lastIndexOf('/') + 1);
+  String extension = addExtension ? ".part.js" : "";
+  return "${outName}_$name$extension";
+}
+
+int compareImportEntities(ImportEntity a, ImportEntity b) {
+  if (a == b) {
+    return 0;
+  } else {
+    return a.uri.path.compareTo(b.uri.path);
+  }
+}
diff --git a/pkg/compiler/lib/src/deferred_load/work_queue.dart b/pkg/compiler/lib/src/deferred_load/work_queue.dart
new file mode 100644
index 0000000..42adfbc
--- /dev/null
+++ b/pkg/compiler/lib/src/deferred_load/work_queue.dart
@@ -0,0 +1,182 @@
+// 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.
+
+part of deferred_load;
+
+/// The deferred_load algorithm work queue.
+class WorkQueue {
+  /// The actual queue of work that needs to be done.
+  final Queue<WorkItem> queue = Queue();
+
+  /// An index to find work items in the queue corresponding to a class.
+  final Map<ClassEntity, WorkItem> pendingClasses = {};
+
+  /// An index to find work items in the queue corresponding to an
+  /// [InterfaceType] represented here by its [ClassEntitiy].
+  final Map<ClassEntity, WorkItem> pendingClassType = {};
+
+  /// An index to find work items in the queue corresponding to a member.
+  final Map<MemberEntity, WorkItem> pendingMembers = {};
+
+  /// An index to find work items in the queue corresponding to a constant.
+  final Map<ConstantValue, WorkItem> pendingConstants = {};
+
+  /// Lattice used to compute unions of [ImportSet]s.
+  final ImportSetLattice _importSets;
+
+  WorkQueue(this._importSets);
+
+  /// Whether there are no more work items in the queue.
+  bool get isNotEmpty => queue.isNotEmpty;
+
+  /// Pop the next element in the queue.
+  WorkItem nextItem() {
+    assert(isNotEmpty);
+    return queue.removeFirst();
+  }
+
+  /// Add to the queue that [element] should be updated to include all imports
+  /// in [importSet]. If there is already a work item in the queue for
+  /// [element], this makes sure that the work item now includes the union of
+  /// [importSet] and the existing work item's import set.
+  void addClass(ClassEntity element, ImportSet importSet) {
+    var item = pendingClasses[element];
+    if (item == null) {
+      item = ClassWorkItem(element, importSet);
+      pendingClasses[element] = item;
+      queue.add(item);
+    } else {
+      item.importsToAdd = _importSets.union(item.importsToAdd, importSet);
+    }
+  }
+
+  /// Add to the queue that class type (represented by [element]) should be
+  /// updated to include all imports in [importSet]. If there is already a
+  /// work item in the queue for [element], this makes sure that the work
+  /// item now includes the union of [importSet] and the existing work
+  /// item's import set.
+  void addClassType(ClassEntity element, ImportSet importSet) {
+    var item = pendingClassType[element];
+    if (item == null) {
+      item = ClassTypeWorkItem(element, importSet);
+      pendingClassType[element] = item;
+      queue.add(item);
+    } else {
+      item.importsToAdd = _importSets.union(item.importsToAdd, importSet);
+    }
+  }
+
+  /// Add to the queue that [element] should be updated to include all imports
+  /// in [importSet]. If there is already a work item in the queue for
+  /// [element], this makes sure that the work item now includes the union of
+  /// [importSet] and the existing work item's import set.
+  void addMember(MemberEntity element, ImportSet importSet) {
+    var item = pendingMembers[element];
+    if (item == null) {
+      item = MemberWorkItem(element, importSet);
+      pendingMembers[element] = item;
+      queue.add(item);
+    } else {
+      item.importsToAdd = _importSets.union(item.importsToAdd, importSet);
+    }
+  }
+
+  /// Add to the queue that [constant] should be updated to include all imports
+  /// in [importSet]. If there is already a work item in the queue for
+  /// [constant], this makes sure that the work item now includes the union of
+  /// [importSet] and the existing work item's import set.
+  void addConstant(ConstantValue constant, ImportSet importSet) {
+    var item = pendingConstants[constant];
+    if (item == null) {
+      item = ConstantWorkItem(constant, importSet);
+      pendingConstants[constant] = item;
+      queue.add(item);
+    } else {
+      item.importsToAdd = _importSets.union(item.importsToAdd, importSet);
+    }
+  }
+}
+
+/// Summary of the work that needs to be done on a class, member, or constant.
+abstract class WorkItem {
+  /// Additional imports that use [element] or [value] and need to be added by
+  /// the algorithm.
+  ///
+  /// This is non-final in case we add more deferred imports to the set before
+  /// the work item is applied (see [WorkQueue.addElement] and
+  /// [WorkQueue.addConstant]).
+  ImportSet importsToAdd;
+
+  WorkItem(this.importsToAdd);
+
+  void update(DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue);
+}
+
+/// Summary of the work that needs to be done on a class.
+class ClassWorkItem extends WorkItem {
+  /// Class to be recursively updated.
+  final ClassEntity cls;
+
+  ClassWorkItem(this.cls, ImportSet newSet) : super(newSet);
+
+  @override
+  void update(
+      DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue) {
+    queue.pendingClasses.remove(cls);
+    ImportSet oldSet = task._classToSet[cls];
+    ImportSet newSet = task.importSets.union(oldSet, importsToAdd);
+    task._updateClassRecursive(closedWorld, cls, oldSet, newSet, queue);
+  }
+}
+
+/// Summary of the work that needs to be done on a class.
+class ClassTypeWorkItem extends WorkItem {
+  /// Class to be recursively updated.
+  final ClassEntity cls;
+
+  ClassTypeWorkItem(this.cls, ImportSet newSet) : super(newSet);
+
+  @override
+  void update(
+      DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue) {
+    queue.pendingClassType.remove(cls);
+    ImportSet oldSet = task._classTypeToSet[cls];
+    ImportSet newSet = task.importSets.union(oldSet, importsToAdd);
+    task._updateClassTypeRecursive(closedWorld, cls, oldSet, newSet, queue);
+  }
+}
+
+/// Summary of the work that needs to be done on a member.
+class MemberWorkItem extends WorkItem {
+  /// Member to be recursively updated.
+  final MemberEntity member;
+
+  MemberWorkItem(this.member, ImportSet newSet) : super(newSet);
+
+  @override
+  void update(
+      DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue) {
+    queue.pendingMembers.remove(member);
+    ImportSet oldSet = task._memberToSet[member];
+    ImportSet newSet = task.importSets.union(oldSet, importsToAdd);
+    task._updateMemberRecursive(closedWorld, member, oldSet, newSet, queue);
+  }
+}
+
+/// Summary of the work that needs to be done on a constant.
+class ConstantWorkItem extends WorkItem {
+  /// Constant to be recursively updated.
+  final ConstantValue constant;
+
+  ConstantWorkItem(this.constant, ImportSet newSet) : super(newSet);
+
+  @override
+  void update(
+      DeferredLoadTask task, KClosedWorld closedWorld, WorkQueue queue) {
+    queue.pendingConstants.remove(constant);
+    ImportSet oldSet = task._constantToSet[constant];
+    ImportSet newSet = task.importSets.union(oldSet, importsToAdd);
+    task._updateConstantRecursive(closedWorld, constant, oldSet, newSet, queue);
+  }
+}
diff --git a/pkg/compiler/lib/src/dump_info.dart b/pkg/compiler/lib/src/dump_info.dart
index c6448fb..5b78296 100644
--- a/pkg/compiler/lib/src/dump_info.dart
+++ b/pkg/compiler/lib/src/dump_info.dart
@@ -19,7 +19,7 @@
 import 'common_elements.dart' show JElementEnvironment;
 import 'compiler.dart' show Compiler;
 import 'constants/values.dart' show ConstantValue, InterceptorConstantValue;
-import 'deferred_load/deferred_load.dart' show OutputUnit, deferredPartFileName;
+import 'deferred_load/output_unit.dart' show OutputUnit, deferredPartFileName;
 import 'elements/entities.dart';
 import 'inferrer/abstract_value_domain.dart';
 import 'inferrer/types.dart'
diff --git a/pkg/compiler/lib/src/js_backend/backend_usage.dart b/pkg/compiler/lib/src/js_backend/backend_usage.dart
index 6b3e73c..425e905 100644
--- a/pkg/compiler/lib/src/js_backend/backend_usage.dart
+++ b/pkg/compiler/lib/src/js_backend/backend_usage.dart
@@ -51,6 +51,9 @@
   /// `true` if 'dart:mirrors' features are used.
   bool get isMirrorsUsed;
 
+  /// `true` if startup timestamps are used.
+  bool get requiresStartupMetrics;
+
   /// `true` if `noSuchMethod` is used.
   bool get isNoSuchMethodUsed;
 
@@ -120,6 +123,9 @@
   /// `true` if a core-library function requires the preamble file to function.
   bool requiresPreamble = false;
 
+  /// `true` if a core-library function accesses startup timestamps.
+  bool requiresStartupMetrics = false;
+
   @override
   bool isFunctionApplyUsed = false;
 
@@ -242,6 +248,8 @@
       isFunctionApplyUsed = true;
     } else if (member.library == _commonElements.mirrorsLibrary) {
       isMirrorsUsed = true;
+    } else if (member == _commonElements.rawStartupMetrics) {
+      requiresStartupMetrics = true;
     }
   }
 
@@ -275,7 +283,7 @@
 
   @override
   BackendUsage close() {
-    return new BackendUsageImpl(
+    return BackendUsageImpl(
         globalFunctionDependencies: _globalFunctionDependencies,
         globalClassDependencies: _globalClassDependencies,
         helperFunctionsUsed: _helperFunctionsUsed,
@@ -283,6 +291,7 @@
         needToInitializeIsolateAffinityTag: _needToInitializeIsolateAffinityTag,
         needToInitializeDispatchProperty: _needToInitializeDispatchProperty,
         requiresPreamble: requiresPreamble,
+        requiresStartupMetrics: requiresStartupMetrics,
         runtimeTypeUses: _runtimeTypeUses,
         isFunctionApplyUsed: isFunctionApplyUsed,
         isMirrorsUsed: isMirrorsUsed,
@@ -317,6 +326,9 @@
   final bool requiresPreamble;
 
   @override
+  final bool requiresStartupMetrics;
+
+  @override
   final bool isFunctionApplyUsed;
 
   @override
@@ -336,6 +348,7 @@
       this.needToInitializeIsolateAffinityTag,
       this.needToInitializeDispatchProperty,
       this.requiresPreamble,
+      this.requiresStartupMetrics,
       Set<RuntimeTypeUse> runtimeTypeUses,
       this.isFunctionApplyUsed,
       this.isMirrorsUsed,
@@ -364,12 +377,13 @@
     bool needToInitializeIsolateAffinityTag = source.readBool();
     bool needToInitializeDispatchProperty = source.readBool();
     bool requiresPreamble = source.readBool();
+    bool requiresStartupMetrics = source.readBool();
     bool isFunctionApplyUsed = source.readBool();
     bool isMirrorsUsed = source.readBool();
     bool isNoSuchMethodUsed = source.readBool();
     bool isHtmlLoaded = source.readBool();
     source.end(tag);
-    return new BackendUsageImpl(
+    return BackendUsageImpl(
         globalFunctionDependencies: globalFunctionDependencies,
         globalClassDependencies: globalClassDependencies,
         helperFunctionsUsed: helperFunctionsUsed,
@@ -378,6 +392,7 @@
         needToInitializeIsolateAffinityTag: needToInitializeIsolateAffinityTag,
         needToInitializeDispatchProperty: needToInitializeDispatchProperty,
         requiresPreamble: requiresPreamble,
+        requiresStartupMetrics: requiresStartupMetrics,
         isFunctionApplyUsed: isFunctionApplyUsed,
         isMirrorsUsed: isMirrorsUsed,
         isNoSuchMethodUsed: isNoSuchMethodUsed,
@@ -399,6 +414,7 @@
     sink.writeBool(needToInitializeIsolateAffinityTag);
     sink.writeBool(needToInitializeDispatchProperty);
     sink.writeBool(requiresPreamble);
+    sink.writeBool(requiresStartupMetrics);
     sink.writeBool(isFunctionApplyUsed);
     sink.writeBool(isMirrorsUsed);
     sink.writeBool(isNoSuchMethodUsed);
diff --git a/pkg/compiler/lib/src/js_backend/specialized_checks.dart b/pkg/compiler/lib/src/js_backend/specialized_checks.dart
index 41e8ea7..431ebe7 100644
--- a/pkg/compiler/lib/src/js_backend/specialized_checks.dart
+++ b/pkg/compiler/lib/src/js_backend/specialized_checks.dart
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import '../common_elements.dart' show ElementEnvironment, JCommonElements;
-import '../deferred_load/deferred_load.dart';
+import '../deferred_load/output_unit.dart';
 import '../elements/entities.dart';
 import '../elements/types.dart';
 import '../js_backend/interceptor_data.dart' show InterceptorData;
diff --git a/pkg/compiler/lib/src/js_emitter/code_emitter_task.dart b/pkg/compiler/lib/src/js_emitter/code_emitter_task.dart
index 0a0a8f7..2337b35 100644
--- a/pkg/compiler/lib/src/js_emitter/code_emitter_task.dart
+++ b/pkg/compiler/lib/src/js_emitter/code_emitter_task.dart
@@ -9,7 +9,7 @@
 import '../common/tasks.dart' show CompilerTask;
 import '../compiler.dart' show Compiler;
 import '../constants/values.dart';
-import '../deferred_load/deferred_load.dart' show OutputUnit;
+import '../deferred_load/output_unit.dart' show OutputUnit;
 import '../elements/entities.dart';
 import '../js/js.dart' as jsAst;
 import '../js_backend/backend.dart' show CodegenInputs;
diff --git a/pkg/compiler/lib/src/js_emitter/main_call_stub_generator.dart b/pkg/compiler/lib/src/js_emitter/main_call_stub_generator.dart
index c969c26..2913bbe 100644
--- a/pkg/compiler/lib/src/js_emitter/main_call_stub_generator.dart
+++ b/pkg/compiler/lib/src/js_emitter/main_call_stub_generator.dart
@@ -14,8 +14,8 @@
 import 'code_emitter_task.dart' show Emitter;
 
 class MainCallStubGenerator {
-  static jsAst.Statement generateInvokeMain(
-      CommonElements commonElements, Emitter emitter, FunctionEntity main) {
+  static jsAst.Statement generateInvokeMain(CommonElements commonElements,
+      Emitter emitter, FunctionEntity main, bool requiresStartupMetrics) {
     jsAst.Expression mainAccess = emitter.staticFunctionAccess(main);
     jsAst.Expression currentScriptAccess =
         emitter.generateEmbeddedGlobalAccess(embeddedNames.CURRENT_SCRIPT);
@@ -90,6 +90,10 @@
         }
       })(function(currentScript) {
         #currentScript = currentScript;
+
+        if (#startupMetrics) {
+          init.#startupMetricsEmbeddedGlobal.add('callMainMs');
+        }
         var callMain = #mainCallClosure;
         if (typeof dartMainRunner === "function") {
           dartMainRunner(callMain, []);
@@ -98,7 +102,9 @@
         }
       })''', {
       'currentScript': currentScriptAccess,
-      'mainCallClosure': mainCallClosure
+      'mainCallClosure': mainCallClosure,
+      'startupMetrics': requiresStartupMetrics,
+      'startupMetricsEmbeddedGlobal': embeddedNames.STARTUP_METRICS,
     });
   }
 }
diff --git a/pkg/compiler/lib/src/js_emitter/metadata_collector.dart b/pkg/compiler/lib/src/js_emitter/metadata_collector.dart
index 6e1cfaa..fafbbc7 100644
--- a/pkg/compiler/lib/src/js_emitter/metadata_collector.dart
+++ b/pkg/compiler/lib/src/js_emitter/metadata_collector.dart
@@ -7,7 +7,7 @@
 import 'package:js_ast/src/precedence.dart' as js_precedence;
 
 import '../common.dart';
-import '../deferred_load/deferred_load.dart' show OutputUnit;
+import '../deferred_load/output_unit.dart' show OutputUnit;
 
 import '../elements/types.dart';
 import '../js/js.dart' as jsAst;
diff --git a/pkg/compiler/lib/src/js_emitter/model.dart b/pkg/compiler/lib/src/js_emitter/model.dart
index 147a80c..19617a0 100644
--- a/pkg/compiler/lib/src/js_emitter/model.dart
+++ b/pkg/compiler/lib/src/js_emitter/model.dart
@@ -6,7 +6,7 @@
 
 import '../common_elements.dart';
 import '../constants/values.dart' show ConstantValue;
-import '../deferred_load/deferred_load.dart' show OutputUnit;
+import '../deferred_load/output_unit.dart' show OutputUnit;
 import '../elements/entities.dart';
 import '../elements/types.dart';
 import '../js/js.dart' as js show Expression, Name, Statement, TokenFinalizer;
diff --git a/pkg/compiler/lib/src/js_emitter/program_builder/program_builder.dart b/pkg/compiler/lib/src/js_emitter/program_builder/program_builder.dart
index f11fd8b..d993bf2 100644
--- a/pkg/compiler/lib/src/js_emitter/program_builder/program_builder.dart
+++ b/pkg/compiler/lib/src/js_emitter/program_builder/program_builder.dart
@@ -9,7 +9,7 @@
 import '../../constants/values.dart'
     show ConstantValue, InterceptorConstantValue;
 import '../../common_elements.dart' show JCommonElements, JElementEnvironment;
-import '../../deferred_load/deferred_load.dart'
+import '../../deferred_load/output_unit.dart'
     show deferredPartFileName, OutputUnit, OutputUnitData;
 import '../../elements/entities.dart';
 import '../../elements/types.dart';
@@ -294,8 +294,8 @@
   }
 
   js.Statement _buildInvokeMain() {
-    return MainCallStubGenerator.generateInvokeMain(
-        _commonElements, _task.emitter, _mainFunction);
+    return MainCallStubGenerator.generateInvokeMain(_commonElements,
+        _task.emitter, _mainFunction, _backendUsage.requiresStartupMetrics);
   }
 
   DeferredFragment _buildDeferredFragment(LibrariesMap librariesMap) {
diff --git a/pkg/compiler/lib/src/js_emitter/runtime_type_generator.dart b/pkg/compiler/lib/src/js_emitter/runtime_type_generator.dart
index d77743f..d8a9417 100644
--- a/pkg/compiler/lib/src/js_emitter/runtime_type_generator.dart
+++ b/pkg/compiler/lib/src/js_emitter/runtime_type_generator.dart
@@ -5,7 +5,7 @@
 library dart2js.js_emitter.runtime_type_generator;
 
 import '../common_elements.dart' show CommonElements;
-import '../deferred_load/deferred_load.dart' show OutputUnit, OutputUnitData;
+import '../deferred_load/output_unit.dart' show OutputUnit, OutputUnitData;
 import '../elements/entities.dart';
 import '../elements/types.dart';
 import '../js/js.dart' as jsAst;
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/emitter.dart
index 3ca343dd..841e2a1 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/emitter.dart
@@ -8,7 +8,7 @@
 import '../../common.dart';
 import '../../common/codegen.dart';
 import '../../constants/values.dart';
-import '../../deferred_load/deferred_load.dart' show OutputUnit;
+import '../../deferred_load/output_unit.dart' show OutputUnit;
 import '../../dump_info.dart';
 import '../../elements/entities.dart';
 import '../../io/source_information.dart';
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
index 7d3ea98..66717cc 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart
@@ -35,6 +35,15 @@
 // reducing their size.
 const String _mainBoilerplate = '''
 (function dartProgram() {
+
+if (#startupMetrics) {
+  // Stash the metrics on the main unit IIFE.
+  // TODO(sra): When the JavaScript local renamer is more intelligent, we can
+  // simply use a local variable.
+  (dartProgram.$STARTUP_METRICS = ${ModelEmitter.startupMetricsGlobal})
+      .add("dartProgramMs");
+}
+
 // Copies the own properties from [from] to [to].
 function copyProperties(from, to) {
   var keys = Object.keys(from);
@@ -469,7 +478,7 @@
 
 // Invokes main (making sure that it records the 'current-script' value).
 #invokeMain;
-})()
+})();
 ''';
 
 /// An expression that returns `true` if `__proto__` can be assigned to stitch
@@ -720,6 +729,7 @@
       'staticStateDeclaration': DeferredHolderParameter(),
       'staticState': DeferredHolderParameter(),
       'holders': holderDeclaration,
+      'startupMetrics': _closedWorld.backendUsage.requiresStartupMetrics,
 
       // Tearoff parameters:
       'tpContainer': js.string(TearOffParametersPropertyNames.container),
@@ -1847,6 +1857,12 @@
                 r'    : "$ti"')
             : js.js(r'Symbol("$ti")')));
 
+    if (_closedWorld.backendUsage.requiresStartupMetrics) {
+      // Copy the metrics object that was stored on the main unit IIFE.
+      globals.add(js.Property(
+          js.string(STARTUP_METRICS), js.js('dartProgram.$STARTUP_METRICS')));
+    }
+
     js.ObjectInitializer globalsObject =
         js.ObjectInitializer(globals, isOneLiner: false);
 
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_merger.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_merger.dart
index 58fc707..faac415 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_merger.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_merger.dart
@@ -4,7 +4,7 @@
 
 import 'dart:collection';
 import '../../common_elements.dart' show ElementEnvironment;
-import '../../deferred_load/deferred_load.dart'
+import '../../deferred_load/output_unit.dart'
     show ImportDescription, OutputUnit, OutputUnitData, deferredPartFileName;
 import '../../elements/entities.dart';
 import '../../js/js.dart' as js;
diff --git a/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart b/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart
index f905d82..9f05395 100644
--- a/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart
+++ b/pkg/compiler/lib/src/js_emitter/startup_emitter/model_emitter.dart
@@ -25,6 +25,7 @@
         NATIVE_SUPERCLASS_TAG_NAME,
         RTI_UNIVERSE,
         RtiUniverseFieldNames,
+        STARTUP_METRICS,
         TearOffParametersPropertyNames,
         TYPE_TO_INTERCEPTOR_MAP,
         TYPES;
@@ -37,7 +38,7 @@
 import '../../constants/values.dart'
     show ConstantValue, FunctionConstantValue, LateSentinelConstantValue;
 import '../../common_elements.dart' show CommonElements, JElementEnvironment;
-import '../../deferred_load/deferred_load.dart' show OutputUnit;
+import '../../deferred_load/output_unit.dart' show OutputUnit;
 import '../../dump_info.dart';
 import '../../elements/entities.dart';
 import '../../elements/types.dart';
@@ -136,6 +137,8 @@
   static const String deferredInitializersGlobal =
       r"$__dart_deferred_initializers__";
 
+  static const String startupMetricsGlobal = r'$__dart_startupMetrics';
+
   static const String partExtension = "part";
   static const String deferredExtension = "part.js";
 
@@ -372,6 +375,32 @@
         {'deferredInitializers': deferredInitializersGlobal});
   }
 
+  js.Statement buildStartupMetrics() {
+    // We want the code that initializes the startup metrics to execute as early
+    // as possible, so it is placed ahead of the main program IIFE instead of,
+    // e.g. as a parameter of the IIFE. It is OK to use a top-level variable,
+    // since the IIFE immediately reads the variable.
+    return js.js.statement('''
+var ${startupMetricsGlobal} =
+(function(){
+  // The timestamp metrics use `performance.now()`. We feature-detect and
+  // fall back on `Date.now()` for JavaScript run in a non-browser evironment.
+  var _performance =
+      (typeof performance == "object" &&
+       performance != null &&
+       typeof performance.now == "function")
+          ? performance
+          : Date;
+  var metrics = {
+    a: [],
+    now: function() { return _performance.now() },
+    add: function(name) { this.a.push(name, this.now()); }
+  };
+  metrics.add('firstMs');
+  return metrics;
+})();''');
+  }
+
   // Writes the given [fragment]'s [code] into a file.
   //
   // Updates the shared [outputBuffers] field with the output.
@@ -395,6 +424,8 @@
       buildGeneratedBy(),
       js.Comment(HOOKS_API_USAGE),
       if (isSplit) buildDeferredInitializerGlobal(),
+      if (_closedWorld.backendUsage.requiresStartupMetrics)
+        buildStartupMetrics(),
       code
     ]);
 
diff --git a/pkg/compiler/lib/src/js_model/element_map_impl.dart b/pkg/compiler/lib/src/js_model/element_map_impl.dart
index a2f7118..22d4a61 100644
--- a/pkg/compiler/lib/src/js_model/element_map_impl.dart
+++ b/pkg/compiler/lib/src/js_model/element_map_impl.dart
@@ -17,7 +17,7 @@
 import '../common/names.dart';
 import '../common_elements.dart';
 import '../constants/values.dart';
-import '../deferred_load/deferred_load.dart';
+import '../deferred_load/output_unit.dart';
 import '../elements/entities.dart';
 import '../elements/entity_utils.dart' as utils;
 import '../elements/indexed.dart';
diff --git a/pkg/compiler/lib/src/js_model/js_strategy.dart b/pkg/compiler/lib/src/js_model/js_strategy.dart
index c24a46b..0e57dd9 100644
--- a/pkg/compiler/lib/src/js_model/js_strategy.dart
+++ b/pkg/compiler/lib/src/js_model/js_strategy.dart
@@ -13,7 +13,7 @@
 import '../common/work.dart';
 import '../common_elements.dart' show CommonElements, ElementEnvironment;
 import '../compiler.dart';
-import '../deferred_load/deferred_load.dart' hide WorkItem;
+import '../deferred_load/output_unit.dart';
 import '../dump_info.dart';
 import '../elements/entities.dart';
 import '../enqueue.dart';
diff --git a/pkg/compiler/lib/src/js_model/js_world.dart b/pkg/compiler/lib/src/js_model/js_world.dart
index f2c3c08..e5956fd 100644
--- a/pkg/compiler/lib/src/js_model/js_world.dart
+++ b/pkg/compiler/lib/src/js_model/js_world.dart
@@ -9,7 +9,7 @@
 import '../common.dart';
 import '../common/names.dart';
 import '../common_elements.dart' show JCommonElements, JElementEnvironment;
-import '../deferred_load/deferred_load.dart';
+import '../deferred_load/output_unit.dart';
 import '../elements/entities.dart';
 import '../elements/entity_utils.dart' as utils;
 import '../elements/names.dart';
diff --git a/pkg/compiler/lib/src/js_model/js_world_builder.dart b/pkg/compiler/lib/src/js_model/js_world_builder.dart
index 087d57a..a5ad110 100644
--- a/pkg/compiler/lib/src/js_model/js_world_builder.dart
+++ b/pkg/compiler/lib/src/js_model/js_world_builder.dart
@@ -9,7 +9,7 @@
 import '../common_elements.dart';
 import '../constants/constant_system.dart' as constant_system;
 import '../constants/values.dart';
-import '../deferred_load/deferred_load.dart';
+import '../deferred_load/output_unit.dart';
 import '../elements/entities.dart';
 import '../elements/indexed.dart';
 import '../elements/names.dart';
@@ -256,7 +256,7 @@
           map.toBackendType(runtimeTypeUse.argumentType));
     }).toSet();
 
-    return new BackendUsageImpl(
+    return BackendUsageImpl(
         globalFunctionDependencies: globalFunctionDependencies,
         globalClassDependencies: globalClassDependencies,
         helperFunctionsUsed: helperFunctionsUsed,
@@ -266,6 +266,7 @@
         needToInitializeDispatchProperty:
             backendUsage.needToInitializeDispatchProperty,
         requiresPreamble: backendUsage.requiresPreamble,
+        requiresStartupMetrics: backendUsage.requiresStartupMetrics,
         runtimeTypeUses: runtimeTypeUses,
         isFunctionApplyUsed: backendUsage.isFunctionApplyUsed,
         isMirrorsUsed: backendUsage.isMirrorsUsed,
diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart
index 74ad9f6..022bb9c 100644
--- a/pkg/compiler/lib/src/kernel/dart2js_target.dart
+++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart
@@ -131,7 +131,9 @@
   @override
   bool allowPlatformPrivateLibraryAccess(Uri importer, Uri imported) =>
       super.allowPlatformPrivateLibraryAccess(importer, imported) ||
-      maybeEnableNative(importer);
+      maybeEnableNative(importer) ||
+      (importer.scheme == 'package' &&
+          importer.path.startsWith('dart2js_runtime_metrics/'));
 
   @override
   bool enableNative(Uri uri) => maybeEnableNative(uri);
@@ -235,6 +237,7 @@
 // compile-platform should just specify which libraries to compile instead.
 const _requiredLibraries = const <String, List<String>>{
   'dart2js': const <String>[
+    'dart:_dart2js_runtime_metrics',
     'dart:_foreign_helper',
     'dart:_interceptors',
     'dart:_internal',
@@ -258,6 +261,7 @@
     'dart:web_sql',
   ],
   'dart2js_server': const <String>[
+    'dart:_dart2js_runtime_metrics',
     'dart:_foreign_helper',
     'dart:_interceptors',
     'dart:_internal',
diff --git a/pkg/compiler/lib/src/serialization/serialization.dart b/pkg/compiler/lib/src/serialization/serialization.dart
index 590efec..8e2cf4d 100644
--- a/pkg/compiler/lib/src/serialization/serialization.dart
+++ b/pkg/compiler/lib/src/serialization/serialization.dart
@@ -9,7 +9,7 @@
 import '../closure.dart';
 import '../constants/constant_system.dart' as constant_system;
 import '../constants/values.dart';
-import '../deferred_load/deferred_load.dart';
+import '../deferred_load/output_unit.dart';
 import '../diagnostics/source_span.dart';
 import '../elements/entities.dart';
 import '../elements/indexed.dart';
diff --git a/pkg/compiler/lib/src/ssa/builder_kernel.dart b/pkg/compiler/lib/src/ssa/builder_kernel.dart
index 9afc681..b2839cb 100644
--- a/pkg/compiler/lib/src/ssa/builder_kernel.dart
+++ b/pkg/compiler/lib/src/ssa/builder_kernel.dart
@@ -12,7 +12,7 @@
 import '../common_elements.dart';
 import '../constants/constant_system.dart' as constant_system;
 import '../constants/values.dart';
-import '../deferred_load/deferred_load.dart';
+import '../deferred_load/output_unit.dart';
 import '../dump_info.dart';
 import '../elements/entities.dart';
 import '../elements/jumps.dart';
diff --git a/pkg/compiler/lib/src/world.dart b/pkg/compiler/lib/src/world.dart
index a17fc60..72b3ec0 100644
--- a/pkg/compiler/lib/src/world.dart
+++ b/pkg/compiler/lib/src/world.dart
@@ -12,7 +12,7 @@
         JElementEnvironment,
         KCommonElements,
         KElementEnvironment;
-import 'deferred_load/deferred_load.dart';
+import 'deferred_load/output_unit.dart';
 import 'diagnostics/diagnostic_listener.dart';
 import 'elements/entities.dart';
 import 'elements/names.dart';
diff --git a/pkg/compiler/test/deferred/constant_emission_test_helper.dart b/pkg/compiler/test/deferred/constant_emission_test_helper.dart
index 0f05dc3..e7bb164 100644
--- a/pkg/compiler/test/deferred/constant_emission_test_helper.dart
+++ b/pkg/compiler/test/deferred/constant_emission_test_helper.dart
@@ -9,7 +9,7 @@
 
 import 'package:compiler/src/compiler.dart';
 import 'package:compiler/src/constants/values.dart';
-import 'package:compiler/src/deferred_load/deferred_load.dart';
+import 'package:compiler/src/deferred_load/output_unit.dart';
 import 'package:compiler/src/elements/entities.dart';
 import 'package:compiler/src/elements/types.dart';
 import 'package:compiler/src/js_emitter/model.dart';
diff --git a/pkg/compiler/test/deferred/load_graph_segmentation_test.dart b/pkg/compiler/test/deferred/load_graph_segmentation_test.dart
index 64ec62d..67954ca 100644
--- a/pkg/compiler/test/deferred/load_graph_segmentation_test.dart
+++ b/pkg/compiler/test/deferred/load_graph_segmentation_test.dart
@@ -10,7 +10,7 @@
 
 import 'package:async_helper/async_helper.dart';
 import 'package:compiler/src/compiler.dart';
-import 'package:compiler/src/deferred_load/deferred_load.dart';
+import 'package:compiler/src/deferred_load/output_unit.dart';
 import 'package:compiler/src/js_emitter/startup_emitter/fragment_merger.dart';
 import 'package:expect/expect.dart';
 import '../helpers/memory_compiler.dart';
diff --git a/pkg/compiler/test/deferred_loading/deferred_loading_test.dart b/pkg/compiler/test/deferred_loading/deferred_loading_test.dart
index aecbf54..57d6c44 100644
--- a/pkg/compiler/test/deferred_loading/deferred_loading_test.dart
+++ b/pkg/compiler/test/deferred_loading/deferred_loading_test.dart
@@ -6,7 +6,6 @@
 
 import 'dart:io' hide Link;
 import 'package:async_helper/async_helper.dart';
-import 'package:compiler/src/deferred_load/deferred_load.dart';
 import '../equivalence/id_equivalence_helper.dart';
 import 'deferred_loading_test_helper.dart';
 
diff --git a/pkg/compiler/test/deferred_loading/deferred_loading_test_helper.dart b/pkg/compiler/test/deferred_loading/deferred_loading_test_helper.dart
index 73271f9..435002d 100644
--- a/pkg/compiler/test/deferred_loading/deferred_loading_test_helper.dart
+++ b/pkg/compiler/test/deferred_loading/deferred_loading_test_helper.dart
@@ -8,7 +8,7 @@
 import 'package:compiler/src/closure.dart';
 import 'package:compiler/src/common.dart';
 import 'package:compiler/src/compiler.dart';
-import 'package:compiler/src/deferred_load/deferred_load.dart';
+import 'package:compiler/src/deferred_load/output_unit.dart';
 import 'package:compiler/src/elements/entities.dart';
 import 'package:compiler/src/ir/util.dart';
 import 'package:compiler/src/js_model/element_map.dart';
diff --git a/pkg/compiler/test/helpers/program_lookup.dart b/pkg/compiler/test/helpers/program_lookup.dart
index c6f3393..d1b5599 100644
--- a/pkg/compiler/test/helpers/program_lookup.dart
+++ b/pkg/compiler/test/helpers/program_lookup.dart
@@ -6,7 +6,7 @@
 
 import 'package:expect/expect.dart';
 import 'package:compiler/src/common_elements.dart';
-import 'package:compiler/src/deferred_load/deferred_load.dart';
+import 'package:compiler/src/deferred_load/output_unit.dart';
 import 'package:compiler/src/elements/entities.dart';
 import 'package:compiler/src/js_backend/namer.dart';
 import 'package:compiler/src/js_emitter/model.dart';
diff --git a/pkg/dart2js_runtime_metrics/CHANGELOG.md b/pkg/dart2js_runtime_metrics/CHANGELOG.md
new file mode 100644
index 0000000..a0712a7
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0
+
+- Initial version.
diff --git a/pkg/dart2js_runtime_metrics/LICENSE b/pkg/dart2js_runtime_metrics/LICENSE
new file mode 100644
index 0000000..a217815
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2021, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkg/dart2js_runtime_metrics/README.md b/pkg/dart2js_runtime_metrics/README.md
new file mode 100644
index 0000000..042f1d9
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/README.md
@@ -0,0 +1,13 @@
+☠☠ **Warning: This package is experimental and may be removed in a future
+version of Dart.** ☠☠
+
+`dart2js` can generate extra code to measure certain activities.
+This library provides access to the measurements at runtime.
+
+An application might make timings and other measurements and report them back to
+a server in order to collect information on how the application is working in
+production. The APIs in this library provide access to measurements that require
+help from dart2js. For example, `startupMetrics` accesses measurements of
+activities that happen as the program is loaded, before `main()`.
+
+The APIs are stubbed so that dummy values are returned on the VM and dartdevc.
diff --git a/pkg/dart2js_runtime_metrics/analysis_options.yaml b/pkg/dart2js_runtime_metrics/analysis_options.yaml
new file mode 100644
index 0000000..edcd64e
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/analysis_options.yaml
@@ -0,0 +1,5 @@
+analyzer:
+  errors:
+    import_internal_library: ignore
+  strong-mode:
+    implicit-casts: false
diff --git a/pkg/dart2js_runtime_metrics/lib/_startup_metrics_dart2js.dart b/pkg/dart2js_runtime_metrics/lib/_startup_metrics_dart2js.dart
new file mode 100644
index 0000000..021c666
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/lib/_startup_metrics_dart2js.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'dart:_dart2js_runtime_metrics' show startupMetrics;
diff --git a/pkg/dart2js_runtime_metrics/lib/_startup_metrics_dartdevc.dart b/pkg/dart2js_runtime_metrics/lib/_startup_metrics_dartdevc.dart
new file mode 100644
index 0000000..4f4fe5b
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/lib/_startup_metrics_dartdevc.dart
@@ -0,0 +1,12 @@
+// 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.
+
+/// A collection of metrics for events that happen before `main()` is entered.
+///
+/// The contents of the map depend on the platform. The map values are simple
+/// objects (strings, numbers, Booleans). There is always an entry for the key
+/// `'runtime'` with a [String] value.
+Map<String, Object> get startupMetrics {
+  return {'runtime': 'dartdevc'};
+}
diff --git a/pkg/dart2js_runtime_metrics/lib/_startup_metrics_unknown.dart b/pkg/dart2js_runtime_metrics/lib/_startup_metrics_unknown.dart
new file mode 100644
index 0000000..7f8dd74
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/lib/_startup_metrics_unknown.dart
@@ -0,0 +1,12 @@
+// 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.
+
+/// A collection of metrics for events that happen before `main()` is entered.
+///
+/// The contents of the map depend on the platform. The map values are simple
+/// objects (strings, numbers, Booleans). There is always an entry for the key
+/// `'runtime'` with a [String] value.
+Map<String, Object> get startupMetrics {
+  return {'runtime': 'unknown'};
+}
diff --git a/pkg/dart2js_runtime_metrics/lib/_startup_metrics_vm.dart b/pkg/dart2js_runtime_metrics/lib/_startup_metrics_vm.dart
new file mode 100644
index 0000000..090a914
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/lib/_startup_metrics_vm.dart
@@ -0,0 +1,12 @@
+// 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.
+
+/// A collection of metrics for events that happen before `main()` is entered.
+///
+/// The contents of the map depend on the platform. The map values are simple
+/// objects (strings, numbers, Booleans). There is always an entry for the key
+/// `'runtime'` with a [String] value.
+Map<String, Object> get startupMetrics {
+  return {'runtime': 'vm'};
+}
diff --git a/pkg/dart2js_runtime_metrics/lib/startup_metrics.dart b/pkg/dart2js_runtime_metrics/lib/startup_metrics.dart
new file mode 100644
index 0000000..c3f7d08
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/lib/startup_metrics.dart
@@ -0,0 +1,8 @@
+// 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.
+
+export '_startup_metrics_unknown.dart'
+    if (dart.library._dart2js_runtime_metrics) '_startup_metrics_dart2js.dart'
+    if (dart.library.ffi) '_startup_metrics_vm.dart'
+    if (dart.library.js) '_startup_metrics_dartdevc.dart';
diff --git a/pkg/dart2js_runtime_metrics/pubspec.yaml b/pkg/dart2js_runtime_metrics/pubspec.yaml
new file mode 100644
index 0000000..c79dd5a
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/pubspec.yaml
@@ -0,0 +1,18 @@
+name: dart2js_runtime_metrics
+# This package is not intended for consumption on pub.dev. DO NOT publish.
+publish_to: none
+version: 0.1.0
+repository: https://github.com/dart-lang/sdk/tree/master/pkg/dart2js_runtime_metrics
+description: >-
+  `dart2js` can generate extra code to measure certain activities.
+  This library provides access to the measurements at runtime.
+
+environment:
+  # Restrict the upper bound so that we can remove support for this in
+  # a later version of the SDK without it being a breaking change.
+  sdk: ">=2.14.0 <2.15.0"
+
+dev_dependencies:
+  # Unpublished packages that can be used via path dependency
+  expect:
+    path: ../expect
diff --git a/pkg/dart2js_runtime_metrics/test/startup_metrics_test.dart b/pkg/dart2js_runtime_metrics/test/startup_metrics_test.dart
new file mode 100644
index 0000000..5655927
--- /dev/null
+++ b/pkg/dart2js_runtime_metrics/test/startup_metrics_test.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:dart2js_runtime_metrics/startup_metrics.dart';
+import 'package:expect/expect.dart';
+
+void main() {
+  Map<String, Object> metrics = startupMetrics;
+
+  print('metrics: $metrics');
+
+  String expectedRuntime;
+  if (1.0 is! int) {
+    expectedRuntime = 'vm';
+  } else if (ClassWithLongName().toString().contains('minified:')) {
+    // dart2js minified: "Instance of 'minified:xy'".
+    expectedRuntime = 'dart2js';
+  } else if ('$main' == "Closure 'main'") {
+    // dart2js non-minified.
+    expectedRuntime = 'dart2js';
+  } else if ('$main'.startsWith('Closure: () => void from: function main()')) {
+    expectedRuntime = 'dartdevc';
+  } else {
+    throw 'Cannot feature-test current runtime:'
+        '\nmetrics = $metrics\n main = $main';
+  }
+
+  Expect.isTrue(metrics.containsKey('runtime'), "Has 'runtime' key: $metrics");
+  Expect.equals(expectedRuntime, metrics['runtime'],
+      "Expected 'runtime: $expectedRuntime': $metrics");
+
+  if (expectedRuntime == 'dart2js') {
+    Expect.isTrue(metrics.containsKey('callMainMs'));
+    return;
+  }
+
+  if (expectedRuntime == 'dartdevc') {
+    Expect.equals(1, metrics.length);
+    return;
+  }
+
+  if (expectedRuntime == 'vm') {
+    Expect.equals(1, metrics.length);
+    return;
+  }
+
+  throw 'Should not get here.';
+}
+
+class ClassWithLongName {}
diff --git a/pkg/dds/dds_protocol.md b/pkg/dds/dds_protocol.md
index b690c1a..9224302 100644
--- a/pkg/dds/dds_protocol.md
+++ b/pkg/dds/dds_protocol.md
@@ -1,6 +1,6 @@
-# Dart Development Service Protocol 1.2
+# Dart Development Service Protocol 1.3
 
-This document describes _version 1.2_ of the Dart Development Service Protocol.
+This document describes _version 1.3_ of the Dart Development Service Protocol.
 This protocol is an extension of the Dart VM Service Protocol and implements it
 in it's entirety. For details on the VM Service Protocol, see the [Dart VM Service Protocol Specification][service-protocol].
 
@@ -67,6 +67,29 @@
 
 The DDS Protocol supports all [public RPCs defined in the VM Service protocol][service-protocol-public-rpcs].
 
+### getAvailableCachedCpuSamples
+
+```
+AvailableCachedCpuSamples getAvailableCachedCpuSamples();
+```
+
+The _getAvailableCachedCpuSamples_ RPC is used to determine which caches of CPU samples
+are available. Caches are associated with individual _UserTag_ names and are specified
+when DDS is started via the _cachedUserTags_ parameter.
+
+See [AvailableCachedCpuSamples](#availablecachedcpusamples).
+
+### getCachedCpuSamples
+
+```
+CachedCpuSamples getCachedCpuSamples(string isolateId, string userTag);
+```
+
+The _getCachedCpuSamples_ RPC is used to retrieve a cache of CPU samples collected
+under a _UserTag_ with name _userTag_.
+
+See [CachedCpuSamples](#cachedcpusamples).
+
 ### getClientName
 
 ```
@@ -181,6 +204,37 @@
 
 The DDS Protocol supports all [public types defined in the VM Service protocol][service-protocol-public-types].
 
+### AvailableCachedCpuSamples
+
+```
+class AvailableCachedCpuSamples extends Response {
+  // A list of UserTag names associated with CPU sample caches.
+  string[] cacheNames;
+}
+```
+
+A collection of [UserTag] names associated with caches of CPU samples.
+
+See [getAvailableCachedCpuSamples](#getavailablecachedcpusamples).
+
+### CachedCpuSamples
+
+```
+class CachedCpuSamples extends CpuSamples {
+  // The name of the UserTag associated with this cache of samples.
+  string userTag;
+
+  // Provided if the CPU sample cache has filled and older samples have been
+  // dropped.
+  bool truncated [optional];
+}
+```
+
+An extension of [CpuSamples](#cpu-samples) which represents a set of cached
+samples, associated with a particular [UserTag] name.
+
+See [getCachedCpuSamples](#getcachedcpusamples).
+
 ### ClientName
 
 ```
@@ -220,10 +274,12 @@
 1.0 | Initial revision
 1.1 | Added `getDartDevelopmentServiceVersion` RPC.
 1.2 | Added `getStreamHistory` RPC.
+1.3 | Added `getAvailableCachedCpuSamples` and `getCachedCpuSamples` RPCs.
 
 [resume]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#resume
 [success]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#success
 [version]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#version
+[cpu-samples]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#cpusamples
 
 [service-protocol]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md
 [service-protocol-rpcs-requests-and-responses]: https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses
diff --git a/pkg/dds/lib/dds.dart b/pkg/dds/lib/dds.dart
index e8f05bb..f31e447 100644
--- a/pkg/dds/lib/dds.dart
+++ b/pkg/dds/lib/dds.dart
@@ -42,6 +42,7 @@
     Uri? serviceUri,
     bool enableAuthCodes = true,
     bool ipv6 = false,
+    List<String> cachedUserTags = const [],
     DevToolsConfiguration? devToolsConfiguration,
     bool logRequests = false,
   }) async {
@@ -79,6 +80,7 @@
       remoteVmServiceUri,
       serviceUri,
       enableAuthCodes,
+      cachedUserTags,
       ipv6,
       devToolsConfiguration,
       logRequests,
@@ -136,9 +138,13 @@
   /// requests.
   bool get isRunning;
 
+  /// The list of [UserTag]s used to determine which CPU samples are cached by
+  /// DDS.
+  List<String> get cachedUserTags;
+
   /// The version of the DDS protocol supported by this [DartDevelopmentService]
   /// instance.
-  static const String protocolVersion = '1.2';
+  static const String protocolVersion = '1.3';
 }
 
 class DartDevelopmentServiceException implements Exception {
diff --git a/pkg/dds/lib/src/client.dart b/pkg/dds/lib/src/client.dart
index 1df3a3a..771ccee 100644
--- a/pkg/dds/lib/src/client.dart
+++ b/pkg/dds/lib/src/client.dart
@@ -206,6 +206,19 @@
       return supportedProtocols;
     });
 
+    _clientPeer.registerMethod(
+      'getAvailableCachedCpuSamples',
+      (_) => {
+        'type': 'AvailableCachedCpuSamples',
+        'cacheNames': dds.cachedUserTags,
+      },
+    );
+
+    _clientPeer.registerMethod(
+      'getCachedCpuSamples',
+      dds.isolateManager.getCachedCpuSamples,
+    );
+
     // `evaluate` and `evaluateInFrame` actually consist of multiple RPC
     // invocations, including a call to `compileExpression` which can be
     // overridden by clients which provide their own implementation (e.g.,
diff --git a/pkg/dds/lib/src/common/ring_buffer.dart b/pkg/dds/lib/src/common/ring_buffer.dart
new file mode 100644
index 0000000..9ae802d
--- /dev/null
+++ b/pkg/dds/lib/src/common/ring_buffer.dart
@@ -0,0 +1,68 @@
+// 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:math';
+
+class RingBuffer<T> {
+  RingBuffer(this._bufferSize) {
+    _buffer = List<T?>.filled(
+      _bufferSize,
+      null,
+    );
+  }
+
+  Iterable<T> call() sync* {
+    for (int i = _size - 1; i >= 0; --i) {
+      yield _buffer[(_count - i - 1) % _bufferSize]!;
+    }
+  }
+
+  /// Inserts a new element into the [RingBuffer].
+  /// 
+  /// Returns the element evicted as a result of adding the new element if the
+  /// buffer is as max capacity, null otherwise.
+  T? add(T e) {
+    if (_buffer.isEmpty) {
+      return null;
+    }
+    T? evicted;
+    final index = _count % _bufferSize;
+    if (isTruncated) {
+      evicted = _buffer[index];
+    }
+    _buffer[index] = e;
+    _count++;
+    return evicted;
+  }
+
+  void resize(int size) {
+    assert(size >= 0);
+    if (size == _bufferSize) {
+      return;
+    }
+    final resized = List<T?>.filled(
+      size,
+      null,
+    );
+    int count = 0;
+    if (size > 0) {
+      for (final e in this()) {
+        resized[count++ % size] = e;
+      }
+    }
+    _count = count;
+    _bufferSize = size;
+    _buffer = resized;
+  }
+
+  bool get isTruncated => _count % bufferSize < _count;
+
+  int get bufferSize => _bufferSize;
+
+  int get _size => min(_count, _bufferSize);
+
+  int _bufferSize;
+  int _count = 0;
+  late List<T?> _buffer;
+}
diff --git a/pkg/dds/lib/src/cpu_samples_manager.dart b/pkg/dds/lib/src/cpu_samples_manager.dart
new file mode 100644
index 0000000..1fe5084
--- /dev/null
+++ b/pkg/dds/lib/src/cpu_samples_manager.dart
@@ -0,0 +1,201 @@
+// 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 'package:dds/src/common/ring_buffer.dart';
+import 'package:vm_service/vm_service.dart';
+
+import 'dds_impl.dart';
+
+/// Manages CPU sample caches for an individual [Isolate].
+class CpuSamplesManager {
+  CpuSamplesManager(this.dds, this.isolateId) {
+    for (final userTag in dds.cachedUserTags) {
+      cpuSamplesCaches[userTag] = CpuSamplesRepository(userTag);
+    }
+  }
+
+  void handleUserTagEvent(Event event) {
+    assert(event.kind! == EventKind.kUserTagChanged);
+    _currentTag = event.updatedTag!;
+    final previousTag = event.previousTag!;
+    if (cpuSamplesCaches.containsKey(previousTag)) {
+      _lastCachedTag = previousTag;
+    }
+  }
+
+  void handleCpuSamplesEvent(Event event) {
+    assert(event.kind! == EventKind.kCpuSamples);
+    // There might be some samples left in the buffer for the previously set
+    // user tag. We'll check for them here and then close out the cache.
+    if (_lastCachedTag != null) {
+      cpuSamplesCaches[_lastCachedTag]!.cacheSamples(
+        event.cpuSamples!,
+      );
+      _lastCachedTag = null;
+    }
+    cpuSamplesCaches[_currentTag]?.cacheSamples(event.cpuSamples!);
+  }
+
+  final DartDevelopmentServiceImpl dds;
+  final String isolateId;
+  final cpuSamplesCaches = <String, CpuSamplesRepository>{};
+
+  String _currentTag = '';
+  String? _lastCachedTag;
+}
+
+class CpuSamplesRepository extends RingBuffer<CpuSample> {
+  // TODO: math to figure out proper buffer sizes.
+  CpuSamplesRepository(
+    this.tag, [
+    int bufferSize = 1000000,
+  ]) : super(bufferSize);
+
+  void cacheSamples(CpuSamples samples) {
+    String getFunctionId(ProfileFunction function) {
+      final functionObject = function.function;
+      if (functionObject is NativeFunction) {
+        return 'native/${functionObject.name}';
+      }
+      return functionObject.id!;
+    }
+
+    // Initialize upon seeing our first samples.
+    if (functions.isEmpty) {
+      samplePeriod = samples.samplePeriod!;
+      maxStackDepth = samples.maxStackDepth!;
+      pid = samples.pid!;
+      functions.addAll(samples.functions!);
+
+      // Build the initial id to function index mapping. This allows for us to
+      // lookup a ProfileFunction in the global function list stored in this
+      // cache. This works since most ProfileFunction objects will have an
+      // associated function with a *typically* stable service ID that we can
+      // use as a key.
+      //
+      // TODO(bkonyi): investigate creating some form of stable ID for
+      // Functions tied to closures.
+      for (int i = 0; i < functions.length; ++i) {
+        idToFunctionIndex[getFunctionId(functions[i])] = i;
+      }
+
+      // Clear tick information as we'll need to recalculate these values later
+      // when a request for samples from this repository is received.
+      for (final f in functions) {
+        f.inclusiveTicks = 0;
+        f.exclusiveTicks = 0;
+      }
+
+      _firstSampleTimestamp = samples.timeOriginMicros!;
+    } else {
+      final newFunctions = samples.functions!;
+      final indexMapping = <int, int>{};
+
+      // Check to see if we've got a function object we've never seen before.
+      for (int i = 0; i < newFunctions.length; ++i) {
+        final key = getFunctionId(newFunctions[i]);
+        if (!idToFunctionIndex.containsKey(key)) {
+          idToFunctionIndex[key] = functions.length;
+          // Keep track of the original index and the location of the function
+          // in the master function list so we can update the function indicies
+          // for each sample in this batch.
+          indexMapping[i] = functions.length;
+          functions.add(newFunctions[i]);
+
+          // Reset tick state as we'll recalculate later.
+          functions.last.inclusiveTicks = 0;
+          functions.last.exclusiveTicks = 0;
+        }
+      }
+
+      // Update the indicies into the function table for functions that were
+      // newly processed in the most recent event.
+      for (final sample in samples.samples!) {
+        final stack = sample.stack!;
+        for (int i = 0; i < stack.length; ++i) {
+          if (indexMapping.containsKey(stack[i])) {
+            stack[i] = indexMapping[stack[i]]!;
+          }
+        }
+      }
+    }
+
+    final relevantSamples = samples.samples!.where((s) => s.userTag == tag);
+    for (final sample in relevantSamples) {
+      add(sample);
+    }
+  }
+
+  @override
+  CpuSample? add(CpuSample sample) {
+    final evicted = super.add(sample);
+
+    void updateTicksForSample(CpuSample sample, int increment) {
+      final stack = sample.stack!;
+      for (int i = 0; i < stack.length; ++i) {
+        final function = functions[stack[i]];
+        function.inclusiveTicks = function.inclusiveTicks! + increment;
+        if (i + 1 == stack.length) {
+          function.exclusiveTicks = function.exclusiveTicks! + increment;
+        }
+      }
+    }
+
+    if (evicted != null) {
+      // If a sample is evicted from the cache, we need to decrement the tick
+      // counters for each function in the sample's stack.
+      updateTicksForSample(sample, -1);
+
+      // We also need to change the first timestamp to that of the next oldest
+      // sample.
+      _firstSampleTimestamp = call().first.timestamp!;
+    }
+    _lastSampleTimestamp = sample.timestamp!;
+
+    // Update function ticks to include the new sample.
+    updateTicksForSample(sample, 1);
+
+    return evicted;
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'type': 'CachedCpuSamples',
+      'userTag': tag,
+      'truncated': isTruncated,
+      if (functions.isNotEmpty) ...{
+        'samplePeriod': samplePeriod,
+        'maxStackDepth': maxStackDepth,
+      },
+      'timeOriginMicros': _firstSampleTimestamp,
+      'timeExtentMicros': _lastSampleTimestamp - _firstSampleTimestamp,
+      'functions': [
+        // TODO(bkonyi): remove functions with no ticks and update sample stacks.
+        for (final f in functions) f.toJson(),
+      ],
+      'sampleCount': call().length,
+      'samples': [
+        for (final s in call()) s.toJson(),
+      ]
+    };
+  }
+
+  /// The UserTag associated with all samples stored in this repository.
+  final String tag;
+
+  /// The list of function references with corresponding profiler tick data.
+  /// ** NOTE **: The tick values here need to be updated as new CpuSamples
+  /// events are delivered.
+  final functions = <ProfileFunction>[];
+  final idToFunctionIndex = <String, int>{};
+
+  /// Assume sample period and max stack depth won't change.
+  late final int samplePeriod;
+  late final int maxStackDepth;
+
+  late final int pid;
+
+  int _firstSampleTimestamp = 0;
+  int _lastSampleTimestamp = 0;
+}
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 64b66be..e9710c3 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -7,6 +7,8 @@
 
 import 'package:collection/collection.dart';
 import 'package:meta/meta.dart';
+import 'package:package_config/package_config.dart';
+import 'package:path/path.dart' as path;
 import 'package:vm_service/vm_service.dart' as vm;
 
 import '../../../dds.dart';
@@ -122,6 +124,17 @@
   /// have been called.
   late final bool isAttach;
 
+  /// A list of all possible project paths that should be considered the users
+  /// own code.
+  ///
+  /// This is made up of the folder containing the 'program' being executed, the
+  /// 'cwd' and any 'additionalProjectPaths' from the launch arguments.
+  late final List<String> projectPaths = [
+    args.cwd,
+    path.dirname(args.program),
+    ...?args.additionalProjectPaths,
+  ].whereNotNull().toList();
+
   DartDebugAdapter(
     ByteStreamServerChannel channel, {
     this.ipv6 = false,
@@ -533,6 +546,12 @@
     sendResponse();
   }
 
+  /// Resolves a `package: URI` to the real underlying source path.
+  ///
+  /// Returns `null` if no mapping was possible, for example if the package is
+  /// not in the package mapping file.
+  String? resolvePackageUri(Uri uri) => _converter.resolvePackageUri(uri);
+
   /// [scopesRequest] is called by the client to request all of the variables
   /// scopes available for a given stack frame.
   @override
@@ -715,7 +734,10 @@
       // The VM doesn't support fetching an arbitrary slice of frames, only a
       // maximum limit, so if the client asks for frames 20-30 we must send a
       // request for the first 30 and trim them ourselves.
-      final limit = startFrame + numFrames;
+
+      // DAP says if numFrames is 0 or missing (which we swap to 0 above) we
+      // should return all.
+      final limit = numFrames == 0 ? null : startFrame + numFrames;
       final stack = await vmService?.getStack(thread.isolate.id!, limit: limit);
       final frames = stack?.frames;
 
@@ -820,6 +842,18 @@
     sendResponse(ThreadsResponseBody(threads: threads));
   }
 
+  /// Sets the package config file to use for `package: URI` resolution.
+  ///
+  /// TODO(dantup): Remove this once
+  ///   https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
+  ///   necessary.
+  void usePackageConfigFile(File packageConfig) {
+    _converter.packageConfig = PackageConfig.parseString(
+      packageConfig.readAsStringSync(),
+      Uri.file(packageConfig.path),
+    );
+  }
+
   /// [variablesRequest] is called by the client to request child variables for
   /// a given variables variablesReference.
   ///
@@ -1104,6 +1138,14 @@
   final List<String>? vmAdditionalArgs;
   final bool? enableAsserts;
 
+  /// Paths that should be considered the users local code.
+  ///
+  /// These paths will generally be all of the open folders in the users editor
+  /// and are used to determine whether a library is "external" or not to
+  /// support debugging "just my code" where SDK/Pub package code will be marked
+  /// as not-debuggable.
+  final List<String>? additionalProjectPaths;
+
   /// Whether SDK libraries should be marked as debuggable.
   ///
   /// Treated as `false` if null, which means "step in" will not step into SDK
@@ -1152,6 +1194,7 @@
     this.vmServicePort,
     this.vmAdditionalArgs,
     this.enableAsserts,
+    this.additionalProjectPaths,
     this.debugSdkLibraries,
     this.debugExternalPackageLibraries,
     this.evaluateGettersInDebugViews,
@@ -1167,6 +1210,8 @@
         vmServicePort = obj['vmServicePort'] as int?,
         vmAdditionalArgs = (obj['vmAdditionalArgs'] as List?)?.cast<String>(),
         enableAsserts = obj['enableAsserts'] as bool?,
+        additionalProjectPaths =
+            (obj['additionalProjectPaths'] as List?)?.cast<String>(),
         debugSdkLibraries = obj['debugSdkLibraries'] as bool?,
         debugExternalPackageLibraries =
             obj['debugExternalPackageLibraries'] as bool?,
@@ -1187,6 +1232,8 @@
         if (vmServicePort != null) 'vmServicePort': vmServicePort,
         if (vmAdditionalArgs != null) 'vmAdditionalArgs': vmAdditionalArgs,
         if (enableAsserts != null) 'enableAsserts': enableAsserts,
+        if (additionalProjectPaths != null)
+          'additionalProjectPaths': additionalProjectPaths,
         if (debugSdkLibraries != null) 'debugSdkLibraries': debugSdkLibraries,
         if (debugExternalPackageLibraries != null)
           'debugExternalPackageLibraries': debugExternalPackageLibraries,
diff --git a/pkg/dds/lib/src/dap/adapters/dart_cli.dart b/pkg/dds/lib/src/dap/adapters/dart_cli.dart
index c01214d..f0fcd43 100644
--- a/pkg/dds/lib/src/dap/adapters/dart_cli.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart_cli.dart
@@ -132,6 +132,15 @@
       ...?args.args,
     ];
 
+    // Find the package_config file for this script.
+    // TODO(dantup): Remove this once
+    //   https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
+    //   necessary.
+    final packageConfig = _findPackageConfigFile();
+    if (packageConfig != null) {
+      this.usePackageConfigFile(packageConfig);
+    }
+
     logger?.call('Spawning $vmPath with $processArgs in ${args.cwd}');
     final process = await Process.start(
       vmPath,
@@ -146,6 +155,40 @@
     unawaited(process.exitCode.then(_handleExitCode));
   }
 
+  /// Find the `package_config.json` file for the program being launched.
+  ///
+  /// TODO(dantup): Remove this once
+  ///   https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
+  ///   necessary.
+  File? _findPackageConfigFile() {
+    var possibleRoot = path.isAbsolute(args.program)
+        ? path.dirname(args.program)
+        : path.dirname(path.normalize(path.join(args.cwd ?? '', args.program)));
+
+    File? packageConfig;
+    while (true) {
+      packageConfig =
+          File(path.join(possibleRoot, '.dart_tool', 'package_config.json'));
+
+      // If this packageconfig exists, use it.
+      if (packageConfig.existsSync()) {
+        break;
+      }
+
+      final parent = path.dirname(possibleRoot);
+
+      // If we can't go up anymore, the search failed.
+      if (parent == possibleRoot) {
+        packageConfig = null;
+        break;
+      }
+
+      possibleRoot = parent;
+    }
+
+    return packageConfig;
+  }
+
   /// Called by [terminateRequest] to request that we gracefully shut down the
   /// app being run (or in the case of an attach, disconnect).
   Future<void> terminateImpl() async {
diff --git a/pkg/dds/lib/src/dap/isolate_manager.dart b/pkg/dds/lib/src/dap/isolate_manager.dart
index 9c16536..6861be1 100644
--- a/pkg/dds/lib/src/dap/isolate_manager.dart
+++ b/pkg/dds/lib/src/dap/isolate_manager.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:path/path.dart' as path;
 import 'package:vm_service/vm_service.dart' as vm;
 
 import 'adapters/dart.dart';
@@ -371,17 +372,37 @@
     }
   }
 
-  bool _isExternalPackageLibrary(vm.LibraryRef library) =>
-      // TODO(dantup): This needs to check if it's _external_, eg.
-      //
-      // - is from the flutter SDK (flutter, flutter_test, ...)
-      // - is from pub/pubcache
-      //
-      // This is intended to match the users idea of "my code". For example
-      // they may wish to debug the current app being run, as well as any other
-      // projects that are references with path: dependencies (which are likely
-      // their own supporting projects).
-      false /*library.uri?.startsWith('package:') ?? false*/;
+  /// Checks whether this library is from an external package.
+  ///
+  /// This is used to support debugging "Just My Code" so Pub packages can be
+  /// marked as not-debuggable.
+  ///
+  /// A library is considered local if the path is within the 'cwd' or
+  /// 'additionalProjectPaths' in the launch arguments. An editor should include
+  /// the paths of all open workspace folders in 'additionalProjectPaths' to
+  /// support this feature correctly.
+  bool _isExternalPackageLibrary(vm.LibraryRef library) {
+    final libraryUri = library.uri;
+    if (libraryUri == null) {
+      return false;
+    }
+    final uri = Uri.parse(libraryUri);
+    if (!uri.isScheme('package')) {
+      return false;
+    }
+    final libraryPath = _adapter.resolvePackageUri(uri);
+    if (libraryPath == null) {
+      return false;
+    }
+
+    // Always compare paths case-insensitively to avoid any issues where APIs
+    // may have returned different casing (eg. Windows drive letters). It's
+    // almost certain a user wouldn't have a "local" package and an "external"
+    // package with paths differing only be case.
+    final libraryPathLower = libraryPath.toLowerCase();
+    return !_adapter.projectPaths.any((projectPath) =>
+        path.isWithin(projectPath.toLowerCase(), libraryPathLower));
+  }
 
   bool _isSdkLibrary(vm.LibraryRef library) =>
       library.uri?.startsWith('dart:') ?? false;
diff --git a/pkg/dds/lib/src/dap/protocol_converter.dart b/pkg/dds/lib/src/dap/protocol_converter.dart
index 6285121..1243e5a 100644
--- a/pkg/dds/lib/src/dap/protocol_converter.dart
+++ b/pkg/dds/lib/src/dap/protocol_converter.dart
@@ -6,6 +6,7 @@
 import 'dart:io';
 
 import 'package:collection/collection.dart';
+import 'package:package_config/package_config_types.dart';
 import 'package:path/path.dart' as path;
 import 'package:vm_service/vm_service.dart' as vm;
 
@@ -25,6 +26,11 @@
   /// the debug session.
   final DartDebugAdapter _adapter;
 
+  /// Temporary PackageConfig used for resolving package: URIs.
+  /// TODO(dantup): Replace this implementation with one that calls the VM
+  ///   Service once https://github.com/dart-lang/sdk/issues/45530 is done.
+  PackageConfig packageConfig = PackageConfig.empty;
+
   ProtocolConverter(this._adapter);
 
   /// Converts an absolute path to one relative to the cwd used to launch the
@@ -325,14 +331,16 @@
 
     final scriptRef = location.script;
     final tokenPos = location.tokenPos;
-    final uri = scriptRef?.uri;
+    final scriptRefUri = scriptRef?.uri;
+    final uri = scriptRefUri != null ? Uri.parse(scriptRefUri) : null;
+    final uriIsPackage = uri?.isScheme('package') ?? false;
     final sourcePath = uri != null ? await convertVmUriToSourcePath(uri) : null;
     var canShowSource = sourcePath != null && File(sourcePath).existsSync();
 
     // Download the source if from a "dart:" uri.
     int? sourceReference;
     if (uri != null &&
-        (uri.startsWith('dart:') || uri.startsWith('org-dartlang-app:')) &&
+        (uri.isScheme('dart') || uri.isScheme('org-dartlang-app')) &&
         scriptRef != null) {
       sourceReference = thread.storeData(scriptRef);
       canShowSource = true;
@@ -351,7 +359,11 @@
 
     final source = canShowSource
         ? dap.Source(
-            name: sourcePath != null ? convertToRelativePath(sourcePath) : uri,
+            name: uriIsPackage
+                ? uri!.toString()
+                : sourcePath != null
+                    ? convertToRelativePath(sourcePath)
+                    : uri?.toString() ?? '<unknown source>',
             path: sourcePath,
             sourceReference: sourceReference,
             origin: null,
@@ -374,18 +386,17 @@
     );
   }
 
-  /// Converts the source path from the VM to a file path.
+  /// Converts the source URI from the VM to a file path.
   ///
   /// This is required so that when the user stops (or navigates via a stack
   /// frame) we open the same file on their local disk. If we downloaded the
   /// source from the VM, they would end up seeing two copies of files (and they
   /// would each have their own breakpoints) which can be confusing.
-  Future<String?> convertVmUriToSourcePath(String uri) async {
-    if (uri.startsWith('file://')) {
-      return Uri.parse(uri).toFilePath();
-    } else if (uri.startsWith('package:')) {
-      // TODO(dantup): Handle mapping package: uris ?
-      return null;
+  Future<String?> convertVmUriToSourcePath(Uri uri) async {
+    if (uri.isScheme('file')) {
+      return uri.toFilePath();
+    } else if (uri.isScheme('package')) {
+      return resolvePackageUri(uri);
     } else {
       return null;
     }
@@ -402,6 +413,19 @@
         kind == 'Closure';
   }
 
+  /// Resolves a `package: URI` to the real underlying source path.
+  ///
+  /// Returns `null` if no mapping was possible, for example if the package is
+  /// not in the package mapping file.
+  String? resolvePackageUri(Uri uri) {
+    // TODO(dantup): Replace this implementation with one that calls the VM
+    //   Service once https://github.com/dart-lang/sdk/issues/45530 is done.
+    // This implementation makes assumptions about the package file being used
+    // that might not be correct (for example if the user uses the --packages
+    // flag).
+    return packageConfig.resolve(uri)?.toFilePath();
+  }
+
   /// Invokes the toString() method on a [vm.InstanceRef] and converts the
   /// response to a user-friendly display string.
   ///
diff --git a/pkg/dds/lib/src/dds_impl.dart b/pkg/dds/lib/src/dds_impl.dart
index 4360bcc..87ed83d 100644
--- a/pkg/dds/lib/src/dds_impl.dart
+++ b/pkg/dds/lib/src/dds_impl.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:collection';
 import 'dart:convert';
 import 'dart:io';
 import 'dart:math';
@@ -54,6 +55,7 @@
     this._remoteVmServiceUri,
     this._uri,
     this._authCodesEnabled,
+    this._cachedUserTags,
     this._ipv6,
     this._devToolsConfiguration,
     this.shouldLogRequests,
@@ -381,6 +383,9 @@
 
   final DevToolsConfiguration? _devToolsConfiguration;
 
+  List<String> get cachedUserTags => UnmodifiableListView(_cachedUserTags);
+  final List<String> _cachedUserTags;
+
   Future<void> get done => _done.future;
   Completer _done = Completer<void>();
   bool _shuttingDown = false;
diff --git a/pkg/dds/lib/src/isolate_manager.dart b/pkg/dds/lib/src/isolate_manager.dart
index e9e14df..3a7a067 100644
--- a/pkg/dds/lib/src/isolate_manager.dart
+++ b/pkg/dds/lib/src/isolate_manager.dart
@@ -2,7 +2,9 @@
 // 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/src/cpu_samples_manager.dart';
 import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:vm_service/vm_service.dart';
 
 import 'client.dart';
 import 'constants.dart';
@@ -35,7 +37,11 @@
 }
 
 class _RunningIsolate {
-  _RunningIsolate(this.isolateManager, this.id, this.name);
+  _RunningIsolate(this.isolateManager, this.id, this.name)
+      : cpuSamplesManager = CpuSamplesManager(
+          isolateManager.dds,
+          id,
+        );
 
   // State setters.
   void pausedOnExit() => _state = _IsolateState.pauseExit;
@@ -103,6 +109,29 @@
   /// Should always be called after an isolate is resumed.
   void clearResumeApprovals() => _resumeApprovalsByName.clear();
 
+  Map<String, dynamic> getCachedCpuSamples(String userTag) {
+    final repo = cpuSamplesManager.cpuSamplesCaches[userTag];
+    if (repo == null) {
+      throw json_rpc.RpcException.invalidParams(
+        'CPU sample caching is not enabled for tag: "$userTag"',
+      );
+    }
+    return repo.toJson();
+  }
+
+  void handleEvent(Event event) {
+    switch (event.kind) {
+      case EventKind.kUserTagChanged:
+        cpuSamplesManager.handleUserTagEvent(event);
+        return;
+      case EventKind.kCpuSamples:
+        cpuSamplesManager.handleCpuSamplesEvent(event);
+        return;
+      default:
+        return;
+    }
+  }
+
   int get _isolateStateMask => isolateStateToMaskMapping[_state] ?? 0;
 
   static const isolateStateToMaskMapping = {
@@ -112,6 +141,7 @@
   };
 
   final IsolateManager isolateManager;
+  final CpuSamplesManager cpuSamplesManager;
   final String name;
   final String id;
   final Set<String?> _resumeApprovalsByName = {};
@@ -122,20 +152,25 @@
   IsolateManager(this.dds);
 
   /// Handles state changes for isolates.
-  void handleIsolateEvent(json_rpc.Parameters parameters) {
-    final event = parameters['event'];
-    final eventKind = event['kind'].asString;
-
+  void handleIsolateEvent(Event event) {
     // There's no interesting information about isolate state associated with
     // and IsolateSpawn event.
-    if (eventKind == ServiceEvents.isolateSpawn) {
+    // TODO(bkonyi): why isn't IsolateSpawn in package:vm_service
+    if (event.kind! == ServiceEvents.isolateSpawn) {
       return;
     }
 
-    final isolateData = event['isolate'];
-    final id = isolateData['id'].asString;
-    final name = isolateData['name'].asString;
-    _updateIsolateState(id, name, eventKind);
+    final isolateData = event.isolate!;
+    final id = isolateData.id!;
+    final name = isolateData.name!;
+    _updateIsolateState(id, name, event.kind!);
+  }
+
+  void routeEventToIsolate(Event event) {
+    final isolateId = event.isolate!.id!;
+    if (isolates.containsKey(isolateId)) {
+      isolates[isolateId]!.handleEvent(event);
+    }
   }
 
   void _updateIsolateState(String id, String name, String eventKind) {
@@ -230,6 +265,16 @@
     return RPCResponses.success;
   }
 
+  Map<String, dynamic> getCachedCpuSamples(json_rpc.Parameters parameters) {
+    final isolateId = parameters['isolateId'].asString;
+    if (!isolates.containsKey(isolateId)) {
+      return RPCResponses.collectedSentinel;
+    }
+    final isolate = isolates[isolateId]!;
+    final userTag = parameters['userTag'].asString;
+    return isolate.getCachedCpuSamples(userTag);
+  }
+
   /// Forwards a `resume` request to the VM service.
   Future<Map<String, dynamic>> _sendResumeRequest(
     String isolateId,
diff --git a/pkg/dds/lib/src/logging_repository.dart b/pkg/dds/lib/src/logging_repository.dart
index 4537d7e..062529a 100644
--- a/pkg/dds/lib/src/logging_repository.dart
+++ b/pkg/dds/lib/src/logging_repository.dart
@@ -2,17 +2,16 @@
 // 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:math';
-
 import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
 
 import 'client.dart';
+import 'common/ring_buffer.dart';
 
 /// [LoggingRepository] is used to store historical log messages from the
 /// target VM service. Clients which connect to DDS and subscribe to the
 /// `Logging` stream will be sent all messages contained within this repository
 /// upon initial subscription.
-class LoggingRepository extends _RingBuffer<Map<String, dynamic>> {
+class LoggingRepository extends RingBuffer<Map<String, dynamic>> {
   LoggingRepository([int logHistoryLength = 10000]) : super(logHistoryLength) {
     // TODO(bkonyi): enforce log history limit when DartDevelopmentService
     // allows for this to be set via Dart code.
@@ -46,53 +45,3 @@
   static const int _kMaxLogBufferSize = 100000;
 }
 
-// TODO(bkonyi): move to standalone file if we decide to use this elsewhere.
-class _RingBuffer<T> {
-  _RingBuffer(this._bufferSize) {
-    _buffer = List<T?>.filled(
-      _bufferSize,
-      null,
-    );
-  }
-
-  Iterable<T> call() sync* {
-    for (int i = _size - 1; i >= 0; --i) {
-      yield _buffer[(_count - i - 1) % _bufferSize]!;
-    }
-  }
-
-  void add(T e) {
-    if (_buffer.isEmpty) {
-      return;
-    }
-    _buffer[_count++ % _bufferSize] = e;
-  }
-
-  void resize(int size) {
-    assert(size >= 0);
-    if (size == _bufferSize) {
-      return;
-    }
-    final resized = List<T?>.filled(
-      size,
-      null,
-    );
-    int count = 0;
-    if (size > 0) {
-      for (final e in this()) {
-        resized[count++ % size] = e;
-      }
-    }
-    _count = count;
-    _bufferSize = size;
-    _buffer = resized;
-  }
-
-  int get bufferSize => _bufferSize;
-
-  int get _size => min(_count, _bufferSize);
-
-  int _bufferSize;
-  int _count = 0;
-  late List<T?> _buffer;
-}
diff --git a/pkg/dds/lib/src/stream_manager.dart b/pkg/dds/lib/src/stream_manager.dart
index 94f791a..f396c04 100644
--- a/pkg/dds/lib/src/stream_manager.dart
+++ b/pkg/dds/lib/src/stream_manager.dart
@@ -5,6 +5,7 @@
 import 'dart:typed_data';
 
 import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:vm_service/vm_service.dart';
 
 import 'client.dart';
 import 'dds_impl.dart';
@@ -107,18 +108,31 @@
         // Stdout and Stderr streams may not exist.
       }
     }
+    if (dds.cachedUserTags.isNotEmpty) {
+      await streamListen(null, EventStreams.kProfiler);
+    }
     dds.vmServiceClient.registerMethod(
       'streamNotify',
-      (parameters) {
+      (json_rpc.Parameters parameters) {
         final streamId = parameters['streamId'].asString;
+        final event =
+            Event.parse(parameters['event'].asMap.cast<String, dynamic>())!;
+
         // Forward events from the streams IsolateManager subscribes to.
         if (isolateManagerStreams.contains(streamId)) {
-          dds.isolateManager.handleIsolateEvent(parameters);
+          dds.isolateManager.handleIsolateEvent(event);
         }
         // Keep a history of messages to send to clients when they first
         // subscribe to a stream with an event history.
         if (loggingRepositories.containsKey(streamId)) {
-          loggingRepositories[streamId]!.add(parameters.asMap);
+          loggingRepositories[streamId]!.add(
+            parameters.asMap.cast<String, dynamic>(),
+          );
+        }
+        // If the event contains an isolate, forward the event to the
+        // corresponding isolate to be handled.
+        if (event.isolate != null) {
+          dds.isolateManager.routeEventToIsolate(event);
         }
         streamNotify(streamId, parameters.value);
       },
@@ -251,6 +265,7 @@
   static const kExtensionStream = 'Extension';
   static const kIsolateStream = 'Isolate';
   static const kLoggingStream = 'Logging';
+  static const kProfilerStream = 'Profiler';
   static const kStderrStream = 'Stderr';
   static const kStdoutStream = 'Stdout';
 
@@ -272,10 +287,17 @@
     kStdoutStream,
   };
 
+  // Never cancel the profiler stream as `CpuSampleRepository` requires
+  // `UserTagChanged` events to enable/disable sample caching.
+  static const cpuSampleRepositoryStreams = <String>{
+    kProfilerStream,
+  };
+
   // The set of streams that DDS requires to function.
   static final ddsCoreStreams = <String>{
     ...isolateManagerStreams,
     ...loggingRepositoryStreams,
+    ...cpuSampleRepositoryStreams,
   };
 
   final DartDevelopmentServiceImpl dds;
diff --git a/pkg/dds/lib/vm_service_extensions.dart b/pkg/dds/lib/vm_service_extensions.dart
index 903c14a..09bda25 100644
--- a/pkg/dds/lib/vm_service_extensions.dart
+++ b/pkg/dds/lib/vm_service_extensions.dart
@@ -13,18 +13,46 @@
   static bool _factoriesRegistered = false;
   static Version? _ddsVersion;
 
-  /// The _getDartDevelopmentServiceVersion_ RPC is used to determine what version of
+  /// The [getDartDevelopmentServiceVersion] RPC is used to determine what version of
   /// the Dart Development Service Protocol is served by a DDS instance.
   ///
   /// The result of this call is cached for subsequent invocations.
   Future<Version> getDartDevelopmentServiceVersion() async {
     if (_ddsVersion == null) {
-      _ddsVersion =
-          await _callHelper<Version>('getDartDevelopmentServiceVersion');
+      _ddsVersion = await _callHelper<Version>(
+        'getDartDevelopmentServiceVersion',
+      );
     }
     return _ddsVersion!;
   }
 
+  /// The [getCachedCpuSamples] RPC is used to retrieve a cache of CPU samples
+  /// collected under a [UserTag] with name `userTag`.
+  Future<CachedCpuSamples> getCachedCpuSamples(
+      String isolateId, String userTag) async {
+    if (!(await _versionCheck(1, 3))) {
+      throw UnimplementedError('getCachedCpuSamples requires DDS version 1.3');
+    }
+    return _callHelper<CachedCpuSamples>('getCachedCpuSamples', args: {
+      'isolateId': isolateId,
+      'userTag': userTag,
+    });
+  }
+
+  /// The [getAvailableCachedCpuSamples] RPC is used to determine which caches of CPU samples
+  /// are available. Caches are associated with individual [UserTag] names and are specified
+  /// when DDS is started via the `cachedUserTags` parameter.
+  Future<AvailableCachedCpuSamples> getAvailableCachedCpuSamples() async {
+    if (!(await _versionCheck(1, 3))) {
+      throw UnimplementedError(
+        'getAvailableCachedCpuSamples requires DDS version 1.3',
+      );
+    }
+    return _callHelper<AvailableCachedCpuSamples>(
+      'getAvailableCachedCpuSamples',
+    );
+  }
+
   /// Retrieve the event history for `stream`.
   ///
   /// If `stream` does not have event history collected, a parameter error is
@@ -126,6 +154,11 @@
 
   static void _registerFactories() {
     addTypeFactory('StreamHistory', StreamHistory.parse);
+    addTypeFactory(
+      'AvailableCachedCpuSamples',
+      AvailableCachedCpuSamples.parse,
+    );
+    addTypeFactory('CachedCpuSamples', CachedCpuSamples.parse);
     _factoriesRegistered = true;
   }
 }
@@ -154,3 +187,86 @@
   List<Event> get history => UnmodifiableListView(_history);
   final List<Event> _history;
 }
+
+/// An extension of [CpuSamples] which represents a set of cached samples,
+/// associated with a particular [UserTag] name.
+class CachedCpuSamples extends CpuSamples {
+  static CachedCpuSamples? parse(Map<String, dynamic>? json) =>
+      json == null ? null : CachedCpuSamples._fromJson(json);
+
+  CachedCpuSamples({
+    required this.userTag,
+    this.truncated,
+    required int? samplePeriod,
+    required int? maxStackDepth,
+    required int? sampleCount,
+    required int? timeSpan,
+    required int? timeOriginMicros,
+    required int? timeExtentMicros,
+    required int? pid,
+    required List<ProfileFunction>? functions,
+    required List<CpuSample>? samples,
+  }) : super(
+          samplePeriod: samplePeriod,
+          maxStackDepth: maxStackDepth,
+          sampleCount: sampleCount,
+          timeSpan: timeSpan,
+          timeOriginMicros: timeOriginMicros,
+          timeExtentMicros: timeExtentMicros,
+          pid: pid,
+          functions: functions,
+          samples: samples,
+        );
+
+  CachedCpuSamples._fromJson(Map<String, dynamic> json)
+      : userTag = json['userTag']!,
+        truncated = json['truncated'],
+        super(
+          samplePeriod: json['samplePeriod'] ?? -1,
+          maxStackDepth: json['maxStackDepth'] ?? -1,
+          sampleCount: json['sampleCount'] ?? -1,
+          timeSpan: json['timeSpan'] ?? -1,
+          timeOriginMicros: json['timeOriginMicros'] ?? -1,
+          timeExtentMicros: json['timeExtentMicros'] ?? -1,
+          pid: json['pid'] ?? -1,
+          functions: List<ProfileFunction>.from(
+            createServiceObject(json['functions'], const ['ProfileFunction'])
+                    as List? ??
+                [],
+          ),
+          samples: List<CpuSample>.from(
+            createServiceObject(json['samples'], const ['CpuSample'])
+                    as List? ??
+                [],
+          ),
+        );
+
+  @override
+  String get type => 'CachedCpuSamples';
+
+  /// The name of the [UserTag] associated with this cache of [CpuSamples].
+  final String userTag;
+
+  /// Provided if the CPU sample cache has filled and older samples have been
+  /// dropped.
+  final bool? truncated;
+}
+
+/// A collection of [UserTag] names associated with caches of CPU samples.
+class AvailableCachedCpuSamples extends Response {
+  static AvailableCachedCpuSamples? parse(Map<String, dynamic>? json) =>
+      json == null ? null : AvailableCachedCpuSamples._fromJson(json);
+
+  AvailableCachedCpuSamples({
+    required this.cacheNames,
+  });
+
+  AvailableCachedCpuSamples._fromJson(Map<String, dynamic> json)
+      : cacheNames = List<String>.from(json['cacheNames']);
+
+  @override
+  String get type => 'AvailableCachedUserTagCpuSamples';
+
+  /// A [List] of [UserTag] names associated with CPU sample caches.
+  final List<String> cacheNames;
+}
diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml
index 012f3e3..5468e58 100644
--- a/pkg/dds/pubspec.yaml
+++ b/pkg/dds/pubspec.yaml
@@ -3,12 +3,12 @@
   A library used to spawn the Dart Developer Service, used to communicate with
   a Dart VM Service instance.
 
-version: 2.0.2
+version: 2.1.0
 
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/dds
 
 environment:
-  sdk: '>=2.12.0 <3.0.0'
+  sdk: '>=2.14.0-0 <3.0.0'
 
 dependencies:
   async: ^2.4.1
@@ -16,6 +16,7 @@
   devtools_shared: ^2.3.0
   json_rpc_2: ^3.0.0
   meta: ^1.1.8
+  package_config: ^2.0.0
   path: ^1.8.0
   pedantic: ^1.7.0
   shelf: ^1.0.0
@@ -25,7 +26,7 @@
   sse: ^4.0.0
   stream_channel: ^2.0.0
   usage: ^4.0.0
-  vm_service: ^7.0.0
+  vm_service: ^7.2.0
   web_socket_channel: ^2.0.0
 
 dev_dependencies:
diff --git a/pkg/dds/test/common/test_helper.dart b/pkg/dds/test/common/test_helper.dart
index 9fc441b..077e75a 100644
--- a/pkg/dds/test/common/test_helper.dart
+++ b/pkg/dds/test/common/test_helper.dart
@@ -24,6 +24,7 @@
     '--observe=0',
     if (pauseOnStart) '--pause-isolates-on-start',
     '--write-service-info=$serviceInfoUri',
+    '--sample-buffer-duration=1',
     ...Platform.executableArguments,
     Platform.script.resolve(script).toString(),
   ];
diff --git a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
index 62cacbf..eadf81f 100644
--- a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
+++ b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
@@ -200,15 +200,103 @@
 
     test(
         'does not step into external package code with debugExternalPackageLibraries=false',
-        () {
-      // TODO(dantup): Support for debugExternalPackageLibraries
-    }, skip: true);
+        () async {
+      final client = dap.client;
+      final otherPackageUri = await dap.createFooPackage();
+      final testFile = dap.createTestFile('''
+import '$otherPackageUri';
+
+void main(List<String> args) async {
+  foo(); // BREAKPOINT
+  foo(); // STEP
+}
+    ''');
+      final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+      final stepLine = lineWith(testFile, '// STEP');
+
+      // Hit the initial breakpoint.
+      final stop = await client.hitBreakpoint(
+        testFile,
+        breakpointLine,
+        launch: () => client.launch(
+          testFile.path,
+          debugExternalPackageLibraries: false,
+        ),
+      );
+
+      // Step in and expect stopping on the next line (don't go into the package).
+      await Future.wait([
+        client.expectStop('step', file: testFile, line: stepLine),
+        client.stepIn(stop.threadId!),
+      ], eagerError: true);
+    });
 
     test(
         'steps into external package code with debugExternalPackageLibraries=true',
-        () {
-      // TODO(dantup): Support for debugExternalPackageLibraries
-    }, skip: true);
+        () async {
+      final client = dap.client;
+      final otherPackageUri = await dap.createFooPackage();
+      final testFile = dap.createTestFile('''
+import '$otherPackageUri';
+
+void main(List<String> args) async {
+  foo(); // BREAKPOINT
+  foo();
+}
+    ''');
+      final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+
+      // Hit the initial breakpoint.
+      final stop = await dap.client.hitBreakpoint(
+        testFile,
+        breakpointLine,
+        launch: () => client.launch(
+          testFile.path,
+          debugExternalPackageLibraries: true,
+        ),
+      );
+
+      // Step in and expect to go into the package.
+      await Future.wait([
+        client.expectStop('step', sourceName: '$otherPackageUri'),
+        client.stepIn(stop.threadId!),
+      ], eagerError: true);
+    });
+
+    test(
+        'steps into other-project package code with debugExternalPackageLibraries=false',
+        () async {
+      final client = dap.client;
+      final otherPackageUri = await dap.createFooPackage();
+      final testFile = dap.createTestFile('''
+import '$otherPackageUri';
+
+void main(List<String> args) async {
+  foo(); // BREAKPOINT
+  foo();
+}
+    ''');
+      final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+
+      // Hit the initial breakpoint.
+      final stop = await client.hitBreakpoint(
+        testFile,
+        breakpointLine,
+        launch: () => client.launch(
+          testFile.path,
+          debugExternalPackageLibraries: false,
+          // Include the packages folder as an additional project path so that
+          // it will be treated as local code.
+          additionalProjectPaths: [dap.testPackageDir.path],
+        ),
+      );
+
+      // Step in and expect stopping in the the other package.
+      await Future.wait([
+        client.expectStop('step', sourceName: '$otherPackageUri'),
+        client.stepIn(stop.threadId!),
+      ], eagerError: true);
+    });
 
     test('allows changing debug settings during session', () async {
       final client = dap.client;
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index bfa05c6..e1a4aec 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -121,6 +121,7 @@
     List<String>? args,
     String? cwd,
     bool? noDebug,
+    List<String>? additionalProjectPaths,
     bool? debugSdkLibraries,
     bool? debugExternalPackageLibraries,
     bool? evaluateGettersInDebugViews,
@@ -132,6 +133,7 @@
         program: program,
         cwd: cwd,
         args: args,
+        additionalProjectPaths: additionalProjectPaths,
         debugSdkLibraries: debugSdkLibraries,
         debugExternalPackageLibraries: debugExternalPackageLibraries,
         evaluateGettersInDebugViews: evaluateGettersInDebugViews,
@@ -388,8 +390,12 @@
   ///
   /// If [file] or [line] are provided, they will be checked against the stop
   /// location for the top stack frame.
-  Future<StoppedEventBody> expectStop(String reason,
-      {File? file, int? line, String? sourceName}) async {
+  Future<StoppedEventBody> expectStop(
+    String reason, {
+    File? file,
+    int? line,
+    String? sourceName,
+  }) async {
     final e = await event('stopped');
     final stop = StoppedEventBody.fromJson(e.body as Map<String, Object?>);
     expect(stop.reason, equals(reason));
@@ -400,10 +406,10 @@
     final frame = result.stackFrames[0];
 
     if (file != null) {
-      expect(frame.source!.path, equals(file.path));
+      expect(frame.source?.path, equals(file.path));
     }
     if (sourceName != null) {
-      expect(frame.source!.name, equals(sourceName));
+      expect(frame.source?.name, equals(sourceName));
     }
     if (line != null) {
       expect(frame.line, equals(line));
diff --git a/pkg/dds/test/dap/integration/test_server.dart b/pkg/dds/test/dap/integration/test_server.dart
index dc96dc7..ffaabf2 100644
--- a/pkg/dds/test/dap/integration/test_server.dart
+++ b/pkg/dds/test/dap/integration/test_server.dart
@@ -31,8 +31,15 @@
   StreamSink<List<int>> get sink => stdinController.sink;
   Stream<List<int>> get stream => stdoutController.stream;
 
-  InProcessDapTestServer._() {
-    _server = DapServer(stdinController.stream, stdoutController.sink);
+  InProcessDapTestServer._(List<String> args) {
+    _server = DapServer(
+      stdinController.stream,
+      stdoutController.sink,
+      // Simulate flags based on the args to aid testing.
+      enableDds: !args.contains('--no-dds'),
+      ipv6: args.contains('--ipv6'),
+      enableAuthCodes: !args.contains('--no-auth-codes'),
+    );
   }
 
   @override
@@ -44,7 +51,10 @@
     Logger? logger,
     List<String>? additionalArgs,
   }) async {
-    return InProcessDapTestServer._();
+    return InProcessDapTestServer._([
+      ...?additionalArgs,
+      if (logger != null) '--verbose',
+    ]);
   }
 }
 
diff --git a/pkg/dds/test/dap/integration/test_support.dart b/pkg/dds/test/dap/integration/test_support.dart
index a44a791..dc88974 100644
--- a/pkg/dds/test/dap/integration/test_support.dart
+++ b/pkg/dds/test/dap/integration/test_support.dart
@@ -6,6 +6,7 @@
 import 'dart:io';
 
 import 'package:dds/src/dap/logging.dart';
+import 'package:package_config/package_config.dart';
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 
@@ -43,35 +44,81 @@
 class DapTestSession {
   DapTestServer server;
   DapTestClient client;
-  final _testFolders = <Directory>[];
+  final Directory _testDir =
+      Directory.systemTemp.createTempSync('dart-sdk-dap-test');
+  late final Directory testAppDir;
+  late final Directory testPackageDir;
+  var _packageConfig = PackageConfig.empty;
 
-  DapTestSession._(this.server, this.client);
+  DapTestSession._(this.server, this.client) {
+    testAppDir = _testDir.createTempSync('app');
+    testPackageDir = _testDir.createTempSync('packages');
+  }
+
+  /// Create a simple package named `foo` that has an empty `foo` function.
+  Future<Uri> createFooPackage() {
+    return createSimplePackage(
+      'foo',
+      '''
+foo() {
+  // Does nothing.
+}
+      ''',
+    );
+  }
+
+  /// Creates a simple package script and adds the package to
+  /// .dart_tool/package_config.json
+  Future<Uri> createSimplePackage(
+    String name,
+    String content,
+  ) async {
+    final dartToolDirectory =
+        Directory(path.join(testAppDir.path, '.dart_tool'))..createSync();
+    final packageConfigJsonFile =
+        File(path.join(dartToolDirectory.path, 'package_config.json'));
+    final packageConfigJsonUri = Uri.file(packageConfigJsonFile.path);
+
+    // Write the packages Dart implementation file.
+    final testPackageDirectory = Directory(path.join(testPackageDir.path, name))
+      ..createSync(recursive: true);
+    final testFile = File(path.join(testPackageDirectory.path, '$name.dart'));
+    testFile.writeAsStringSync(content);
+
+    // Add this new package to the PackageConfig.
+    final newPackage = Package(name, Uri.file('${testPackageDirectory.path}/'));
+    _packageConfig = PackageConfig([..._packageConfig.packages, newPackage]);
+
+    // Write the PackageConfig to disk.
+    final sink = packageConfigJsonFile.openWrite();
+    PackageConfig.writeString(_packageConfig, sink, packageConfigJsonUri);
+    await sink.close();
+
+    return Uri.parse('package:$name/$name.dart');
+  }
 
   /// Creates a file in a temporary folder to be used as an application for testing.
   ///
   /// The file will be deleted at the end of the test run.
   File createTestFile(String content) {
-    final testAppDir = Directory.systemTemp.createTempSync('dart-sdk-dap-test');
-    _testFolders.add(testAppDir);
     final testFile = File(path.join(testAppDir.path, 'test_file.dart'));
     testFile.writeAsStringSync(content);
     return testFile;
   }
 
-  static Future<DapTestSession> setUp({List<String>? additionalArgs}) async {
-    final server = await _startServer(additionalArgs: additionalArgs);
-    final client = await DapTestClient.connect(server);
-    return DapTestSession._(server, client);
-  }
-
   Future<void> tearDown() async {
     await client.stop();
     await server.stop();
 
     // Clean up any temp folders created during the test runs.
-    _testFolders
-      ..forEach((dir) => dir.deleteSync(recursive: true))
-      ..clear();
+    _testDir.deleteSync(recursive: true);
+  }
+
+  static Future<DapTestSession> setUp({List<String>? additionalArgs}) async {
+    final server = await _startServer(additionalArgs: additionalArgs);
+    final client =
+        await DapTestClient.connect(server, captureVmServiceTraffic: true);
+    return DapTestSession._(server, client);
   }
 
   /// Starts a DAP server that can be shared across tests.
diff --git a/pkg/dds/test/get_cached_cpu_samples_script.dart b/pkg/dds/test/get_cached_cpu_samples_script.dart
new file mode 100644
index 0000000..5949574
--- /dev/null
+++ b/pkg/dds/test/get_cached_cpu_samples_script.dart
@@ -0,0 +1,21 @@
+// 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:developer';
+
+fib(int n) {
+  if (n <= 1) {
+    return n;
+  }
+  return fib(n - 1) + fib(n - 2);
+}
+
+void main() {
+  UserTag('Testing').makeCurrent();
+  int i = 5;
+  while (true) {
+    ++i;
+    fib(i);
+  }
+}
diff --git a/pkg/dds/test/get_cached_cpu_samples_test.dart b/pkg/dds/test/get_cached_cpu_samples_test.dart
new file mode 100644
index 0000000..fe8a223
--- /dev/null
+++ b/pkg/dds/test/get_cached_cpu_samples_test.dart
@@ -0,0 +1,112 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:dds/dds.dart';
+import 'package:dds/vm_service_extensions.dart';
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+import 'package:vm_service/vm_service_io.dart';
+import 'common/test_helper.dart';
+
+void main() {
+  late Process process;
+  late DartDevelopmentService dds;
+
+  setUp(() async {
+    process = await spawnDartProcess(
+      'get_cached_cpu_samples_script.dart',
+    );
+  });
+
+  tearDown(() async {
+    await dds.shutdown();
+    process.kill();
+  });
+
+  test(
+    'No UserTags to cache',
+    () async {
+      dds = await DartDevelopmentService.startDartDevelopmentService(
+        remoteVmServiceUri,
+      );
+      expect(dds.isRunning, true);
+      final service = await vmServiceConnectUri(dds.wsUri.toString());
+
+      // We didn't provide `cachedUserTags` when starting DDS, so we shouldn't
+      // be caching anything.
+      final availableCaches = await service.getAvailableCachedCpuSamples();
+      expect(availableCaches.cacheNames.length, 0);
+
+      final isolate = (await service.getVM()).isolates!.first;
+
+      try {
+        await service.getCachedCpuSamples(isolate.id!, 'Fake');
+        fail('Invalid userTag did not cause an exception');
+      } on RPCError catch (e) {
+        expect(
+          e.message,
+          'CPU sample caching is not enabled for tag: "Fake"',
+        );
+      }
+    },
+    timeout: Timeout.none,
+  );
+
+  test(
+    'Cache CPU samples for provided UserTag name',
+    () async {
+      const kUserTag = 'Testing';
+      dds = await DartDevelopmentService.startDartDevelopmentService(
+        remoteVmServiceUri,
+        cachedUserTags: [kUserTag],
+      );
+      expect(dds.isRunning, true);
+      final service = await vmServiceConnectUri(dds.wsUri.toString());
+
+      // Ensure we're caching results for samples under the 'Testing' UserTag.
+      final availableCaches = await service.getAvailableCachedCpuSamples();
+      expect(availableCaches.cacheNames.length, 1);
+      expect(availableCaches.cacheNames.first, kUserTag);
+
+      final isolate = (await service.getVM()).isolates!.first;
+
+      final completer = Completer<void>();
+      int i = 0;
+      int count = 0;
+      service.onProfilerEvent.listen((event) async {
+        if (event.kind == EventKind.kCpuSamples &&
+            event.isolate!.id! == isolate.id!) {
+          // Pause so we don't evict another block of samples before we've
+          // retrieved the cached samples after this event.
+          await service.pause(isolate.id!);
+
+          // Ensure the number of CPU samples in the CpuSample event is
+          // is consistent with the number of samples in the cache.
+          expect(event.cpuSamples, isNotNull);
+          count += event.cpuSamples!.samples!
+              .where((e) => e.userTag == kUserTag)
+              .length;
+          final cache = await service.getCachedCpuSamples(
+            isolate.id!,
+            availableCaches.cacheNames.first,
+          );
+          expect(cache.sampleCount, count);
+
+          await service.resume(isolate.id!);
+          i++;
+          if (i == 3) {
+            completer.complete();
+          }
+        }
+      });
+      await service.streamListen(EventStreams.kProfiler);
+      await service.resume(isolate.id!);
+      await completer.future;
+    },
+    timeout: Timeout.none,
+  );
+}
diff --git a/pkg/dds/tool/dap/run_server.dart b/pkg/dds/tool/dap/run_server.dart
index cfddd48..ba22d72 100644
--- a/pkg/dds/tool/dap/run_server.dart
+++ b/pkg/dds/tool/dap/run_server.dart
@@ -41,6 +41,7 @@
     argParser
       ..addFlag(
         argIpv6,
+        defaultsTo: false,
         help: 'Whether to bind DAP/VM Service/DDS to IPv6 addresses',
       )
       ..addFlag(
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index e777ddf..bb8e920 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -2889,8 +2889,9 @@
     // underlying type for type checks, so they operate virtually the same as
     // anonymous types. We represent package:js types with a corresponding type
     // object.
-    var jsName = isJSAnonymousType(c) ?
-        getLocalClassName(c) : _emitJsNameWithoutGlobal(c);
+    var jsName = isJSAnonymousType(c)
+        ? getLocalClassName(c)
+        : _emitJsNameWithoutGlobal(c);
     if (jsName != null) {
       typeRep = runtimeCall('packageJSType(#)', [js.escapedString(jsName)]);
     }
diff --git a/pkg/dev_compiler/lib/src/kernel/expression_compiler_worker.dart b/pkg/dev_compiler/lib/src/kernel/expression_compiler_worker.dart
index 86a0cc2..06310f6 100644
--- a/pkg/dev_compiler/lib/src/kernel/expression_compiler_worker.dart
+++ b/pkg/dev_compiler/lib/src/kernel/expression_compiler_worker.dart
@@ -83,6 +83,8 @@
   final ModuleFormat _moduleFormat;
   final Component _sdkComponent;
 
+  void Function() onDone;
+
   ExpressionCompilerWorker._(
     this._processedOptions,
     this._compilerOptions,
@@ -90,6 +92,7 @@
     this._sdkComponent,
     this.requestStream,
     this.sendResponse,
+    this.onDone,
   );
 
   /// Create expression compiler worker from [args] and start it.
@@ -140,6 +143,7 @@
     }
   }
 
+  /// Parse args and create the worker, hook cleanup code to run when done.
   static Future<ExpressionCompilerWorker> createFromArgs(
     List<String> args, {
     Stream<Map<String, dynamic>> requestStream,
@@ -186,6 +190,9 @@
       verbose: parsedArgs['verbose'] as bool,
       requestStream: requestStream,
       sendResponse: sendResponse,
+      onDone: () {
+        if (fileSystem is AssetFileSystem) fileSystem.close();
+      },
     );
   }
 
@@ -209,6 +216,7 @@
     Stream<Map<String, dynamic>> requestStream, // Defaults to read from stdin
     void Function(Map<String, dynamic>)
         sendResponse, // Defaults to write to stdout
+    void Function() onDone,
   }) async {
     var compilerOptions = CompilerOptions()
       ..compileSdk = false
@@ -241,7 +249,7 @@
       throw Exception('Could not load SDK component: $sdkSummary');
     }
     return ExpressionCompilerWorker._(processedOptions, compilerOptions,
-        moduleFormat, sdkComponent, requestStream, sendResponse)
+        moduleFormat, sdkComponent, requestStream, sendResponse, onDone)
       .._updateCache(sdkComponent, dartSdkModule, true);
   }
 
@@ -281,10 +289,7 @@
     _processedOptions.ticker.logMs('Stopped expression compiler worker.');
   }
 
-  void close() {
-    var fileSystem = _processedOptions?.fileSystem;
-    if (fileSystem != null && fileSystem is AssetFileSystem) fileSystem.close();
-  }
+  void close() => onDone?.call();
 
   /// Handles a `CompileExpression` request.
   Future<Map<String, dynamic>> _compileExpression(
diff --git a/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart b/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
index c54916b..8bf3653 100644
--- a/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
+++ b/pkg/dev_compiler/test/expression_compiler/expression_compiler_worker_test.dart
@@ -895,6 +895,7 @@
   Future<void> stop() async {
     server.stop();
     await super.stop();
+    (assetFileSystem as AssetFileSystem).close();
   }
 }
 
diff --git a/pkg/front_end/lib/src/api_unstable/vm.dart b/pkg/front_end/lib/src/api_unstable/vm.dart
index 42a227f..7bb6bef 100644
--- a/pkg/front_end/lib/src/api_unstable/vm.dart
+++ b/pkg/front_end/lib/src/api_unstable/vm.dart
@@ -87,4 +87,5 @@
         serializeComponent,
         serializeProcedure;
 
-export '../fasta/resolve_input_uri.dart' show resolveInputUri;
+export 'package:_fe_analyzer_shared/src/util/resolve_input_uri.dart'
+    show resolveInputUri;
diff --git a/pkg/front_end/lib/src/base/command_line_options.dart b/pkg/front_end/lib/src/base/command_line_options.dart
index 3babd76..fc67845 100644
--- a/pkg/front_end/lib/src/base/command_line_options.dart
+++ b/pkg/front_end/lib/src/base/command_line_options.dart
@@ -2,6 +2,10 @@
 // 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:_fe_analyzer_shared/src/util/options.dart';
+import 'package:front_end/src/api_prototype/compiler_options.dart';
+import 'package:kernel/target/targets.dart';
+
 class Flags {
   // TODO(johnniwinther): What is the right name for this?
   static const String nnbdStrongMode = "--nnbd-strong";
@@ -47,3 +51,81 @@
 
   static const String invocationModes = "--invocation-modes";
 }
+
+class Options {
+  static const Option<Uri?> compileSdk =
+      const Option(Flags.compileSdk, const UriValue());
+  static const Option<bool> dumpIr =
+      const Option(Flags.dumpIr, const BoolValue(false));
+  static const Option<List<String>?> enableExperiment =
+      const Option(Flags.enableExperiment, const StringListValue());
+  static const Option<bool> excludeSource =
+      const Option(Flags.excludeSource, const BoolValue(false));
+  static const Option<bool> omitPlatform =
+      const Option(Flags.omitPlatform, const BoolValue(false));
+  static const Option<List<String>?> fatal =
+      const Option(Flags.fatal, const StringListValue());
+  static const Option<String?> fatalSkip =
+      const Option(Flags.fatalSkip, const StringValue());
+  static const Option<int> forceLateLowering = const Option(
+      Flags.forceLateLowering,
+      const IntValue(
+          defaultValue: LateLowering.none, noArgValue: LateLowering.all));
+  static const Option<bool> forceLateLoweringSentinel =
+      const Option(Flags.forceLateLoweringSentinel, const BoolValue(false));
+  static const Option<bool> forceStaticFieldLowering =
+      const Option(Flags.forceStaticFieldLowering, const BoolValue(false));
+  static const Option<bool> forceNoExplicitGetterCalls =
+      const Option(Flags.forceNoExplicitGetterCalls, const BoolValue(false));
+  static const Option<int> forceConstructorTearOffLowering = const Option(
+      Flags.forceConstructorTearOffLowering,
+      const IntValue(
+          defaultValue: ConstructorTearOffLowering.none,
+          noArgValue: ConstructorTearOffLowering.all));
+  static const Option<bool> help = const Option(
+      Flags.help, const BoolValue(false),
+      aliases: ["-h", "/?", "/h"]);
+  static const Option<Uri?> librariesJson =
+      const Option(Flags.librariesJson, const UriValue());
+  static const Option<bool> noDefines =
+      const Option(Flags.noDefines, const BoolValue(false));
+  static const Option<Uri?> output =
+      const Option(Flags.output, const UriValue(), aliases: ["--out", "-o"]);
+  static const Option<Uri?> packages =
+      const Option(Flags.packages, const UriValue());
+  static const Option<Uri?> platform =
+      const Option(Flags.platform, const UriValue());
+  static const Option<Uri?> sdk = const Option(Flags.sdk, const UriValue());
+  static const Option<Uri?> singleRootBase =
+      const Option(Flags.singleRootBase, const UriValue());
+  static const Option<String?> singleRootScheme =
+      const Option(Flags.singleRootScheme, const StringValue());
+  static const Option<bool> nnbdWeakMode =
+      const Option(Flags.nnbdWeakMode, const BoolValue(false));
+  static const Option<bool> nnbdStrongMode =
+      const Option(Flags.nnbdStrongMode, const BoolValue(false));
+  static const Option<bool> nnbdAgnosticMode =
+      const Option(Flags.nnbdAgnosticMode, const BoolValue(false));
+  static const Option<String> target = const Option(
+      Flags.target, const StringValue(defaultValue: 'vm'),
+      aliases: ["-t"]);
+  static const Option<bool> verbose =
+      const Option(Flags.verbose, const BoolValue(false), aliases: ["-v"]);
+  static const Option<String> verbosity = const Option(
+      Flags.verbosity, const StringValue(defaultValue: Verbosity.defaultValue));
+  static const Option<bool> verify =
+      const Option(Flags.verify, const BoolValue(false));
+  static const Option<bool> skipPlatformVerification =
+      const Option(Flags.skipPlatformVerification, const BoolValue(false));
+  static const Option<bool> warnOnReachabilityCheck =
+      const Option(Flags.warnOnReachabilityCheck, const BoolValue(false));
+  static const Option<List<Uri>?> linkDependencies =
+      const Option(Flags.linkDependencies, const UriListValue());
+  static const Option<bool> noDeps =
+      const Option(Flags.noDeps, const BoolValue(false));
+  static const Option<String?> invocationModes =
+      const Option(Flags.invocationModes, const StringValue());
+  static const Option<Map<String, String>> defines = const Option(
+      "-D", const DefineValue(),
+      isDefines: true, aliases: ["--define"]);
+}
diff --git a/pkg/front_end/test/fasta/testing/suite.dart b/pkg/front_end/test/fasta/testing/suite.dart
index e8d1ece..727bb3f 100644
--- a/pkg/front_end/test/fasta/testing/suite.dart
+++ b/pkg/front_end/test/fasta/testing/suite.dart
@@ -16,6 +16,7 @@
     show LanguageVersionToken, Token;
 
 import 'package:_fe_analyzer_shared/src/util/colors.dart' as colors;
+import 'package:_fe_analyzer_shared/src/util/options.dart';
 import 'package:compiler/src/kernel/dart2js_target.dart';
 import 'package:dev_compiler/dev_compiler.dart';
 
@@ -51,7 +52,7 @@
 import 'package:front_end/src/compute_platform_binaries_location.dart'
     show computePlatformBinariesLocation, computePlatformDillName;
 
-import 'package:front_end/src/base/command_line_options.dart' show Flags;
+import 'package:front_end/src/base/command_line_options.dart';
 
 import 'package:front_end/src/base/nnbd_mode.dart' show NnbdMode;
 
@@ -223,8 +224,35 @@
 final Expectation runtimeError = ExpectationSet.Default["RuntimeError"];
 
 const String experimentalFlagOptions = '--enable-experiment=';
-const String overwriteCurrentSdkVersion = '--overwrite-current-sdk-version=';
-const String noVerifyCmd = '--no-verify';
+const Option<String> overwriteCurrentSdkVersion =
+    const Option('--overwrite-current-sdk-version', const StringValue());
+const Option<bool> noVerifyCmd =
+    const Option('--no-verify', const BoolValue(false));
+
+const List<Option> folderOptionsSpecification = [
+  Options.enableExperiment,
+  Options.forceLateLoweringSentinel,
+  overwriteCurrentSdkVersion,
+  Options.forceLateLowering,
+  Options.forceStaticFieldLowering,
+  Options.forceNoExplicitGetterCalls,
+  Options.forceConstructorTearOffLowering,
+  Options.nnbdAgnosticMode,
+  Options.noDefines,
+  noVerifyCmd,
+  Options.target,
+  Options.defines,
+];
+
+const Option<bool> fixNnbdReleaseVersion =
+    const Option('--fix-nnbd-release-version', const BoolValue(false));
+
+const List<Option> testOptionsSpecification = [
+  Options.nnbdAgnosticMode,
+  Options.nnbdStrongMode,
+  Options.nnbdWeakMode,
+  fixNnbdReleaseVersion,
+];
 
 final ExpectationSet staticExpectationSet =
     new ExpectationSet.fromJsonList(jsonDecode(EXPECTATIONS));
@@ -440,74 +468,34 @@
         File optionsFile =
             new File.fromUri(directory.uri.resolve('folder.options'));
         if (optionsFile.existsSync()) {
-          List<String> experimentalFlagsArguments = [];
-          String overwriteCurrentSdkVersionArgument = null;
-          for (String line in optionsFile.readAsStringSync().split('\n')) {
-            line = line.trim();
-            if (line.startsWith(experimentalFlagOptions)) {
-              experimentalFlagsArguments =
-                  line.substring(experimentalFlagOptions.length).split(',');
-            } else if (line.startsWith(overwriteCurrentSdkVersion)) {
-              overwriteCurrentSdkVersionArgument =
-                  line.substring(overwriteCurrentSdkVersion.length);
-            } else if (line.startsWith(Flags.forceLateLoweringSentinel)) {
-              forceLateLoweringSentinel = true;
-            } else if (line.startsWith('${Flags.forceLateLowering}=')) {
-              int mask = int.parse(
-                  line.substring('${Flags.forceLateLowering}='.length));
-              forceLateLowering = mask;
-            } else if (line.startsWith(Flags.forceLateLowering)) {
-              forceLateLowering = LateLowering.all;
-            } else if (line.startsWith(Flags.forceStaticFieldLowering)) {
-              forceStaticFieldLowering = true;
-            } else if (line.startsWith(Flags.forceNoExplicitGetterCalls)) {
-              forceNoExplicitGetterCalls = true;
-            } else if (line.startsWith(Flags.forceNoExplicitGetterCalls)) {
-              forceNoExplicitGetterCalls = true;
-            } else if (line.startsWith(Flags.forceConstructorTearOffLowering)) {
-              forceConstructorTearOffLowering = ConstructorTearOffLowering.all;
-            } else if (line.startsWith(Flags.nnbdAgnosticMode)) {
-              nnbdAgnosticMode = true;
-            } else if (line.startsWith(Flags.noDefines)) {
-              if (defines == null) {
-                throw "Specifying ${Flags.noDefines} several times "
-                    "is unsupported.";
-              }
-              if (defines.isNotEmpty) {
-                throw "Can't have no defines and specific defines "
-                    "at the same time.";
-              }
-              defines = null;
-            } else if (line.startsWith("-D")) {
-              if (defines == null) {
-                throw "Can't have no defines and specific defines "
-                    "at the same time.";
-              }
-              String define = line.substring(2); // removes "-D".
-              int index = define.indexOf('=');
-              String name;
-              String expression;
-              if (index != -1) {
-                name = define.substring(0, index);
-                expression = define.substring(index + 1);
-              } else {
-                name = define;
-                expression = define;
-              }
-              if (defines.containsKey(name)) {
-                throw "Defining '$name' several times is unsupported.";
-              }
-              defines[name] = expression;
-            } else if (line.startsWith(noVerifyCmd)) {
-              noVerify = true;
-            } else if (line.startsWith(Flags.target) &&
-                line.indexOf('=') == Flags.target.length) {
-              target = line.substring(Flags.target.length + 1);
-            } else if (line.isNotEmpty) {
-              throw new UnsupportedError("Unsupported test option '$line'");
+          List<String> arguments =
+              ParsedOptions.readOptionsFile(optionsFile.readAsStringSync());
+          ParsedOptions parsedOptions =
+              ParsedOptions.parse(arguments, folderOptionsSpecification);
+          List<String> experimentalFlagsArguments =
+              Options.enableExperiment.read(parsedOptions) ?? <String>[];
+          String overwriteCurrentSdkVersionArgument =
+              overwriteCurrentSdkVersion.read(parsedOptions);
+          forceLateLoweringSentinel =
+              Options.forceLateLoweringSentinel.read(parsedOptions);
+          forceLateLowering = Options.forceLateLowering.read(parsedOptions);
+          forceStaticFieldLowering =
+              Options.forceStaticFieldLowering.read(parsedOptions);
+          forceNoExplicitGetterCalls =
+              Options.forceNoExplicitGetterCalls.read(parsedOptions);
+          forceConstructorTearOffLowering =
+              Options.forceConstructorTearOffLowering.read(parsedOptions);
+          nnbdAgnosticMode = Options.nnbdAgnosticMode.read(parsedOptions);
+          defines = parsedOptions.defines;
+          if (Options.noDefines.read(parsedOptions)) {
+            if (defines.isNotEmpty) {
+              throw "Can't have no defines and specific defines "
+                  "at the same time.";
             }
+            defines = null;
           }
-
+          noVerify = noVerifyCmd.read(parsedOptions);
+          target = Options.target.read(parsedOptions);
           folderOptions = new FolderOptions(
               parseExperimentalFlags(
                   parseExperimentalArguments(experimentalFlagsArguments),
@@ -590,56 +578,56 @@
       Map<ExperimentalFlag, Version> experimentEnabledVersion;
       Map<ExperimentalFlag, Version> experimentReleasedVersion;
       if (optionsFile.existsSync()) {
-        for (String line in optionsFile.readAsStringSync().split('\n')) {
-          line = line.trim();
-          if (line.isEmpty) continue;
-          if (line.startsWith(Flags.nnbdAgnosticMode)) {
-            if (nnbdMode != null) {
-              throw new UnsupportedError(
-                  'Nnbd mode $nnbdMode already specified.');
-            }
-            nnbdMode = NnbdMode.Agnostic;
-          } else if (line.startsWith(Flags.nnbdStrongMode)) {
-            if (nnbdMode != null) {
-              throw new UnsupportedError(
-                  'Nnbd mode $nnbdMode already specified.');
-            }
-            nnbdMode = NnbdMode.Strong;
-          } else if (line.startsWith(Flags.nnbdWeakMode)) {
-            if (nnbdMode != null) {
-              throw new UnsupportedError(
-                  'Nnbd mode $nnbdMode already specified.');
-            }
-            nnbdMode = NnbdMode.Weak;
-          } else if (line == '--fix-nnbd-release-version') {
-            // Allow package:allowed_package to use nnbd features from version
-            // 2.9.
-            allowedExperimentalFlags = new AllowedExperimentalFlags(
-                sdkDefaultExperiments:
-                    defaultAllowedExperimentalFlags.sdkDefaultExperiments,
-                sdkLibraryExperiments:
-                    defaultAllowedExperimentalFlags.sdkLibraryExperiments,
-                packageExperiments: {
-                  ...defaultAllowedExperimentalFlags.packageExperiments,
-                  'allowed_package': {ExperimentalFlag.nonNullable}
-                });
-            experimentEnabledVersion = const {
-              ExperimentalFlag.nonNullable: const Version(2, 10)
-            };
-            experimentReleasedVersion = const {
-              ExperimentalFlag.nonNullable: const Version(2, 9)
-            };
-          } else {
-            Uri uri = description.uri.resolve(line);
-            if (uri.scheme != 'package') {
-              File f = new File.fromUri(uri);
-              if (!f.existsSync()) {
-                throw new UnsupportedError("No file found: $f ($line)");
-              }
-              uri = f.uri;
-            }
-            linkDependencies.add(uri);
+        List<String> arguments =
+            ParsedOptions.readOptionsFile(optionsFile.readAsStringSync());
+        ParsedOptions parsedOptions =
+            ParsedOptions.parse(arguments, testOptionsSpecification);
+        if (Options.nnbdAgnosticMode.read(parsedOptions)) {
+          nnbdMode = NnbdMode.Agnostic;
+        }
+        if (Options.nnbdStrongMode.read(parsedOptions)) {
+          if (nnbdMode != null) {
+            throw new UnsupportedError(
+                'Nnbd mode $nnbdMode already specified.');
           }
+          nnbdMode = NnbdMode.Strong;
+        }
+        if (Options.nnbdWeakMode.read(parsedOptions)) {
+          if (nnbdMode != null) {
+            throw new UnsupportedError(
+                'Nnbd mode $nnbdMode already specified.');
+          }
+          nnbdMode = NnbdMode.Weak;
+        }
+        if (fixNnbdReleaseVersion.read(parsedOptions)) {
+          // Allow package:allowed_package to use nnbd features from version
+          // 2.9.
+          allowedExperimentalFlags = new AllowedExperimentalFlags(
+              sdkDefaultExperiments:
+                  defaultAllowedExperimentalFlags.sdkDefaultExperiments,
+              sdkLibraryExperiments:
+                  defaultAllowedExperimentalFlags.sdkLibraryExperiments,
+              packageExperiments: {
+                ...defaultAllowedExperimentalFlags.packageExperiments,
+                'allowed_package': {ExperimentalFlag.nonNullable}
+              });
+          experimentEnabledVersion = const {
+            ExperimentalFlag.nonNullable: const Version(2, 10)
+          };
+          experimentReleasedVersion = const {
+            ExperimentalFlag.nonNullable: const Version(2, 9)
+          };
+        }
+        for (String argument in parsedOptions.arguments) {
+          Uri uri = description.uri.resolve(argument);
+          if (uri.scheme != 'package') {
+            File f = new File.fromUri(uri);
+            if (!f.existsSync()) {
+              throw new UnsupportedError("No file found: $f ($argument)");
+            }
+            uri = f.uri;
+          }
+          linkDependencies.add(uri);
         }
       }
       testOptions = new TestOptions(linkDependencies,
diff --git a/pkg/front_end/test/resolve_input_uri_test.dart b/pkg/front_end/test/resolve_input_uri_test.dart
deleted file mode 100644
index 3705440..0000000
--- a/pkg/front_end/test/resolve_input_uri_test.dart
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) 2018, 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.
-
-// @dart = 2.9
-
-import 'package:_fe_analyzer_shared/src/util/relativize.dart';
-import 'package:expect/expect.dart' show Expect;
-import 'package:front_end/src/fasta/resolve_input_uri.dart';
-
-test() {
-  // data URI scheme is supported by default'.
-  Expect.stringEquals('data', resolveInputUri('data:,foo').scheme);
-
-  // Custom Dart schemes are recognized by default.
-  Expect.stringEquals('dart', resolveInputUri('dart:foo').scheme);
-  Expect.stringEquals('package', resolveInputUri('package:foo').scheme);
-
-  // Unknown schemes are recognized by default.
-  Expect.stringEquals(
-      isWindows ? 'file' : 'c', resolveInputUri('c:/foo').scheme);
-  Expect.stringEquals('test', resolveInputUri('test:foo').scheme);
-  Expect.stringEquals(
-      'org-dartlang-foo', resolveInputUri('org-dartlang-foo:bar').scheme);
-  Expect.stringEquals('test', resolveInputUri('test:/foo').scheme);
-  Expect.stringEquals(
-      'org-dartlang-foo', resolveInputUri('org-dartlang-foo:/bar').scheme);
-  Expect.stringEquals(
-      "${Uri.base.resolve('file.txt')}", "${resolveInputUri('file:file.txt')}");
-}
-
-main() {
-  // Test platform default.
-  test();
-  // Test non-Windows behavior.
-  isWindows = false;
-  test();
-  // Test Windows behavior.
-  isWindows = true;
-  test();
-}
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 7cc2003..aed39dd 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -286,6 +286,7 @@
 dartdoc
 dartfix
 dartlang
+dashes
 dc
 ddc
 ddk
@@ -577,6 +578,7 @@
 inc
 incomparable
 inconsistency
+increased
 incremented
 independently
 indexer
diff --git a/pkg/front_end/test/spell_checking_list_common.txt b/pkg/front_end/test/spell_checking_list_common.txt
index 0b93c1a..73be42a 100644
--- a/pkg/front_end/test/spell_checking_list_common.txt
+++ b/pkg/front_end/test/spell_checking_list_common.txt
@@ -1866,6 +1866,8 @@
 metadata
 method
 methods
+metric
+metrics
 microseconds
 microtask
 mid
diff --git a/pkg/front_end/test/unit_test_suites_impl.dart b/pkg/front_end/test/unit_test_suites_impl.dart
index 5c6ccff..cbf00f0 100644
--- a/pkg/front_end/test/unit_test_suites_impl.dart
+++ b/pkg/front_end/test/unit_test_suites_impl.dart
@@ -101,37 +101,37 @@
       ..addFlag("onlyTestsThatRequireGit",
           help: "Whether to only run tests that require git",
           defaultsTo: false);
-    var parsedArguments = parser.parse(args);
-    String outputPath = parsedArguments["output-directory"] ?? ".";
+    var parsedOptions = parser.parse(args);
+    String outputPath = parsedOptions["output-directory"] ?? ".";
     Uri outputDirectory = Uri.base.resolveUri(Uri.directory(outputPath));
 
-    bool verbose = parsedArguments["verbose"];
+    bool verbose = parsedOptions["verbose"];
 
     String? filter;
-    if (parsedArguments.rest.length == 1) {
-      filter = parsedArguments.rest.single;
+    if (parsedOptions.rest.length == 1) {
+      filter = parsedOptions.rest.single;
       if (filter.startsWith("$suiteNamePrefix/")) {
         filter = filter.substring(suiteNamePrefix.length + 1);
       }
     }
-    String tasksString = parsedArguments["tasks"];
+    String tasksString = parsedOptions["tasks"];
     int? tasks = int.tryParse(tasksString);
     if (tasks == null || tasks < 1) {
       throw "--tasks (-j) has to be an integer >= 1";
     }
 
-    String shardsString = parsedArguments["shards"];
+    String shardsString = parsedOptions["shards"];
     int? shardCount = int.tryParse(shardsString);
     if (shardCount == null || shardCount < 1) {
       throw "--shards has to be an integer >= 1";
     }
-    String shardString = parsedArguments["shard"];
+    String shardString = parsedOptions["shard"];
     int? shard = int.tryParse(shardString);
     if (shard == null || shard < 1 || shard > shardCount) {
       throw "--shard has to be an integer >= 1 (and <= --shards)";
     }
-    bool skipTestsThatRequireGit = parsedArguments["skipTestsThatRequireGit"];
-    bool onlyTestsThatRequireGit = parsedArguments["onlyTestsThatRequireGit"];
+    bool skipTestsThatRequireGit = parsedOptions["skipTestsThatRequireGit"];
+    bool onlyTestsThatRequireGit = parsedOptions["onlyTestsThatRequireGit"];
     if (skipTestsThatRequireGit && onlyTestsThatRequireGit) {
       throw "Only one of --skipTestsThatRequireGit and "
           "--onlyTestsThatRequireGit can be provided.";
@@ -139,12 +139,12 @@
 
     if (verbose) {
       print("NOTE: Created with options\n  "
-          "${parsedArguments["named-configuration"]},\n  "
+          "${parsedOptions["named-configuration"]},\n  "
           "${verbose},\n  "
-          "${parsedArguments["print"]},\n  "
+          "${parsedOptions["print"]},\n  "
           "${outputDirectory},\n  "
           "${filter},\n  "
-          "${parsedArguments['environment']},\n  "
+          "${parsedOptions['environment']},\n  "
           "shardCount: ${shardCount},\n  "
           "shard: ${shard - 1 /* make it 0-indexed */},\n  "
           "onlyTestsThatRequireGit: ${onlyTestsThatRequireGit},\n  "
@@ -153,12 +153,12 @@
     }
 
     return Options(
-      parsedArguments["named-configuration"],
+      parsedOptions["named-configuration"],
       verbose,
-      parsedArguments["print"],
+      parsedOptions["print"],
       outputDirectory,
       filter,
-      parsedArguments['environment'],
+      parsedOptions['environment'],
       shardCount: shardCount,
       shard: shard - 1 /* make it 0-indexed */,
       onlyTestsThatRequireGit: onlyTestsThatRequireGit,
diff --git a/pkg/front_end/tool/_fasta/command_line.dart b/pkg/front_end/tool/_fasta/command_line.dart
index 6ab0e47..a4a76a2 100644
--- a/pkg/front_end/tool/_fasta/command_line.dart
+++ b/pkg/front_end/tool/_fasta/command_line.dart
@@ -7,6 +7,7 @@
 import 'dart:io' show exit;
 
 import 'package:_fe_analyzer_shared/src/messages/severity.dart' show Severity;
+import 'package:_fe_analyzer_shared/src/util/options.dart';
 
 import 'package:build_integration/file_system/single_root.dart'
     show SingleRootFileSystem;
@@ -39,230 +40,102 @@
         PlainAndColorizedString,
         messageFastaUsageLong,
         messageFastaUsageShort,
-        templateFastaCLIArgumentRequired,
         templateUnspecified;
 
 import 'package:front_end/src/fasta/problems.dart' show DebugAbort;
 
-import 'package:front_end/src/fasta/resolve_input_uri.dart'
+import 'package:_fe_analyzer_shared/src/util/resolve_input_uri.dart'
     show resolveInputUri;
 
 import 'package:front_end/src/scheme_based_file_system.dart'
     show SchemeBasedFileSystem;
 
 import 'package:kernel/target/targets.dart'
-    show
-        ConstructorTearOffLowering,
-        LateLowering,
-        Target,
-        TargetFlags,
-        getTarget,
-        targets;
-
-class CommandLineProblem {
-  final Message message;
-
-  CommandLineProblem(this.message);
-
-  CommandLineProblem.deprecated(String message)
-      : this(templateUnspecified.withArguments(message));
-}
-
-class ParsedArguments {
-  final Map<String, dynamic> options = <String, dynamic>{};
-  final List<String> arguments = <String>[];
-  final Map<String, String> defines = <String, String>{};
-
-  toString() => "ParsedArguments($options, $arguments)";
-
-  /// Parses a list of command-line [arguments] into options and arguments.
-  ///
-  /// An /option/ is something that, normally, starts with `-` or `--` (one or
-  /// two dashes). However, as a special case `/?` and `/h` are also recognized
-  /// as options for increased compatibility with Windows. An option can have a
-  /// value.
-  ///
-  /// An /argument/ is something that isn't an option, for example, a file name.
-  ///
-  /// The specification is a map of options to one of the following values:
-  /// * the type literal `Uri`, representing an option value of type [Uri],
-  /// * the type literal `int`, representing an option value of type [int],
-  /// * the bool literal `false`, representing a boolean option that is turned
-  ///   off by default,
-  /// * the bool literal `true, representing a boolean option that is turned on
-  ///   by default,
-  /// * or the string literal `","`, representing a comma-separated list of
-  ///   values.
-  ///
-  /// If [arguments] contains `"--"`, anything before is parsed as options, and
-  /// arguments; anything following is treated as arguments (even if starting
-  /// with, for example, a `-`).
-  ///
-  /// If an option isn't found in [specification], an error is thrown.
-  ///
-  /// Boolean options do not require an option value, but an optional value can
-  /// be provided using the forms `--option=value` where `value` can be `true`
-  /// or `yes` to turn on the option, or `false` or `no` to turn it off.  If no
-  /// option value is specified, a boolean option is turned on.
-  ///
-  /// All other options require an option value, either on the form `--option
-  /// value` or `--option=value`.
-  static ParsedArguments parse(
-      List<String> arguments, Map<String, ValueSpecification>? specification) {
-    specification ??= const <String, ValueSpecification>{};
-    ParsedArguments result = new ParsedArguments();
-    int index = arguments.indexOf("--");
-    Iterable<String> nonOptions = const <String>[];
-    Iterator<String> iterator = arguments.iterator;
-    if (index != -1) {
-      nonOptions = arguments.skip(index + 1);
-      iterator = arguments.take(index).iterator;
-    }
-    while (iterator.moveNext()) {
-      String argument = iterator.current;
-      if (argument.startsWith("-") || argument == "/?" || argument == "/h") {
-        String? value;
-        if (argument.startsWith("-D")) {
-          value = argument.substring("-D".length);
-          argument = "-D";
-        } else {
-          index = argument.indexOf("=");
-          if (index != -1) {
-            value = argument.substring(index + 1);
-            argument = argument.substring(0, index);
-          }
-        }
-        ValueSpecification? valueSpecification = specification[argument];
-        if (valueSpecification == null) {
-          throw new CommandLineProblem.deprecated(
-              "Unknown option '$argument'.");
-        }
-        String canonicalArgument = argument;
-        if (valueSpecification.alias != null) {
-          canonicalArgument = valueSpecification.alias as String;
-          valueSpecification = specification[valueSpecification.alias];
-        }
-        if (valueSpecification == null) {
-          throw new CommandLineProblem.deprecated(
-              "Unknown option alias '$canonicalArgument'.");
-        }
-        final bool requiresValue = valueSpecification.requiresValue;
-        if (requiresValue && value == null) {
-          if (!iterator.moveNext()) {
-            throw new CommandLineProblem(
-                templateFastaCLIArgumentRequired.withArguments(argument));
-          }
-          value = iterator.current;
-        }
-        valueSpecification.processValue(
-            result, canonicalArgument, argument, value);
-      } else {
-        result.arguments.add(argument);
-      }
-    }
-    specification.forEach((String key, ValueSpecification value) {
-      if (value.defaultValue != null) {
-        result.options[key] ??= value.defaultValue;
-      }
-    });
-    result.arguments.addAll(nonOptions);
-    return result;
-  }
-}
+    show Target, TargetFlags, getTarget, targets;
 
 // Before adding new options here, you must:
 //  * Document the option.
 //  * Get an explicit approval from the front-end team.
-const Map<String, ValueSpecification> optionSpecification =
-    const <String, ValueSpecification>{
-  Flags.compileSdk: const UriValue(),
-  Flags.dumpIr: const BoolValue(false),
-  Flags.enableExperiment: const StringListValue(),
-  Flags.excludeSource: const BoolValue(false),
-  Flags.omitPlatform: const BoolValue(false),
-  Flags.fatal: const StringListValue(),
-  Flags.fatalSkip: const StringValue(),
-  Flags.forceLateLowering: const BoolValue(false),
-  Flags.forceStaticFieldLowering: const BoolValue(false),
-  Flags.forceNoExplicitGetterCalls: const BoolValue(false),
-  Flags.forceConstructorTearOffLowering: const BoolValue(false),
-  Flags.help: const BoolValue(false),
-  Flags.librariesJson: const UriValue(),
-  Flags.noDefines: const BoolValue(false),
-  Flags.output: const UriValue(),
-  Flags.packages: const UriValue(),
-  Flags.platform: const UriValue(),
-  Flags.sdk: const UriValue(),
-  Flags.singleRootBase: const UriValue(),
-  Flags.singleRootScheme: const StringValue(),
-  Flags.nnbdWeakMode: const BoolValue(false),
-  Flags.nnbdStrongMode: const BoolValue(false),
-  Flags.nnbdAgnosticMode: const BoolValue(false),
-  Flags.target: const StringValue(),
-  Flags.verbose: const BoolValue(false),
-  Flags.verbosity: const StringValue(),
-  Flags.verify: const BoolValue(false),
-  Flags.skipPlatformVerification: const BoolValue(false),
-  Flags.warnOnReachabilityCheck: const BoolValue(false),
-  Flags.linkDependencies: const UriListValue(),
-  Flags.noDeps: const BoolValue(false),
-  Flags.invocationModes: const StringValue(),
-  "-D": const DefineValue(),
-  "--define": const AliasValue("-D"),
-  "-h": const AliasValue(Flags.help),
-  "--out": const AliasValue(Flags.output),
-  "-o": const AliasValue(Flags.output),
-  "-t": const AliasValue(Flags.target),
-  "-v": const AliasValue(Flags.verbose),
-  "/?": const AliasValue(Flags.help),
-  "/h": const AliasValue(Flags.help),
-};
+const List<Option> optionSpecification = [
+  Options.compileSdk,
+  Options.dumpIr,
+  Options.enableExperiment,
+  Options.excludeSource,
+  Options.omitPlatform,
+  Options.fatal,
+  Options.fatalSkip,
+  Options.forceLateLowering,
+  Options.forceLateLoweringSentinel,
+  Options.forceStaticFieldLowering,
+  Options.forceNoExplicitGetterCalls,
+  Options.forceConstructorTearOffLowering,
+  Options.help,
+  Options.librariesJson,
+  Options.noDefines,
+  Options.output,
+  Options.packages,
+  Options.platform,
+  Options.sdk,
+  Options.singleRootBase,
+  Options.singleRootScheme,
+  Options.nnbdWeakMode,
+  Options.nnbdStrongMode,
+  Options.nnbdAgnosticMode,
+  Options.target,
+  Options.verbose,
+  Options.verbosity,
+  Options.verify,
+  Options.skipPlatformVerification,
+  Options.warnOnReachabilityCheck,
+  Options.linkDependencies,
+  Options.noDeps,
+  Options.invocationModes,
+  Options.defines,
+];
 
 void throwCommandLineProblem(String message) {
   throw new CommandLineProblem.deprecated(message);
 }
 
 ProcessedOptions analyzeCommandLine(String programName,
-    ParsedArguments parsedArguments, bool areRestArgumentsInputs) {
-  final Map<String, dynamic> options = parsedArguments.options;
+    ParsedOptions parsedOptions, bool areRestArgumentsInputs) {
+  final List<String> arguments = parsedOptions.arguments;
 
-  final List<String> arguments = parsedArguments.arguments;
+  final bool help = Options.help.read(parsedOptions);
 
-  final bool help = options[Flags.help];
-
-  final bool verbose = options[Flags.verbose];
+  final bool verbose = Options.verbose.read(parsedOptions);
 
   if (help) {
     print(computeUsage(programName, verbose).message);
     exit(0);
   }
 
-  if (options.containsKey(Flags.compileSdk) &&
-      options.containsKey(Flags.platform)) {
+  if (parsedOptions.options.containsKey(Flags.compileSdk) &&
+      parsedOptions.options.containsKey(Flags.platform)) {
     return throw new CommandLineProblem.deprecated(
         "Can't specify both '${Flags.compileSdk}' and '${Flags.platform}'.");
   }
 
-  final String targetName = options[Flags.target] ?? "vm";
+  final String targetName = Options.target.read(parsedOptions);
 
   Map<ExperimentalFlag, bool> explicitExperimentalFlags =
       parseExperimentalFlags(
-          parseExperimentalArguments(options[Flags.enableExperiment]),
+          parseExperimentalArguments(
+              Options.enableExperiment.read(parsedOptions)),
           onError: throwCommandLineProblem,
           onWarning: print);
 
   final TargetFlags flags = new TargetFlags(
-      forceLateLoweringsForTesting: options[Flags.forceLateLowering]
-          ? LateLowering.all
-          : LateLowering.none,
+      forceLateLoweringsForTesting:
+          Options.forceLateLowering.read(parsedOptions),
       forceStaticFieldLoweringForTesting:
-          options[Flags.forceStaticFieldLowering],
+          Options.forceStaticFieldLowering.read(parsedOptions),
       forceNoExplicitGetterCallsForTesting:
-          options[Flags.forceNoExplicitGetterCalls],
+          Options.forceNoExplicitGetterCalls.read(parsedOptions),
       forceConstructorTearOffLoweringForTesting:
-          options[Flags.forceConstructorTearOffLowering]
-              ? ConstructorTearOffLowering.all
-              : ConstructorTearOffLowering.none,
+          Options.forceConstructorTearOffLowering.read(parsedOptions),
+      forceLateLoweringSentinelForTesting:
+          Options.forceLateLoweringSentinel.read(parsedOptions),
       enableNullSafety: isExperimentEnabled(ExperimentalFlag.nonNullable,
           explicitExperimentalFlags: explicitExperimentalFlags));
 
@@ -273,53 +146,58 @@
         "Valid targets are:\n  ${targets.keys.join("\n  ")}");
   }
 
-  final bool noDefines = options[Flags.noDefines];
+  final bool noDefines = Options.noDefines.read(parsedOptions);
 
-  final bool noDeps = options[Flags.noDeps];
+  final bool noDeps = Options.noDeps.read(parsedOptions);
 
-  final bool verify = options[Flags.verify];
+  final bool verify = Options.verify.read(parsedOptions);
 
-  final bool skipPlatformVerification = options[Flags.skipPlatformVerification];
+  final bool skipPlatformVerification =
+      Options.skipPlatformVerification.read(parsedOptions);
 
-  final bool dumpIr = options[Flags.dumpIr];
+  final bool dumpIr = Options.dumpIr.read(parsedOptions);
 
-  final bool excludeSource = options[Flags.excludeSource];
+  final bool excludeSource = Options.excludeSource.read(parsedOptions);
 
-  final bool omitPlatform = options[Flags.omitPlatform];
+  final bool omitPlatform = Options.omitPlatform.read(parsedOptions);
 
-  final Uri packages = options[Flags.packages];
+  final Uri? packages = Options.packages.read(parsedOptions);
 
   final Set<String> fatal =
-      new Set<String>.from(options[Flags.fatal] ?? <String>[]);
+      new Set<String>.from(Options.fatal.read(parsedOptions) ?? <String>[]);
 
   final bool errorsAreFatal = fatal.contains("errors");
 
   final bool warningsAreFatal = fatal.contains("warnings");
 
-  final int fatalSkip = int.tryParse(options[Flags.fatalSkip] ?? "0") ?? -1;
+  final int fatalSkip =
+      int.tryParse(Options.fatalSkip.read(parsedOptions) ?? "0") ?? -1;
 
-  final bool compileSdk = options.containsKey(Flags.compileSdk);
+  final bool compileSdk = Options.compileSdk.read(parsedOptions) != null;
 
-  final String? singleRootScheme = options[Flags.singleRootScheme];
-  final Uri singleRootBase = options[Flags.singleRootBase];
+  final String? singleRootScheme = Options.singleRootScheme.read(parsedOptions);
+  final Uri? singleRootBase = Options.singleRootBase.read(parsedOptions);
 
-  final bool nnbdStrongMode = options[Flags.nnbdStrongMode];
+  final bool nnbdStrongMode = Options.nnbdStrongMode.read(parsedOptions);
 
-  final bool nnbdWeakMode = options[Flags.nnbdWeakMode];
+  final bool nnbdWeakMode = Options.nnbdWeakMode.read(parsedOptions);
 
-  final bool nnbdAgnosticMode = options[Flags.nnbdAgnosticMode];
+  final bool nnbdAgnosticMode = Options.nnbdAgnosticMode.read(parsedOptions);
 
   final NnbdMode nnbdMode = nnbdAgnosticMode
       ? NnbdMode.Agnostic
       : (nnbdStrongMode ? NnbdMode.Strong : NnbdMode.Weak);
 
-  final bool warnOnReachabilityCheck = options[Flags.warnOnReachabilityCheck];
+  final bool warnOnReachabilityCheck =
+      Options.warnOnReachabilityCheck.read(parsedOptions);
 
-  final List<Uri> linkDependencies = options[Flags.linkDependencies] ?? [];
+  final List<Uri> linkDependencies =
+      Options.linkDependencies.read(parsedOptions) ?? [];
 
-  final String invocationModes = options[Flags.invocationModes] ?? '';
+  final String invocationModes =
+      Options.invocationModes.read(parsedOptions) ?? '';
 
-  final String verbosity = options[Flags.verbosity] ?? Verbosity.defaultValue;
+  final String verbosity = Options.verbosity.read(parsedOptions);
 
   if (nnbdStrongMode && nnbdWeakMode) {
     return throw new CommandLineProblem.deprecated(
@@ -348,7 +226,7 @@
       // should have been handled elsewhere).
       '': fileSystem,
       singleRootScheme: new SingleRootFileSystem(
-          singleRootScheme, singleRootBase, fileSystem),
+          singleRootScheme, singleRootBase!, fileSystem),
     });
   }
 
@@ -367,7 +245,7 @@
     ..verify = verify
     ..skipPlatformVerification = skipPlatformVerification
     ..explicitExperimentalFlags = explicitExperimentalFlags
-    ..environmentDefines = noDefines ? null : parsedArguments.defines
+    ..environmentDefines = noDefines ? null : parsedOptions.defines
     ..nnbdMode = nnbdMode
     ..additionalDills = linkDependencies
     ..emitDeps = !noDeps
@@ -384,14 +262,14 @@
       return throw new CommandLineProblem.deprecated(
           "Cannot specify '${Flags.compileSdk}' option to compile_platform.");
     }
-    if (options.containsKey(Flags.output)) {
+    if (parsedOptions.options.containsKey(Flags.output)) {
       return throw new CommandLineProblem.deprecated(
           "Cannot specify '${Flags.output}' option to compile_platform.");
     }
 
     return new ProcessedOptions(
         options: compilerOptions
-          ..sdkSummary = options[Flags.platform]
+          ..sdkSummary = Options.platform.read(parsedOptions)
           ..librariesSpecificationUri = resolveInputUri(arguments[1])
           ..setExitCodeOnProblem = true,
         inputs: <Uri>[Uri.parse(arguments[0])],
@@ -402,15 +280,16 @@
 
   final Uri defaultOutput = resolveInputUri("${arguments.first}.dill");
 
-  final Uri output = options[Flags.output] ?? defaultOutput;
+  final Uri output = Options.output.read(parsedOptions) ?? defaultOutput;
 
-  final Uri sdk = options[Flags.sdk] ?? options[Flags.compileSdk];
+  final Uri? sdk =
+      Options.sdk.read(parsedOptions) ?? Options.compileSdk.read(parsedOptions);
 
-  final Uri librariesJson = options[Flags.librariesJson];
+  final Uri? librariesJson = Options.librariesJson.read(parsedOptions);
 
-  final Uri platform = compileSdk
+  final Uri? platform = compileSdk
       ? null
-      : (options[Flags.platform] ??
+      : (Options.platform.read(parsedOptions) ??
           computePlatformBinariesLocation(forceBuildDir: true)
               .resolve(computePlatformDillName(target, nnbdMode, () {
             throwCommandLineProblem(
@@ -437,13 +316,13 @@
     List<String> arguments,
     bool areRestArgumentsInputs,
     Future<T> f(CompilerContext context, List<String> restArguments)) {
-  ParsedArguments? parsedArguments;
+  ParsedOptions? parsedOptions;
   ProcessedOptions options;
   CommandLineProblem? problem;
   try {
-    parsedArguments = ParsedArguments.parse(arguments, optionSpecification);
-    options = analyzeCommandLine(
-        programName, parsedArguments, areRestArgumentsInputs);
+    parsedOptions = ParsedOptions.parse(arguments, optionSpecification);
+    options =
+        analyzeCommandLine(programName, parsedOptions, areRestArgumentsInputs);
   } on CommandLineProblem catch (e) {
     options = new ProcessedOptions();
     problem = e;
@@ -464,7 +343,7 @@
       exit(1);
     }
 
-    return f(c, parsedArguments!.arguments);
+    return f(c, parsedOptions!.arguments);
   }, errorOnMissingInput: problem == null);
 }
 
@@ -521,148 +400,3 @@
     exit(255);
   }
 }
-
-abstract class ValueSpecification {
-  const ValueSpecification();
-
-  String? get alias => null;
-
-  dynamic get defaultValue => null;
-
-  bool get requiresValue => true;
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value);
-}
-
-class AliasValue extends ValueSpecification {
-  final String alias;
-
-  const AliasValue(this.alias);
-
-  bool get requiresValue =>
-      throw new UnsupportedError("AliasValue.requiresValue");
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value) {
-    throw new UnsupportedError("AliasValue.processValue");
-  }
-}
-
-class UriValue extends ValueSpecification {
-  const UriValue();
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value) {
-    if (result.options.containsKey(canonicalArgument)) {
-      throw new CommandLineProblem.deprecated(
-          "Multiple values for '$argument': "
-          "'${result.options[canonicalArgument]}' and '$value'.");
-    }
-    // TODO(ahe): resolve Uris lazily, so that schemes provided by
-    // other flags can be used for parsed command-line arguments too.
-    result.options[canonicalArgument] = resolveInputUri(value!);
-  }
-}
-
-class StringValue extends ValueSpecification {
-  const StringValue();
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value) {
-    if (result.options.containsKey(canonicalArgument)) {
-      throw new CommandLineProblem.deprecated(
-          "Multiple values for '$argument': "
-          "'${result.options[canonicalArgument]}' and '$value'.");
-    }
-    result.options[canonicalArgument] = value!;
-  }
-}
-
-class BoolValue extends ValueSpecification {
-  final bool defaultValue;
-
-  const BoolValue(this.defaultValue);
-
-  bool get requiresValue => false;
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value) {
-    if (result.options.containsKey(canonicalArgument)) {
-      throw new CommandLineProblem.deprecated(
-          "Multiple values for '$argument': "
-          "'${result.options[canonicalArgument]}' and '$value'.");
-    }
-    bool parsedValue;
-    if (value == null || value == "true" || value == "yes") {
-      parsedValue = true;
-    } else if (value == "false" || value == "no") {
-      parsedValue = false;
-    } else {
-      throw new CommandLineProblem.deprecated(
-          "Value for '$argument' is '$value', "
-          "but expected one of: 'true', 'false', 'yes', or 'no'.");
-    }
-    result.options[canonicalArgument] = parsedValue;
-  }
-}
-
-class IntValue extends ValueSpecification {
-  const IntValue();
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value) {
-    if (result.options.containsKey(canonicalArgument)) {
-      throw new CommandLineProblem.deprecated(
-          "Multiple values for '$argument': "
-          "'${result.options[canonicalArgument]}' and '$value'.");
-    }
-    int? parsedValue = int.tryParse(value!);
-    if (parsedValue == null) {
-      throw new CommandLineProblem.deprecated(
-          "Value for '$argument', '$value', isn't an int.");
-    }
-    result.options[canonicalArgument] = parsedValue;
-  }
-}
-
-class DefineValue extends ValueSpecification {
-  const DefineValue();
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value) {
-    int index = value!.indexOf('=');
-    String name;
-    String expression;
-    if (index != -1) {
-      name = value.substring(0, index);
-      expression = value.substring(index + 1);
-    } else {
-      name = value;
-      expression = value;
-    }
-    result.defines[name] = expression;
-  }
-}
-
-class StringListValue extends ValueSpecification {
-  const StringListValue();
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value) {
-    result.options
-        .putIfAbsent(canonicalArgument, () => <String>[])
-        .addAll(value!.split(","));
-  }
-}
-
-class UriListValue extends ValueSpecification {
-  const UriListValue();
-
-  void processValue(ParsedArguments result, String canonicalArgument,
-      String argument, String? value) {
-    result.options
-        .putIfAbsent(canonicalArgument, () => <Uri>[])
-        .addAll(value!.split(",").map(resolveInputUri));
-  }
-}
diff --git a/pkg/js_runtime/lib/shared/embedded_names.dart b/pkg/js_runtime/lib/shared/embedded_names.dart
index ec45847..0334810 100644
--- a/pkg/js_runtime/lib/shared/embedded_names.dart
+++ b/pkg/js_runtime/lib/shared/embedded_names.dart
@@ -179,6 +179,9 @@
 /// This embedded global is used for --experiment-new-rti.
 const RTI_UNIVERSE = 'typeUniverse';
 
+/// An embedded global used to collect and access startup metrics.
+const STARTUP_METRICS = 'sm';
+
 /// Names that are supported by [JS_GET_NAME].
 // TODO(herhut): Make entries lower case (as in fields) and find a better name.
 enum JsGetName {
diff --git a/pkg/vm_service/CHANGELOG.md b/pkg/vm_service/CHANGELOG.md
index 3c1a964..d2a4144 100644
--- a/pkg/vm_service/CHANGELOG.md
+++ b/pkg/vm_service/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## 7.2.0
+- Update to version `3.50` of the spec.
+- Added `CpuSamples` event kind, and `cpuSamples` property to `Event`.
+- Added `returnType`, `parameters`, and `typeParameters` to `InstanceRef`,
+  and `implicit` to `FunctionRef`.
+- Added `Parameter` type.
+
 ## 7.1.1
 - Update to version `3.48` of the spec.
 - Added `shows` and `hides` properties to `LibraryDependency`.
diff --git a/pkg/vm_service/pubspec.yaml b/pkg/vm_service/pubspec.yaml
index 5aad6b6..5677862 100644
--- a/pkg/vm_service/pubspec.yaml
+++ b/pkg/vm_service/pubspec.yaml
@@ -3,7 +3,7 @@
   A library to communicate with a service implementing the Dart VM
   service protocol.
 
-version: 7.1.1
+version: 7.2.0
 
 homepage: https://github.com/dart-lang/sdk/tree/master/pkg/vm_service
 
diff --git a/runtime/vm/clustered_snapshot.cc b/runtime/vm/clustered_snapshot.cc
index b3beb4d..2b984a0 100644
--- a/runtime/vm/clustered_snapshot.cc
+++ b/runtime/vm/clustered_snapshot.cc
@@ -5152,202 +5152,145 @@
 };
 
 #if !defined(DART_PRECOMPILED_RUNTIME)
-class OneByteStringSerializationCluster : public SerializationCluster {
+class StringSerializationCluster
+    : public CanonicalSetSerializationCluster<CanonicalStringSet,
+                                              String,
+                                              StringPtr> {
  public:
-  explicit OneByteStringSerializationCluster(bool is_canonical)
-      : SerializationCluster("OneByteString",
-                             kOneByteStringCid,
-                             kSizeVaries,
-                             is_canonical) {}
-  ~OneByteStringSerializationCluster() {}
+  // To distinguish one and two byte strings, we put a bit in the length to
+  // indicate which it is. The length is an unsigned SMI, so we actually have
+  // two spare bits available. Keep in sync with DecodeLengthAndCid.
+  static intptr_t EncodeLengthAndCid(intptr_t length, intptr_t cid) {
+    ASSERT(cid == kOneByteStringCid || cid == kTwoByteStringCid);
+    ASSERT(length <= compiler::target::kSmiMax);
+    return (length << 1) | (cid == kTwoByteStringCid ? 0x1 : 0x0);
+  }
+
+  explicit StringSerializationCluster(bool is_canonical,
+                                      bool represents_canonical_set)
+      : CanonicalSetSerializationCluster(kStringCid,
+                                         is_canonical,
+                                         represents_canonical_set,
+                                         "String",
+                                         kSizeVaries) {}
+  ~StringSerializationCluster() {}
 
   void Trace(Serializer* s, ObjectPtr object) {
-    OneByteStringPtr str = static_cast<OneByteStringPtr>(object);
+    StringPtr str = static_cast<StringPtr>(object);
     objects_.Add(str);
   }
 
   void WriteAlloc(Serializer* s) {
     const intptr_t count = objects_.length();
     s->WriteUnsigned(count);
+    ReorderObjects(s);
     for (intptr_t i = 0; i < count; i++) {
-      OneByteStringPtr str = objects_[i];
+      StringPtr str = objects_[i];
       s->AssignRef(str);
       AutoTraceObject(str);
+      const intptr_t cid = str->GetClassId();
       const intptr_t length = Smi::Value(str->untag()->length());
-      s->WriteUnsigned(length);
+      const intptr_t encoded = EncodeLengthAndCid(length, cid);
+      s->WriteUnsigned(encoded);
       target_memory_size_ +=
-          compiler::target::OneByteString::InstanceSize(length);
+          cid == kOneByteStringCid
+              ? compiler::target::OneByteString::InstanceSize(length)
+              : compiler::target::TwoByteString::InstanceSize(length);
     }
+    WriteCanonicalSetLayout(s);
   }
 
   void WriteFill(Serializer* s) {
     const intptr_t count = objects_.length();
     for (intptr_t i = 0; i < count; i++) {
-      OneByteStringPtr str = objects_[i];
+      StringPtr str = objects_[i];
       AutoTraceObject(str);
+      const intptr_t cid = str->GetClassId();
       const intptr_t length = Smi::Value(str->untag()->length());
-      ASSERT(length <= compiler::target::kSmiMax);
-      s->WriteUnsigned(length);
-      s->WriteBytes(str->untag()->data(), length);
+      const intptr_t encoded = EncodeLengthAndCid(length, cid);
+      s->WriteUnsigned(encoded);
+      if (cid == kOneByteStringCid) {
+        s->WriteBytes(static_cast<OneByteStringPtr>(str)->untag()->data(),
+                      length);
+      } else {
+        s->WriteBytes(reinterpret_cast<uint8_t*>(
+                          static_cast<TwoByteStringPtr>(str)->untag()->data()),
+                      length * 2);
+      }
     }
   }
-
- private:
-  GrowableArray<OneByteStringPtr> objects_;
 };
 #endif  // !DART_PRECOMPILED_RUNTIME
 
-class StringDeserializationCluster : public DeserializationCluster {
- protected:
-  StringDeserializationCluster(const char* name, bool is_canonical)
-      : DeserializationCluster(name, is_canonical) {}
-
+class StringDeserializationCluster
+    : public CanonicalSetDeserializationCluster<CanonicalStringSet> {
  public:
-#if defined(DART_PRECOMPILED_RUNTIME)
-  void PostLoad(Deserializer* d, const Array& refs, bool primary) {
-    if (!primary && is_canonical()) {
-      auto Z = d->zone();
-      auto isolate_group = d->isolate_group();
-      SafepointMutexLocker ml(isolate_group->constant_canonicalization_mutex());
-      CanonicalStringSet table(Z,
-                               isolate_group->object_store()->symbol_table());
-      String& str = String::Handle(Z);
-      String& str2 = String::Handle(Z);
-      for (intptr_t i = start_index_; i < stop_index_; i++) {
-        str ^= refs.At(i);
-        str2 ^= table.InsertOrGet(str);
-        if (str.ptr() == str2.ptr()) {
-          str.SetCanonical();
-        } else {
-          refs.SetAt(i, str2);
+  static intptr_t DecodeLengthAndCid(intptr_t encoded, intptr_t* out_cid) {
+    *out_cid = (encoded & 0x1) != 0 ? kTwoByteStringCid : kOneByteStringCid;
+    return encoded >> 1;
+  }
+
+  static intptr_t InstanceSize(intptr_t length, intptr_t cid) {
+    return cid == kOneByteStringCid ? OneByteString::InstanceSize(length)
+                                    : TwoByteString::InstanceSize(length);
+  }
+
+  explicit StringDeserializationCluster(bool is_canonical, bool is_root_unit)
+      : CanonicalSetDeserializationCluster(is_canonical,
+                                           is_root_unit,
+                                           "String") {}
+  ~StringDeserializationCluster() {}
+
+  void ReadAlloc(Deserializer* d) {
+    start_index_ = d->next_index();
+    PageSpace* old_space = d->heap()->old_space();
+    const intptr_t count = d->ReadUnsigned();
+    for (intptr_t i = 0; i < count; i++) {
+      const intptr_t encoded = d->ReadUnsigned();
+      intptr_t cid = 0;
+      const intptr_t length = DecodeLengthAndCid(encoded, &cid);
+      d->AssignRef(old_space->AllocateSnapshot(InstanceSize(length, cid)));
+    }
+    stop_index_ = d->next_index();
+    BuildCanonicalSetFromLayout(d);
+  }
+
+  void ReadFill(Deserializer* d, bool primary) {
+    for (intptr_t id = start_index_; id < stop_index_; id++) {
+      StringPtr str = static_cast<StringPtr>(d->Ref(id));
+      const intptr_t encoded = d->ReadUnsigned();
+      intptr_t cid = 0;
+      const intptr_t length = DecodeLengthAndCid(encoded, &cid);
+      Deserializer::InitializeHeader(str, cid, InstanceSize(length, cid),
+                                     primary && is_canonical());
+      str->untag()->length_ = Smi::New(length);
+      StringHasher hasher;
+      if (cid == kOneByteStringCid) {
+        for (intptr_t j = 0; j < length; j++) {
+          uint8_t code_unit = d->Read<uint8_t>();
+          static_cast<OneByteStringPtr>(str)->untag()->data()[j] = code_unit;
+          hasher.Add(code_unit);
+        }
+      } else {
+        for (intptr_t j = 0; j < length; j++) {
+          uint16_t code_unit = d->Read<uint8_t>();
+          code_unit = code_unit | (d->Read<uint8_t>() << 8);
+          static_cast<TwoByteStringPtr>(str)->untag()->data()[j] = code_unit;
+          hasher.Add(code_unit);
         }
       }
-      isolate_group->object_store()->set_symbol_table(table.Release());
-    }
-  }
-#endif
-};
-
-class OneByteStringDeserializationCluster
-    : public StringDeserializationCluster {
- public:
-  explicit OneByteStringDeserializationCluster(bool is_canonical)
-      : StringDeserializationCluster("OneByteString", is_canonical) {}
-  ~OneByteStringDeserializationCluster() {}
-
-  void ReadAlloc(Deserializer* d) {
-    start_index_ = d->next_index();
-    PageSpace* old_space = d->heap()->old_space();
-    const intptr_t count = d->ReadUnsigned();
-    for (intptr_t i = 0; i < count; i++) {
-      const intptr_t length = d->ReadUnsigned();
-      d->AssignRef(
-          old_space->AllocateSnapshot(OneByteString::InstanceSize(length)));
-    }
-    stop_index_ = d->next_index();
-  }
-
-  void ReadFill(Deserializer* d, bool primary) {
-    for (intptr_t id = start_index_; id < stop_index_; id++) {
-      OneByteStringPtr str = static_cast<OneByteStringPtr>(d->Ref(id));
-      const intptr_t length = d->ReadUnsigned();
-      Deserializer::InitializeHeader(str, kOneByteStringCid,
-                                     OneByteString::InstanceSize(length),
-                                     primary && is_canonical());
-      str->untag()->length_ = Smi::New(length);
-      StringHasher hasher;
-      for (intptr_t j = 0; j < length; j++) {
-        uint8_t code_unit = d->Read<uint8_t>();
-        str->untag()->data()[j] = code_unit;
-        hasher.Add(code_unit);
-      }
       String::SetCachedHash(str, hasher.Finalize());
     }
   }
-};
 
-#if !defined(DART_PRECOMPILED_RUNTIME)
-class TwoByteStringSerializationCluster : public SerializationCluster {
- public:
-  explicit TwoByteStringSerializationCluster(bool is_canonical)
-      : SerializationCluster("TwoByteString",
-                             kTwoByteStringCid,
-                             kSizeVaries,
-                             is_canonical) {}
-  ~TwoByteStringSerializationCluster() {}
-
-  void Trace(Serializer* s, ObjectPtr object) {
-    TwoByteStringPtr str = static_cast<TwoByteStringPtr>(object);
-    objects_.Add(str);
-  }
-
-  void WriteAlloc(Serializer* s) {
-    const intptr_t count = objects_.length();
-    s->WriteUnsigned(count);
-    for (intptr_t i = 0; i < count; i++) {
-      TwoByteStringPtr str = objects_[i];
-      s->AssignRef(str);
-      AutoTraceObject(str);
-      const intptr_t length = Smi::Value(str->untag()->length());
-      s->WriteUnsigned(length);
-      target_memory_size_ +=
-          compiler::target::TwoByteString::InstanceSize(length);
-    }
-  }
-
-  void WriteFill(Serializer* s) {
-    const intptr_t count = objects_.length();
-    for (intptr_t i = 0; i < count; i++) {
-      TwoByteStringPtr str = objects_[i];
-      AutoTraceObject(str);
-      const intptr_t length = Smi::Value(str->untag()->length());
-      ASSERT(length <= (compiler::target::kSmiMax / 2));
-      s->WriteUnsigned(length);
-      s->WriteBytes(reinterpret_cast<uint8_t*>(str->untag()->data()),
-                    length * 2);
-    }
-  }
-
- private:
-  GrowableArray<TwoByteStringPtr> objects_;
-};
-#endif  // !DART_PRECOMPILED_RUNTIME
-
-class TwoByteStringDeserializationCluster
-    : public StringDeserializationCluster {
- public:
-  explicit TwoByteStringDeserializationCluster(bool is_canonical)
-      : StringDeserializationCluster("TwoByteString", is_canonical) {}
-  ~TwoByteStringDeserializationCluster() {}
-
-  void ReadAlloc(Deserializer* d) {
-    start_index_ = d->next_index();
-    PageSpace* old_space = d->heap()->old_space();
-    const intptr_t count = d->ReadUnsigned();
-    for (intptr_t i = 0; i < count; i++) {
-      const intptr_t length = d->ReadUnsigned();
-      d->AssignRef(
-          old_space->AllocateSnapshot(TwoByteString::InstanceSize(length)));
-    }
-    stop_index_ = d->next_index();
-  }
-
-  void ReadFill(Deserializer* d, bool primary) {
-    for (intptr_t id = start_index_; id < stop_index_; id++) {
-      TwoByteStringPtr str = static_cast<TwoByteStringPtr>(d->Ref(id));
-      const intptr_t length = d->ReadUnsigned();
-      Deserializer::InitializeHeader(str, kTwoByteStringCid,
-                                     TwoByteString::InstanceSize(length),
-                                     primary && is_canonical());
-      str->untag()->length_ = Smi::New(length);
-      StringHasher hasher;
-      for (intptr_t j = 0; j < length; j++) {
-        uint16_t code_unit = d->Read<uint8_t>();
-        code_unit = code_unit | (d->Read<uint8_t>() << 8);
-        str->untag()->data()[j] = code_unit;
-        hasher.Add(code_unit);
+  void PostLoad(Deserializer* d, const Array& refs, bool primary) {
+    if (!table_.IsNull()) {
+      auto object_store = d->isolate_group()->object_store();
+      VerifyCanonicalSet(d, refs, Array::Handle(object_store->symbol_table()));
+      object_store->set_symbol_table(table_);
+      if (d->isolate_group() == Dart::vm_isolate_group()) {
+        Symbols::InitFromSnapshot(d->isolate_group());
       }
-      String::SetCachedHash(str, hasher.Finalize());
     }
   }
 };
@@ -5612,16 +5555,8 @@
         saved_canonical_type_arguments_(Array::Handle()),
         saved_canonical_type_parameters_(Array::Handle()) {
     saved_symbol_table_ = object_store->symbol_table();
-    if (Snapshot::IncludesStringsInROData(snapshot_kind)) {
-      object_store->set_symbol_table(
-          Array::Handle(HashTables::New<CanonicalStringSet>(4)));
-    } else {
-#if defined(DART_PRECOMPILER)
-      if (FLAG_precompiled_mode) {
-        HashTables::Weaken(saved_symbol_table_);
-      }
-#endif
-    }
+    object_store->set_symbol_table(
+        Array::Handle(HashTables::New<CanonicalStringSet>(4)));
     saved_canonical_types_ = object_store->canonical_types();
     object_store->set_canonical_types(
         Array::Handle(HashTables::New<CanonicalTypeSet>(4)));
@@ -6331,8 +6266,9 @@
     case kCompressedStackMapsCid:
       return "CompressedStackMaps";
     case kStringCid:
-      RELEASE_ASSERT(current_loading_unit_id_ <= LoadingUnit::kRootId);
-      return "CanonicalString";
+      return current_loading_unit_id_ <= LoadingUnit::kRootId
+                 ? "CanonicalString"
+                 : nullptr;
     case kOneByteStringCid:
       return current_loading_unit_id_ <= LoadingUnit::kRootId
                  ? "OneByteStringCid"
@@ -6480,10 +6416,9 @@
     case kImmutableArrayCid:
       return new (Z)
           ArraySerializationCluster(is_canonical, kImmutableArrayCid);
-    case kOneByteStringCid:
-      return new (Z) OneByteStringSerializationCluster(is_canonical);
-    case kTwoByteStringCid:
-      return new (Z) TwoByteStringSerializationCluster(is_canonical);
+    case kStringCid:
+      return new (Z) StringSerializationCluster(
+          is_canonical, cluster_represents_canonical_set && !vm_);
     case kWeakSerializationReferenceCid:
 #if defined(DART_PRECOMPILER)
       ASSERT(kind_ == Snapshot::kFullAOT);
@@ -6690,9 +6625,7 @@
     cid = object->GetClassId();
     is_canonical = object->untag()->IsCanonical();
   }
-  if (Snapshot::IncludesStringsInROData(kind_) && is_canonical &&
-      IsStringClassId(cid) &&
-      current_loading_unit_id_ <= LoadingUnit::kRootId) {
+  if (IsStringClassId(cid)) {
     cid = kStringCid;
   }
 
@@ -7257,15 +7190,12 @@
             RODataDeserializationCluster(is_canonical, !is_non_root_unit_, cid);
       case kOneByteStringCid:
       case kTwoByteStringCid:
+      case kStringCid:
         if (!is_non_root_unit_) {
           return new (Z) RODataDeserializationCluster(is_canonical,
                                                       !is_non_root_unit_, cid);
         }
         break;
-      case kStringCid:
-        RELEASE_ASSERT(!is_non_root_unit_);
-        return new (Z)
-            RODataDeserializationCluster(is_canonical, !is_non_root_unit_, cid);
     }
   }
 #endif
@@ -7397,10 +7327,10 @@
     case kImmutableArrayCid:
       return new (Z)
           ArrayDeserializationCluster(is_canonical, kImmutableArrayCid);
-    case kOneByteStringCid:
-      return new (Z) OneByteStringDeserializationCluster(is_canonical);
-    case kTwoByteStringCid:
-      return new (Z) TwoByteStringDeserializationCluster(is_canonical);
+    case kStringCid:
+      return new (Z) StringDeserializationCluster(
+          is_canonical,
+          !is_non_root_unit_ && isolate_group() != Dart::vm_isolate_group());
     default:
       break;
   }
diff --git a/runtime/vm/compiler/backend/il.h b/runtime/vm/compiler/backend/il.h
index 7699857..40658e5 100644
--- a/runtime/vm/compiler/backend/il.h
+++ b/runtime/vm/compiler/backend/il.h
@@ -6171,6 +6171,8 @@
       : AllocationInstr(source, deopt_id),
         cls_(cls),
         type_arguments_(type_arguments) {
+    ASSERT(cls.IsZoneHandle());
+    ASSERT(!cls.IsNull());
     ASSERT((cls.NumTypeArguments() > 0) == (type_arguments != nullptr));
     if (type_arguments != nullptr) {
       SetInputAt(kTypeArgumentsPos, type_arguments);
diff --git a/runtime/vm/compiler/backend/redundancy_elimination_test.cc b/runtime/vm/compiler/backend/redundancy_elimination_test.cc
index 2668f1d..23ffde4 100644
--- a/runtime/vm/compiler/backend/redundancy_elimination_test.cc
+++ b/runtime/vm/compiler/backend/redundancy_elimination_test.cc
@@ -219,7 +219,7 @@
   const Library& lib =
       Library::Handle(LoadTestScript(script_chars, NoopNativeLookup));
 
-  const Class& cls = Class::Handle(
+  const Class& cls = Class::ZoneHandle(
       lib.LookupLocalClass(String::Handle(Symbols::New(thread, "K"))));
   const Error& err = Error::Handle(cls.EnsureIsFinalized(thread));
   EXPECT(err.IsNull());
@@ -383,7 +383,7 @@
   const Library& lib =
       Library::Handle(LoadTestScript(script_chars, NoopNativeLookup));
 
-  const Class& cls = Class::Handle(
+  const Class& cls = Class::ZoneHandle(
       lib.LookupLocalClass(String::Handle(Symbols::New(thread, "K"))));
   const Error& err = Error::Handle(cls.EnsureIsFinalized(thread));
   EXPECT(err.IsNull());
diff --git a/runtime/vm/compiler/frontend/kernel_to_il.cc b/runtime/vm/compiler/frontend/kernel_to_il.cc
index 68cdb57..f2d956f 100644
--- a/runtime/vm/compiler/frontend/kernel_to_il.cc
+++ b/runtime/vm/compiler/frontend/kernel_to_il.cc
@@ -3450,7 +3450,7 @@
 
   // Push receiver.
   if (target.IsGenerativeConstructor()) {
-    const Class& cls = Class::Handle(Z, target.Owner());
+    const Class& cls = Class::ZoneHandle(Z, target.Owner());
     if (cls.NumTypeArguments() > 0) {
       if (!function.IsGeneric()) {
         Type& cls_type = Type::Handle(Z, cls.DeclarationType());
diff --git a/runtime/vm/datastream.h b/runtime/vm/datastream.h
index 7da4532..c0c642f 100644
--- a/runtime/vm/datastream.h
+++ b/runtime/vm/datastream.h
@@ -85,7 +85,7 @@
   };
 
   // Reads 'len' bytes from the stream.
-  void ReadBytes(uint8_t* addr, intptr_t len) {
+  void ReadBytes(void* addr, intptr_t len) {
     ASSERT((end_ - current_) >= len);
     if (len != 0) {
       memmove(addr, current_, len);
diff --git a/runtime/vm/exceptions.cc b/runtime/vm/exceptions.cc
index db937ad..2ea7550 100644
--- a/runtime/vm/exceptions.cc
+++ b/runtime/vm/exceptions.cc
@@ -1139,8 +1139,6 @@
       break;
   }
 
-  Thread* thread = Thread::Current();
-  NoReloadScope no_reload_scope(thread);
   return DartLibraryCalls::InstanceCreate(library, *class_name,
                                           *constructor_name, arguments);
 }
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index b1a7315..3ef9c62 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -2493,6 +2493,25 @@
     ServiceIsolate::SendIsolateShutdownMessage();
 #if !defined(PRODUCT)
     debugger()->Shutdown();
+    // Cleanup profiler state.
+    SampleBlock* cpu_block = current_sample_block();
+    if (cpu_block != nullptr) {
+      cpu_block->release_block();
+    }
+    SampleBlock* allocation_block = current_allocation_sample_block();
+    if (allocation_block != nullptr) {
+      allocation_block->release_block();
+    }
+
+    // Process the previously assigned sample blocks if we're using the
+    // profiler's sample buffer. Some tests create their own SampleBlockBuffer
+    // and handle block processing themselves.
+    if ((cpu_block != nullptr || allocation_block != nullptr) &&
+        Profiler::sample_block_buffer() != nullptr) {
+      StackZone zone(thread);
+      HandleScope handle_scope(thread);
+      Profiler::sample_block_buffer()->ProcessCompletedBlocks();
+    }
 #endif
   }
 
@@ -2551,26 +2570,6 @@
   // requests anymore.
   Thread::ExitIsolate();
 
-#if !defined(PRODUCT)
-  // Cleanup profiler state.
-  SampleBlock* cpu_block = isolate->current_sample_block();
-  if (cpu_block != nullptr) {
-    cpu_block->release_block();
-  }
-  SampleBlock* allocation_block = isolate->current_allocation_sample_block();
-  if (allocation_block != nullptr) {
-    allocation_block->release_block();
-  }
-
-  // Process the previously assigned sample blocks if we're using the
-  // profiler's sample buffer. Some tests create their own SampleBlockBuffer
-  // and handle block processing themselves.
-  if ((cpu_block != nullptr || allocation_block != nullptr) &&
-      Profiler::sample_block_buffer() != nullptr) {
-    Profiler::sample_block_buffer()->ProcessCompletedBlocks();
-  }
-#endif  // !defined(PRODUCT)
-
   // Now it's safe to delete the isolate.
   delete isolate;
 
diff --git a/runtime/vm/message_snapshot.cc b/runtime/vm/message_snapshot.cc
index 37bb767..3eca655 100644
--- a/runtime/vm/message_snapshot.cc
+++ b/runtime/vm/message_snapshot.cc
@@ -145,7 +145,7 @@
   void WriteWordWith32BitWrites(uword value) {
     stream_.WriteWordWith32BitWrites(value);
   }
-  void WriteBytes(const uint8_t* addr, intptr_t len) {
+  void WriteBytes(const void* addr, intptr_t len) {
     stream_.WriteBytes(addr, len);
   }
   void WriteAscii(const String& str) {
@@ -344,7 +344,7 @@
   }
   intptr_t ReadUnsigned() { return stream_.ReadUnsigned(); }
   uword ReadWordWith32BitReads() { return stream_.ReadWordWith32BitReads(); }
-  void ReadBytes(uint8_t* addr, intptr_t len) { stream_.ReadBytes(addr, len); }
+  void ReadBytes(void* addr, intptr_t len) { stream_.ReadBytes(addr, len); }
   const char* ReadAscii() {
     intptr_t len = ReadUnsigned();
     const char* result = reinterpret_cast<const char*>(CurrentBufferAddress());
@@ -1572,8 +1572,7 @@
       d->AssignRef(data.ptr());
       const intptr_t length_in_bytes = length * element_size;
       NoSafepointScope no_safepoint;
-      uint8_t* cdata = reinterpret_cast<uint8_t*>(data.untag()->data());
-      d->ReadBytes(cdata, length_in_bytes);
+      d->ReadBytes(data.untag()->data(), length_in_bytes);
     }
   }
 
@@ -2066,6 +2065,60 @@
   }
 };
 
+class Simd128MessageSerializationCluster : public MessageSerializationCluster {
+ public:
+  explicit Simd128MessageSerializationCluster(intptr_t cid)
+      : MessageSerializationCluster("Simd128",
+                                    MessagePhase::kBeforeTypes,
+                                    cid) {}
+  ~Simd128MessageSerializationCluster() {}
+
+  void Trace(MessageSerializer* s, Object* object) { objects_.Add(object); }
+
+  void WriteNodes(MessageSerializer* s) {
+    intptr_t count = objects_.length();
+    s->WriteUnsigned(count);
+    for (intptr_t i = 0; i < count; i++) {
+      Object* vector = objects_[i];
+      s->AssignRef(vector);
+      ASSERT_EQUAL(Int32x4::value_offset(), Float32x4::value_offset());
+      ASSERT_EQUAL(Int32x4::value_offset(), Float64x2::value_offset());
+      s->WriteBytes(&(static_cast<Int32x4Ptr>(vector->ptr())->untag()->value_),
+                    sizeof(simd128_value_t));
+    }
+  }
+
+ private:
+  GrowableArray<Object*> objects_;
+};
+
+class Simd128MessageDeserializationCluster
+    : public MessageDeserializationCluster {
+ public:
+  explicit Simd128MessageDeserializationCluster(intptr_t cid)
+      : MessageDeserializationCluster("Simd128"), cid_(cid) {}
+  ~Simd128MessageDeserializationCluster() {}
+
+  void ReadNodes(MessageDeserializer* d) {
+    intptr_t count = d->ReadUnsigned();
+    for (intptr_t i = 0; i < count; i++) {
+      ASSERT_EQUAL(Int32x4::InstanceSize(), Float32x4::InstanceSize());
+      ASSERT_EQUAL(Int32x4::InstanceSize(), Float64x2::InstanceSize());
+      ObjectPtr vector =
+          Object::Allocate(cid_, Int32x4::InstanceSize(), Heap::kNew,
+                           Int32x4::ContainsCompressedPointers());
+      d->AssignRef(vector);
+      ASSERT_EQUAL(Int32x4::value_offset(), Float32x4::value_offset());
+      ASSERT_EQUAL(Int32x4::value_offset(), Float64x2::value_offset());
+      d->ReadBytes(&(static_cast<Int32x4Ptr>(vector)->untag()->value_),
+                   sizeof(simd128_value_t));
+    }
+  }
+
+ private:
+  const intptr_t cid_;
+};
+
 class RegExpMessageSerializationCluster : public MessageSerializationCluster {
  public:
   RegExpMessageSerializationCluster()
@@ -3233,6 +3286,10 @@
       return new (Z) OneByteStringMessageSerializationCluster(Z, is_canonical);
     case kTwoByteStringCid:
       return new (Z) TwoByteStringMessageSerializationCluster(Z, is_canonical);
+    case kInt32x4Cid:
+    case kFloat32x4Cid:
+    case kFloat64x2Cid:
+      return new (Z) Simd128MessageSerializationCluster(cid);
     default:
       break;
   }
@@ -3321,6 +3378,11 @@
       return new (Z) OneByteStringMessageDeserializationCluster(is_canonical);
     case kTwoByteStringCid:
       return new (Z) TwoByteStringMessageDeserializationCluster(is_canonical);
+    case kInt32x4Cid:
+    case kFloat32x4Cid:
+    case kFloat64x2Cid:
+      ASSERT(!is_canonical);
+      return new (Z) Simd128MessageDeserializationCluster(cid);
     default:
       break;
   }
diff --git a/runtime/vm/object.h b/runtime/vm/object.h
index eb0d9e3..ce45815 100644
--- a/runtime/vm/object.h
+++ b/runtime/vm/object.h
@@ -859,6 +859,7 @@
   friend void UntaggedObject::Validate(IsolateGroup* isolate_group) const;
   friend class Closure;
   friend class InstanceDeserializationCluster;
+  friend class Simd128MessageDeserializationCluster;
   friend class OneByteString;
   friend class TwoByteString;
   friend class ExternalOneByteString;
diff --git a/runtime/vm/profiler.cc b/runtime/vm/profiler.cc
index 2436574..cdd8783 100644
--- a/runtime/vm/profiler.cc
+++ b/runtime/vm/profiler.cc
@@ -226,7 +226,9 @@
   int64_t start = Dart_TimelineGetMicros();
   for (intptr_t i = 0; i < capacity_; ++i) {
     SampleBlock* block = &blocks_[i];
-    if (block->is_full() && !block->evictable()) {
+    // Only evict blocks owned by the current thread.
+    if (block->owner() == thread->isolate() && block->is_full() &&
+        !block->evictable()) {
       if (Service::profiler_stream.enabled()) {
         Profile profile(block->owner());
         profile.Build(thread, nullptr, block);
@@ -330,8 +332,12 @@
     isolate->set_current_sample_block(next);
   }
   next->set_is_allocation_block(allocation_sample);
-  can_process_block_.store(true);
-  isolate->mutator_thread()->ScheduleInterrupts(Thread::kVMInterrupt);
+  bool scheduled = can_process_block_.exchange(true);
+  // We don't process samples on the kernel isolate.
+  if (!isolate->is_kernel_isolate() && !isolate->is_service_isolate() &&
+      !scheduled) {
+    isolate->mutator_thread()->ScheduleInterrupts(Thread::kVMInterrupt);
+  }
   return ReserveSampleImpl(isolate, allocation_sample);
 }
 
diff --git a/runtime/vm/raw_object.h b/runtime/vm/raw_object.h
index 3d442b2..e50a979 100644
--- a/runtime/vm/raw_object.h
+++ b/runtime/vm/raw_object.h
@@ -2725,10 +2725,6 @@
 
  private:
   friend class Library;
-  friend class OneByteStringSerializationCluster;
-  friend class TwoByteStringSerializationCluster;
-  friend class OneByteStringDeserializationCluster;
-  friend class TwoByteStringDeserializationCluster;
   friend class RODataSerializationCluster;
   friend class ImageWriter;
 };
@@ -2743,6 +2739,8 @@
 
   friend class RODataSerializationCluster;
   friend class String;
+  friend class StringDeserializationCluster;
+  friend class StringSerializationCluster;
 };
 
 class UntaggedTwoByteString : public UntaggedString {
@@ -2755,6 +2753,8 @@
 
   friend class RODataSerializationCluster;
   friend class String;
+  friend class StringDeserializationCluster;
+  friend class StringSerializationCluster;
 };
 
 // Abstract base class for RawTypedData/RawExternalTypedData/RawTypedDataView/
@@ -3024,6 +3024,8 @@
 
   ALIGN8 int32_t value_[4];
 
+  friend class Simd128MessageSerializationCluster;
+  friend class Simd128MessageDeserializationCluster;
 
  public:
   int32_t x() const { return value_[0]; }
diff --git a/runtime/vm/thread.cc b/runtime/vm/thread.cc
index 09a3b25..74217b6 100644
--- a/runtime/vm/thread.cc
+++ b/runtime/vm/thread.cc
@@ -450,11 +450,15 @@
     }
 
 #if !defined(PRODUCT)
-    // Processes completed SampleBlocks and sends CPU sample events over the
-    // service protocol when applicable.
-    SampleBlockBuffer* sample_buffer = Profiler::sample_block_buffer();
-    if (sample_buffer != nullptr && sample_buffer->process_blocks()) {
-      sample_buffer->ProcessCompletedBlocks();
+    // Don't block the kernel isolate to process CPU samples as we can
+    // potentially deadlock when trying to compile source for the main isolate.
+    if (!isolate()->is_kernel_isolate() && !isolate()->is_service_isolate()) {
+      // Processes completed SampleBlocks and sends CPU sample events over the
+      // service protocol when applicable.
+      SampleBlockBuffer* sample_buffer = Profiler::sample_block_buffer();
+      if (sample_buffer != nullptr && sample_buffer->process_blocks()) {
+        sample_buffer->ProcessCompletedBlocks();
+      }
     }
 #endif  // !defined(PRODUCT)
   }
diff --git a/sdk/lib/_internal/js_runtime/lib/dart2js_runtime_metrics.dart b/sdk/lib/_internal/js_runtime/lib/dart2js_runtime_metrics.dart
new file mode 100644
index 0000000..ab1d879
--- /dev/null
+++ b/sdk/lib/_internal/js_runtime/lib/dart2js_runtime_metrics.dart
@@ -0,0 +1,32 @@
+// 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.
+
+library dart2js_runtime_metrics;
+
+import 'dart:_js_helper' show fillLiteralMap, rawStartupMetrics;
+
+/// A collection of metrics for events that happen before `main()` is entered.
+///
+/// The contents of the map depend on the platform. The map values are simple
+/// objects (strings, numbers, Booleans). There is always an entry for the key
+/// `'runtime'` with a [String] value.
+///
+/// This implementation for dart2js has the content (subject to change):
+///
+/// - `runtime`: `'dart2js'`
+///
+/// - `firstMs`:  first performance.now() reading in the main.dart.js file.
+///
+/// - `dartMainProgramMs`: performance.now() immediately inside the function
+///    wrapping all the Dart code
+///
+/// - `callMainMs`: performance.now() just before calling main()
+///
+/// The injected code uses `Date.now()` if `performance.now()` is not defined.
+Map<String, Object> get startupMetrics {
+  final Map<String, Object> result = {'runtime': 'dart2js'};
+  final raw = rawStartupMetrics();
+  fillLiteralMap(raw, result);
+  return result;
+}
diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
index fa79c12..b39faed 100644
--- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart
+++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart
@@ -21,6 +21,7 @@
         JsGetName,
         LEAF_TAGS,
         NATIVE_SUPERCLASS_TAG_NAME,
+        STARTUP_METRICS,
         STATIC_FUNCTION_NAME_PROPERTY_NAME,
         TearOffParametersPropertyNames;
 
@@ -3005,3 +3006,7 @@
   assert(args.every((arg) => arg is! Function || isJSFunction(arg)),
       'Dart function requires `allowInterop` to be passed to JavaScript.');
 }
+
+Object? rawStartupMetrics() {
+  return JS('JSArray', '#.a', JS_EMBEDDED_GLOBAL('', STARTUP_METRICS));
+}
diff --git a/sdk/lib/_internal/js_runtime/lib/shared/embedded_names.dart b/sdk/lib/_internal/js_runtime/lib/shared/embedded_names.dart
index ec45847..0334810 100644
--- a/sdk/lib/_internal/js_runtime/lib/shared/embedded_names.dart
+++ b/sdk/lib/_internal/js_runtime/lib/shared/embedded_names.dart
@@ -179,6 +179,9 @@
 /// This embedded global is used for --experiment-new-rti.
 const RTI_UNIVERSE = 'typeUniverse';
 
+/// An embedded global used to collect and access startup metrics.
+const STARTUP_METRICS = 'sm';
+
 /// Names that are supported by [JS_GET_NAME].
 // TODO(herhut): Make entries lower case (as in fields) and find a better name.
 enum JsGetName {
diff --git a/sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart b/sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart
index 8e56fe9..b0482a8 100644
--- a/sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart
+++ b/sdk/lib/_internal/sdk_library_metadata/lib/libraries.dart
@@ -153,6 +153,11 @@
       categories: "", documented: false, platforms: DART2JS_PLATFORM),
   "_rti": const LibraryInfo("_internal/js_runtime/lib/rti.dart",
       categories: "", documented: false, platforms: DART2JS_PLATFORM),
+  "_dart2js_runtime_metrics": const LibraryInfo(
+      "_internal/js_runtime/lib/dart2js_runtime_metrics.dart",
+      categories: "",
+      documented: false,
+      platforms: DART2JS_PLATFORM),
   "_interceptors": const LibraryInfo(
       "_internal/js_runtime/lib/interceptors.dart",
       categories: "",
diff --git a/sdk/lib/collection/queue.dart b/sdk/lib/collection/queue.dart
index fb48484..fd83b42 100644
--- a/sdk/lib/collection/queue.dart
+++ b/sdk/lib/collection/queue.dart
@@ -53,8 +53,7 @@
   /// of [S],
   /// then the returned queue can be used as a `Queue<T>`.
   ///
-  /// Methods like [contains] and [remove]
-  /// which accept one `Object?` as argument,
+  /// Methods which accept `Object?` as argument, like [contains] and [remove],
   /// will pass the argument directly to the this queue's method
   /// without any checks.
   static Queue<T> castFrom<S, T>(Queue<S> source) => CastQueue<S, T>(source);
@@ -70,8 +69,7 @@
   /// and they must be instances of [E] as well to be accepted by
   /// this queue as well.
   ///
-  /// Methods like [contains] and [remove]
-  /// which accept one `Object?` as argument,
+  /// Methods which accept `Object?` as argument, like [contains] and [remove],
   /// will pass the argument directly to the this queue's method
   /// without any checks.
   /// That means that you can do `queueOfStrings.cast<int>().remove("a")`
diff --git a/sdk/lib/core/list.dart b/sdk/lib/core/list.dart
index 6c17632..585845a 100644
--- a/sdk/lib/core/list.dart
+++ b/sdk/lib/core/list.dart
@@ -188,8 +188,7 @@
   /// of [S],
   /// then the returned list can be used as a `List<T>`.
   ///
-  /// Methods like [contains] and [remove]
-  /// which accept `Object?` as argument
+  /// Methods which accept `Object?` as argument, like [contains] and [remove],
   /// will pass the argument directly to the this list's method
   /// without any checks.
   static List<T> castFrom<S, T>(List<S> source) => CastList<S, T>(source);
@@ -267,8 +266,7 @@
   /// and they must be instances of [E] as well to be accepted by
   /// this list as well.
   ///
-  /// Methods like [contains] and [remove]
-  /// which accept `Object?` as argument
+  /// Methods which accept `Object?` as argument, like [contains] and [remove],
   /// will pass the argument directly to the this list's method
   /// without any checks.
   /// That means that you can do `listOfStrings.cast<int>().remove("a")`
diff --git a/sdk/lib/core/map.dart b/sdk/lib/core/map.dart
index f6d36b1..164b6f2 100644
--- a/sdk/lib/core/map.dart
+++ b/sdk/lib/core/map.dart
@@ -164,8 +164,8 @@
   /// and if all entries added to the returned map have [K] keys and [V]] values,
   /// then the returned map can be used as a `Map<K2, V2>`.
   ///
-  /// Methods like [containsKey], [remove] and [operator[]]
-  /// which accept `Object?` as argument
+  /// Methods which accept `Object?` as argument,
+  /// like [containsKey], [remove] and [operator []],
   /// will pass the argument directly to the this map's method
   /// without any checks.
   static Map<K2, V2> castFrom<K, V, K2, V2>(Map<K, V> source) =>
@@ -199,8 +199,8 @@
   /// Entries added to the map must be valid for both a `Map<K, V>` and a
   /// `Map<RK, RV>`.
   ///
-  /// Methods like [containsKey], [remove] and [operator[]]
-  /// which accept `Object?` as argument
+  /// Methods which accept `Object?` as argument,
+  /// like [containsKey], [remove] and [operator []],
   /// will pass the argument directly to the this map's method
   /// without any checks.
   /// That means that you can do `mapWithStringKeys.cast<int,int>().remove("a")`
diff --git a/sdk/lib/core/set.dart b/sdk/lib/core/set.dart
index b19ed8e..d94a58f 100644
--- a/sdk/lib/core/set.dart
+++ b/sdk/lib/core/set.dart
@@ -107,8 +107,8 @@
   /// of [S],
   /// then the returned set can be used as a `Set<T>`.
   ///
-  /// Methods like [contains], [remove] and [removeAll]
-  /// which accept one or more `Object?` as argument,
+  /// Methods which accept one or more `Object?` as argument,
+  /// like [contains], [remove] and [removeAll],
   /// will pass the argument directly to the this set's method
   /// without any checks.
   static Set<T> castFrom<S, T>(Set<S> source, {Set<R> Function<R>()? newSet}) =>
@@ -125,8 +125,8 @@
   /// and they must be instances of [E] as well to be accepted by
   /// this set as well.
   ///
-  /// Methods like [contains], [remove] and [removeAll]
-  /// which accept one or more `Object?` as argument,
+  /// Methods which accept one or more `Object?` as argument,
+  /// like [contains], [remove] and [removeAll],
   /// will pass the argument directly to the this set's method
   /// without any checks.
   /// That means that you can do `setOfStrings.cast<int>().remove("a")`
diff --git a/sdk/lib/core/string.dart b/sdk/lib/core/string.dart
index 7c31513..fbe6a4b 100644
--- a/sdk/lib/core/string.dart
+++ b/sdk/lib/core/string.dart
@@ -314,6 +314,9 @@
   /// string.substring(1);    // 'artlang'
   /// string.substring(1, 4); // 'art'
   /// ```
+  ///
+  /// Both [start] and [end] must be non-negative and no greater than [length], and
+  /// [end], if provided, must be greater than or equal to [start].
   String substring(int start, [int? end]);
 
   /// The string without any leading and trailing whitespace.
diff --git a/sdk/lib/libraries.json b/sdk/lib/libraries.json
index a595503..c1cf9c2 100644
--- a/sdk/lib/libraries.json
+++ b/sdk/lib/libraries.json
@@ -235,6 +235,9 @@
       "web_sql": {
         "uri": "web_sql/dart2js/web_sql_dart2js.dart"
       },
+      "_dart2js_runtime_metrics": {
+        "uri": "_internal/js_runtime/lib/dart2js_runtime_metrics.dart"
+      },
       "_internal": {
         "uri": "internal/internal.dart",
         "patches": "_internal/js_runtime/lib/internal_patch.dart"
@@ -338,6 +341,9 @@
         "uri": "internal/internal.dart",
         "patches": "_internal/js_runtime/lib/internal_patch.dart"
       },
+      "_dart2js_runtime_metrics": {
+        "uri": "_internal/js_runtime/lib/dart2js_runtime_metrics.dart"
+      },
       "_js_helper": {
         "uri": "_internal/js_runtime/lib/js_helper.dart"
       },
@@ -477,4 +483,4 @@
       }
     }
   }
-}
+}
\ No newline at end of file
diff --git a/sdk/lib/libraries.yaml b/sdk/lib/libraries.yaml
index 9a1a1d8..1b92562 100644
--- a/sdk/lib/libraries.yaml
+++ b/sdk/lib/libraries.yaml
@@ -233,6 +233,9 @@
     web_sql:
       uri: "web_sql/dart2js/web_sql_dart2js.dart"
 
+    _dart2js_runtime_metrics:
+      uri: "_internal/js_runtime/lib/dart2js_runtime_metrics.dart"
+
     _internal:
       uri: "internal/internal.dart"
       patches: "_internal/js_runtime/lib/internal_patch.dart"
@@ -334,6 +337,9 @@
       uri: "internal/internal.dart"
       patches: "_internal/js_runtime/lib/internal_patch.dart"
 
+    _dart2js_runtime_metrics:
+      uri: "_internal/js_runtime/lib/dart2js_runtime_metrics.dart"
+
     _js_helper:
       uri: "_internal/js_runtime/lib/js_helper.dart"
 
diff --git a/tests/lib/isolate/simd_vectors_message_test.dart b/tests/lib/isolate/simd_vectors_message_test.dart
new file mode 100644
index 0000000..324d4ef
--- /dev/null
+++ b/tests/lib/isolate/simd_vectors_message_test.dart
@@ -0,0 +1,48 @@
+// 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.
+
+// See https://github.com/dart-lang/sdk/issues/46793
+
+import "dart:async";
+import "dart:isolate";
+import "dart:typed_data";
+
+import "package:async_helper/async_helper.dart";
+import "package:expect/expect.dart";
+
+main() {
+  asyncStart();
+  var port;
+  port = new RawReceivePort((message) {
+    print("Receive $message");
+
+    var int32x4 = message[0] as Int32x4;
+    Expect.equals(-1, int32x4.x);
+    Expect.equals(0, int32x4.y);
+    Expect.equals(1, int32x4.z);
+    Expect.equals(2, int32x4.w);
+
+    var float32x4 = message[1] as Float32x4;
+    Expect.equals(-2.5, float32x4.x);
+    Expect.equals(0.0, float32x4.y);
+    Expect.equals(1.25, float32x4.z);
+    Expect.equals(2.125, float32x4.w);
+
+    var float64x2 = message[2] as Float64x2;
+    Expect.equals(16.5, float64x2.x);
+    Expect.equals(-32.25, float64x2.y);
+
+    port.close();
+    asyncEnd();
+  });
+
+
+  var list = [
+    new Int32x4(-1, 0, 1, 2),
+    new Float32x4(-2.5, 0.0, 1.25, 2.125),
+    new Float64x2(16.5, -32.25),
+  ];
+  print("Send $list");
+  port.sendPort.send(list);
+}
diff --git a/tests/lib_2/isolate/simd_vectors_message_test.dart b/tests/lib_2/isolate/simd_vectors_message_test.dart
new file mode 100644
index 0000000..fedd5a0
--- /dev/null
+++ b/tests/lib_2/isolate/simd_vectors_message_test.dart
@@ -0,0 +1,50 @@
+// 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.
+
+// See https://github.com/dart-lang/sdk/issues/46793
+
+// @dart = 2.9
+
+import "dart:async";
+import "dart:isolate";
+import "dart:typed_data";
+
+import "package:async_helper/async_helper.dart";
+import "package:expect/expect.dart";
+
+main() {
+  asyncStart();
+  var port;
+  port = new RawReceivePort((message) {
+    print("Receive $message");
+
+    var int32x4 = message[0] as Int32x4;
+    Expect.equals(-1, int32x4.x);
+    Expect.equals(0, int32x4.y);
+    Expect.equals(1, int32x4.z);
+    Expect.equals(2, int32x4.w);
+
+    var float32x4 = message[1] as Float32x4;
+    Expect.equals(-2.5, float32x4.x);
+    Expect.equals(0.0, float32x4.y);
+    Expect.equals(1.25, float32x4.z);
+    Expect.equals(2.125, float32x4.w);
+
+    var float64x2 = message[2] as Float64x2;
+    Expect.equals(16.5, float64x2.x);
+    Expect.equals(-32.25, float64x2.y);
+
+    port.close();
+    asyncEnd();
+  });
+
+
+  var list = [
+    new Int32x4(-1, 0, 1, 2),
+    new Float32x4(-2.5, 0.0, 1.25, 2.125),
+    new Float64x2(16.5, -32.25),
+  ];
+  print("Send $list");
+  port.sendPort.send(list);
+}
diff --git a/tools/VERSION b/tools/VERSION
index 42c0eae..dc0eb52 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 377
+PRERELEASE 378
 PRERELEASE_PATCH 0
\ No newline at end of file