Version 2.14.0-359.0.dev

Merge commit 'f424f3a4cca306513e77c7747682f1c1c99e3307' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index 13301b6..b225f74 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-26T14:57:34.624319",
+  "generated": "2021-07-27T19:27:52.638315",
   "generator": "tools/generate_package_config.dart",
   "packages": [
     {
@@ -244,7 +244,7 @@
       "name": "dds",
       "rootUri": "../pkg/dds",
       "packageUri": "lib/",
-      "languageVersion": "2.12"
+      "languageVersion": "2.14"
     },
     {
       "name": "dev_compiler",
@@ -725,7 +725,7 @@
       "name": "testing",
       "rootUri": "../pkg/testing",
       "packageUri": "lib/",
-      "languageVersion": "2.0"
+      "languageVersion": "2.12"
     },
     {
       "name": "typed_data",
diff --git a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
index 69bbfeb..5be60d4 100644
--- a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
@@ -6218,6 +6218,18 @@
         message: r"""This is the enclosing class.""");
 
 // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Code<Null> codeJsInteropExternalExtensionMemberNotOnJSClass =
+    messageJsInteropExternalExtensionMemberNotOnJSClass;
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const MessageCode messageJsInteropExternalExtensionMemberNotOnJSClass =
+    const MessageCode("JsInteropExternalExtensionMemberNotOnJSClass",
+        message:
+            r"""JS interop class required for 'external' extension members.""",
+        tip:
+            r"""Try adding a JS interop annotation to the on type class of the extension.""");
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
 const Code<Null> codeJsInteropExternalMemberNotJSAnnotated =
     messageJsInteropExternalMemberNotJSAnnotated;
 
diff --git a/pkg/_js_interop_checks/lib/js_interop_checks.dart b/pkg/_js_interop_checks/lib/js_interop_checks.dart
index e4d4691..7567930 100644
--- a/pkg/_js_interop_checks/lib/js_interop_checks.dart
+++ b/pkg/_js_interop_checks/lib/js_interop_checks.dart
@@ -12,6 +12,7 @@
         messageJsInteropAnonymousFactoryPositionalParameters,
         messageJsInteropEnclosingClassJSAnnotation,
         messageJsInteropEnclosingClassJSAnnotationContext,
+        messageJsInteropExternalExtensionMemberNotOnJSClass,
         messageJsInteropExternalMemberNotJSAnnotated,
         messageJsInteropIndexNotSupported,
         messageJsInteropNamedParameters,
@@ -30,6 +31,7 @@
   bool _classHasJSAnnotation = false;
   bool _classHasAnonymousAnnotation = false;
   bool _libraryHasJSAnnotation = false;
+  Map<Reference, Extension>? _libraryExtensionsIndex;
 
   /// Libraries that use `external` to exclude from checks on external.
   static final Iterable<String> _pathsWithAllowedDartExternalUsage = <String>[
@@ -86,7 +88,8 @@
 
   @override
   void defaultMember(Member member) {
-    _checkJSInteropAnnotation(member);
+    _checkInstanceMemberJSAnnotation(member);
+    if (!_isJSInteropMember(member)) _checkDisallowedExternal(member);
     // TODO(43530): Disallow having JS interop annotations on non-external
     // members (class members or otherwise). Currently, they're being ignored.
     super.defaultMember(member);
@@ -165,11 +168,12 @@
     super.visitLibrary(lib);
     _libraryIsGlobalNamespace = false;
     _libraryHasJSAnnotation = false;
+    _libraryExtensionsIndex = null;
   }
 
   @override
   void visitProcedure(Procedure procedure) {
-    _checkJSInteropAnnotation(procedure);
+    _checkInstanceMemberJSAnnotation(procedure);
     if (_classHasJSAnnotation && !procedure.isExternal) {
       // If not one of few exceptions, member is not allowed to exclude
       // `external` inside of a JS interop class.
@@ -183,38 +187,45 @@
             procedure.fileUri);
       }
     }
-    if (!_isJSInteropMember(procedure)) return;
 
-    if (!procedure.isStatic &&
-        (procedure.name.text == '[]=' || procedure.name.text == '[]')) {
-      _diagnosticsReporter.report(messageJsInteropIndexNotSupported,
-          procedure.fileOffset, procedure.name.text.length, procedure.fileUri);
-    }
-
-    var isAnonymousFactory =
-        _classHasAnonymousAnnotation && procedure.isFactory;
-
-    if (isAnonymousFactory) {
-      // ignore: unnecessary_null_comparison
-      if (procedure.function != null &&
-          !procedure.function.positionalParameters.isEmpty) {
-        var firstPositionalParam = procedure.function.positionalParameters[0];
-        _diagnosticsReporter.report(
-            messageJsInteropAnonymousFactoryPositionalParameters,
-            firstPositionalParam.fileOffset,
-            firstPositionalParam.name!.length,
-            firstPositionalParam.location!.file);
-      }
+    if (!_isJSInteropMember(procedure)) {
+      _checkDisallowedExternal(procedure);
     } else {
-      // Only factory constructors for anonymous classes are allowed to have
-      // named parameters.
-      _checkNoNamedParameters(procedure.function);
+      // Check JS interop indexing.
+      if (!procedure.isStatic &&
+          (procedure.name.text == '[]=' || procedure.name.text == '[]')) {
+        _diagnosticsReporter.report(
+            messageJsInteropIndexNotSupported,
+            procedure.fileOffset,
+            procedure.name.text.length,
+            procedure.fileUri);
+      }
+
+      // Check JS Interop positional and named parameters.
+      var isAnonymousFactory =
+          _classHasAnonymousAnnotation && procedure.isFactory;
+      if (isAnonymousFactory) {
+        // ignore: unnecessary_null_comparison
+        if (procedure.function != null &&
+            !procedure.function.positionalParameters.isEmpty) {
+          var firstPositionalParam = procedure.function.positionalParameters[0];
+          _diagnosticsReporter.report(
+              messageJsInteropAnonymousFactoryPositionalParameters,
+              firstPositionalParam.fileOffset,
+              firstPositionalParam.name!.length,
+              firstPositionalParam.location!.file);
+        }
+      } else {
+        // Only factory constructors for anonymous classes are allowed to have
+        // named parameters.
+        _checkNoNamedParameters(procedure.function);
+      }
     }
   }
 
   @override
   void visitConstructor(Constructor constructor) {
-    _checkJSInteropAnnotation(constructor);
+    _checkInstanceMemberJSAnnotation(constructor);
     if (_classHasJSAnnotation &&
         !constructor.isExternal &&
         !constructor.isSynthetic) {
@@ -225,9 +236,12 @@
           constructor.name.text.length,
           constructor.fileUri);
     }
-    if (!_isJSInteropMember(constructor)) return;
 
-    _checkNoNamedParameters(constructor.function);
+    if (!_isJSInteropMember(constructor)) {
+      _checkDisallowedExternal(constructor);
+    } else {
+      _checkNoNamedParameters(constructor.function);
+    }
   }
 
   /// Reports an error if [functionNode] has named parameters.
@@ -243,9 +257,9 @@
     }
   }
 
-  /// Reports an error if [member] does not correctly use the JS interop
-  /// annotation or the keyword `external`.
-  void _checkJSInteropAnnotation(Member member) {
+  /// Reports an error if given instance [member] is JS interop, but inside a
+  /// non JS interop class.
+  void _checkInstanceMemberJSAnnotation(Member member) {
     var enclosingClass = member.enclosingClass;
 
     if (!_classHasJSAnnotation &&
@@ -262,13 +276,24 @@
                 enclosingClass.name.length)
           ]);
     }
+  }
 
-    // Check for correct `external` usage.
-    if (member.isExternal &&
-        !_isAllowedExternalUsage(member) &&
-        !hasJSInteropAnnotation(member)) {
-      if (member.enclosingClass != null && !_classHasJSAnnotation ||
-          member.enclosingClass == null && !_libraryHasJSAnnotation) {
+  /// Assumes given [member] is not JS interop, and reports an error if
+  /// [member] is `external` and not an allowed `external` usage.
+  void _checkDisallowedExternal(Member member) {
+    if (member.isExternal) {
+      // TODO(rileyporter): Allow extension members on some Native classes.
+      if (member.isExtensionMember) {
+        _diagnosticsReporter.report(
+            messageJsInteropExternalExtensionMemberNotOnJSClass,
+            member.fileOffset,
+            member.name.text.length,
+            member.fileUri);
+      } else if (!hasJSInteropAnnotation(member) &&
+          !_isAllowedExternalUsage(member)) {
+        // Member could be JS annotated and not considered a JS interop member
+        // if inside a non-JS interop class. Should not report an error in this
+        // case, since a different error will already be produced.
         _diagnosticsReporter.report(
             messageJsInteropExternalMemberNotJSAnnotated,
             member.fileOffset,
@@ -289,13 +314,19 @@
   }
 
   /// Returns whether [member] is considered to be a JS interop member.
+  ///
+  /// A JS interop member is `external`, and is in a valid JS interop context,
+  /// which can be:
+  ///   - inside a JS interop class
+  ///   - inside an extension on a JS interop class
+  ///   - a top level member that is JS interop annotated or in a JS interop
+  ///     library
+  /// If a member belongs to a class, the class must be JS interop annotated.
   bool _isJSInteropMember(Member member) {
     if (member.isExternal) {
       if (_classHasJSAnnotation) return true;
+      if (member.isExtensionMember) return _isJSExtensionMember(member);
       if (member.enclosingClass == null) {
-        // In the case where the member does not belong to any class, a JS
-        // annotation is not needed on the library to be considered JS interop
-        // as long as the member has an annotation.
         return hasJSInteropAnnotation(member) || _libraryHasJSAnnotation;
       }
     }
@@ -303,4 +334,19 @@
     // Otherwise, not JS interop.
     return false;
   }
+
+  /// Returns whether given extension [member] is in an extension that is on a
+  /// JS interop class.
+  bool _isJSExtensionMember(Member member) {
+    assert(member.isExtensionMember);
+    if (_libraryExtensionsIndex == null) {
+      _libraryExtensionsIndex = {};
+      member.enclosingLibrary.extensions.forEach((extension) =>
+          extension.members.forEach((memberDescriptor) =>
+              _libraryExtensionsIndex![memberDescriptor.member] = extension));
+    }
+
+    var onType = _libraryExtensionsIndex![member.reference]!.onType;
+    return onType is InterfaceType && hasJSInteropAnnotation(onType.classNode);
+  }
 }
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/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..b21b4c5 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
@@ -25,7 +25,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/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/front_end/messages.status b/pkg/front_end/messages.status
index 76faff7..b201a4c 100644
--- a/pkg/front_end/messages.status
+++ b/pkg/front_end/messages.status
@@ -510,6 +510,8 @@
 JsInteropDartClassExtendsJSClass/example: Fail # Web compiler specific
 JsInteropEnclosingClassJSAnnotation/analyzerCode: Fail # Web compiler specific
 JsInteropEnclosingClassJSAnnotation/example: Fail # Web compiler specific
+JsInteropExternalExtensionMemberNotOnJSClass/analyzerCode: Fail # Web compiler specific
+JsInteropExternalExtensionMemberNotOnJSClass/example: Fail # Web compiler specific
 JsInteropExternalMemberNotJSAnnotated/analyzerCode: Fail # Web compiler specific
 JsInteropExternalMemberNotJSAnnotated/example: Fail # Web compiler specific
 JsInteropIndexNotSupported/analyzerCode: Fail # Web compiler specific
diff --git a/pkg/front_end/messages.yaml b/pkg/front_end/messages.yaml
index 58334a0..f0cb719 100644
--- a/pkg/front_end/messages.yaml
+++ b/pkg/front_end/messages.yaml
@@ -4908,6 +4908,10 @@
   template: "This is the enclosing class."
   severity: CONTEXT
 
+JsInteropExternalExtensionMemberNotOnJSClass:
+  template: "JS interop class required for 'external' extension members."
+  tip: "Try adding a JS interop annotation to the on type class of the extension."
+
 JsInteropExternalMemberNotJSAnnotated:
   template: "Only JS interop members may be 'external'."
   tip: "Try removing the 'external' keyword or adding a JS interop annotation."
diff --git a/pkg/testing/lib/src/analyze.dart b/pkg/testing/lib/src/analyze.dart
index 099d020..05009ec 100644
--- a/pkg/testing/lib/src/analyze.dart
+++ b/pkg/testing/lib/src/analyze.dart
@@ -18,21 +18,21 @@
 import 'suite.dart' show Suite;
 
 class Analyze extends Suite {
-  final Uri analysisOptions;
+  final Uri? analysisOptions;
 
   final List<Uri> uris;
 
   final List<RegExp> exclude;
 
-  final List<String> gitGrepPathspecs;
+  final List<String>? gitGrepPathspecs;
 
-  final List<String> gitGrepPatterns;
+  final List<String>? gitGrepPatterns;
 
   Analyze(this.analysisOptions, this.uris, this.exclude, this.gitGrepPathspecs,
       this.gitGrepPatterns)
       : super("analyze", "analyze", null);
 
-  Future<Null> run(Uri packages, List<Uri> extraUris) {
+  Future<Null> run(Uri packages, List<Uri>? extraUris) {
     List<Uri> allUris = new List<Uri>.from(uris);
     if (extraUris != null) {
       allUris.addAll(extraUris);
@@ -43,8 +43,8 @@
 
   static Future<Analyze> fromJsonMap(
       Uri base, Map json, List<Suite> suites) async {
-    String optionsPath = json["options"];
-    Uri optionsUri = optionsPath == null ? null : base.resolve(optionsPath);
+    String? optionsPath = json["options"];
+    Uri? optionsUri = optionsPath == null ? null : base.resolve(optionsPath);
 
     List<Uri> uris = json["uris"].map<Uri>((relative) {
       String r = relative;
@@ -54,9 +54,9 @@
     List<RegExp> exclude =
         json["exclude"].map<RegExp>((p) => new RegExp(p)).toList();
 
-    Map gitGrep = json["git grep"];
-    List<String> gitGrepPathspecs;
-    List<String> gitGrepPatterns;
+    Map? gitGrep = json["git grep"];
+    List<String>? gitGrepPathspecs;
+    List<String>? gitGrepPatterns;
     if (gitGrep != null) {
       gitGrepPathspecs = gitGrep["pathspecs"] == null
           ? const <String>["."]
@@ -73,13 +73,13 @@
 }
 
 class AnalyzerDiagnostic {
-  final String kind;
+  final String? kind;
 
-  final String detailedKind;
+  final String? detailedKind;
 
-  final String code;
+  final String? code;
 
-  final Uri uri;
+  final Uri? uri;
 
   final int line;
 
@@ -106,7 +106,7 @@
     addPart() {
       parts.add(line
           .substring(start, index == -1 ? null : index)
-          .replaceAllMapped(unescapePattern, (Match m) => m[1]));
+          .replaceAllMapped(unescapePattern, (Match m) => m[1]!));
     }
 
     while (index != -1) {
@@ -136,8 +136,8 @@
   String toString() {
     return kind == null
         ? "Malformed output from dartanalyzer:\n$message"
-        : "${uri.toFilePath()}:$line:$startColumn: "
-            "${kind == 'INFO' ? 'warning: hint' : kind.toLowerCase()}:\n"
+        : "${uri!.toFilePath()}:$line:$startColumn: "
+            "${kind == 'INFO' ? 'warning: hint' : kind!.toLowerCase()}:\n"
             "[$code] $message";
   }
 }
@@ -154,12 +154,12 @@
 
 /// Run dartanalyzer on all tests in [uris].
 Future<Null> analyzeUris(
-    Uri analysisOptions,
+    Uri? analysisOptions,
     Uri packages,
     List<Uri> uris,
     List<RegExp> exclude,
-    List<String> gitGrepPathspecs,
-    List<String> gitGrepPatterns) async {
+    List<String>? gitGrepPathspecs,
+    List<String>? gitGrepPatterns) async {
   if (uris.isEmpty) return;
   String topLevel;
   try {
@@ -205,7 +205,7 @@
     arguments.addAll(
         gitGrepPatterns.expand((String pattern) => <String>["-e", pattern]));
     arguments.add("--");
-    arguments.addAll(gitGrepPathspecs);
+    arguments.addAll(gitGrepPathspecs!);
     filesToAnalyze.addAll(splitLines(await git("grep", arguments))
         .map((String line) => line.trimRight()));
   }
@@ -245,7 +245,7 @@
   processAnalyzerOutput(Stream<AnalyzerDiagnostic> diagnostics) async {
     await for (AnalyzerDiagnostic diagnostic in diagnostics) {
       if (diagnostic.uri != null) {
-        String path = toFilePath(diagnostic.uri);
+        String path = toFilePath(diagnostic.uri!);
         if (!filesToAnalyze.contains(path)) continue;
       }
       String message = "$diagnostic";
@@ -283,7 +283,7 @@
 }
 
 Future<String> git(String command, Iterable<String> arguments,
-    {String workingDirectory}) async {
+    {String? workingDirectory}) async {
   ProcessResult result = await Process.run(
       Platform.isWindows ? "git.bat" : "git",
       <String>[command]..addAll(arguments),
diff --git a/pkg/testing/lib/src/chain.dart b/pkg/testing/lib/src/chain.dart
index c56fc21..8ab797b 100644
--- a/pkg/testing/lib/src/chain.dart
+++ b/pkg/testing/lib/src/chain.dart
@@ -115,7 +115,7 @@
         .map((s) => s.substring(0, s.length - 3))
         .toList();
     TestExpectations expectations = await ReadTestExpectations(
-        <String>[suite.statusFile.toFilePath()], {}, expectationSet);
+        <String>[suite.statusFile!.toFilePath()], {}, expectationSet);
     Stream<TestDescription> stream = list(suite);
     if (suite.processMultitests) {
       stream = stream.transform(new MultitestTransformer());
@@ -149,12 +149,12 @@
       final Set<Expectation> expectedOutcomes = processExpectedOutcomes(
           expectations.expectations(description.shortName), description);
       final StringBuffer sb = new StringBuffer();
-      final Step lastStep = steps.isNotEmpty ? steps.last : null;
+      final Step? lastStep = steps.isNotEmpty ? steps.last : null;
       final Iterator<Step> iterator = steps.iterator;
 
-      Result result;
+      Result? result;
       // Records the outcome of the last step that was run.
-      Step lastStepRun;
+      Step? lastStepRun;
 
       /// Performs one step of [iterator].
       ///
@@ -194,30 +194,30 @@
           future = new Future.value(null);
         }
         future = future.then((_currentResult) async {
-          Result currentResult = _currentResult;
+          Result? currentResult = _currentResult;
           if (currentResult != null) {
             logger.logStepComplete(completed, unexpectedResults.length,
-                descriptions.length, suite, description, lastStepRun);
+                descriptions.length, suite, description, lastStepRun!);
             result = currentResult;
             if (currentResult.outcome == Expectation.Pass) {
               // The input to the next step is the output of this step.
-              return doStep(result.output);
+              return doStep(result!.output);
             }
           }
-          await cleanUp(description, result);
+          await cleanUp(description, result!);
           result =
-              processTestResult(description, result, lastStep == lastStepRun);
-          if (!expectedOutcomes.contains(result.outcome) &&
-              !expectedOutcomes.contains(result.outcome.canonical)) {
-            result.addLog("$sb");
-            unexpectedResults[description] = result;
+              processTestResult(description, result!, lastStep == lastStepRun);
+          if (!expectedOutcomes.contains(result!.outcome) &&
+              !expectedOutcomes.contains(result!.outcome.canonical)) {
+            result!.addLog("$sb");
+            unexpectedResults[description] = result!;
             unexpectedOutcomes[description] = expectedOutcomes;
             logger.logUnexpectedResult(
-                suite, description, result, expectedOutcomes);
+                suite, description, result!, expectedOutcomes);
             exitCode = 1;
           } else {
             logger.logExpectedResult(
-                suite, description, result, expectedOutcomes);
+                suite, description, result!, expectedOutcomes);
             logger.logMessage(sb);
           }
           logger.logTestComplete(++completed, unexpectedResults.length,
@@ -241,7 +241,7 @@
     if (unexpectedResults.isNotEmpty) {
       unexpectedResults.forEach((TestDescription description, Result result) {
         logger.logUnexpectedResult(
-            suite, description, result, unexpectedOutcomes[description]);
+            suite, description, result, unexpectedOutcomes[description]!);
       });
       print("${unexpectedResults.length} failed:");
       unexpectedResults.forEach((TestDescription description, Result result) {
@@ -278,7 +278,7 @@
       TestDescription description, Result result, bool last) {
     if (description is FileBasedTestDescription &&
         description.multitestExpectations != null) {
-      if (isError(description.multitestExpectations)) {
+      if (isError(description.multitestExpectations!)) {
         result =
             toNegativeTestResult(result, description.multitestExpectations);
       }
@@ -286,14 +286,14 @@
       if (result.outcome == Expectation.Pass) {
         result.addLog("Negative test didn't report an error.\n");
       } else if (result.outcome == Expectation.Fail) {
-        result.addLog("Negative test reported an error as expeceted.\n");
+        result.addLog("Negative test reported an error as expected.\n");
       }
       result = toNegativeTestResult(result);
     }
     return result;
   }
 
-  Result toNegativeTestResult(Result result, [Set<String> expectations]) {
+  Result toNegativeTestResult(Result result, [Set<String>? expectations]) {
     Expectation outcome = result.outcome;
     if (outcome == Expectation.Pass) {
       if (expectations == null) {
@@ -312,9 +312,9 @@
     return result.copyWithOutcome(outcome);
   }
 
-  Future<void> cleanUp(TestDescription description, Result result) => null;
+  Future<void> cleanUp(TestDescription description, Result result) async {}
 
-  Future<void> postRun() => null;
+  Future<void> postRun() async {}
 }
 
 abstract class Step<I, O, C extends ChainContext> {
@@ -355,25 +355,25 @@
 
   Result<O> crash(error, StackTrace trace) => new Result<O>.crash(error, trace);
 
-  Result<O> fail(O output, [error, StackTrace trace]) {
+  Result<O> fail(O output, [error, StackTrace? trace]) {
     return new Result<O>.fail(output, error, trace);
   }
 }
 
 class Result<O> {
-  final O output;
+  final O? output;
 
   final Expectation outcome;
 
   final error;
 
-  final StackTrace trace;
+  final StackTrace? trace;
 
   final List<String> logs = <String>[];
 
   /// If set, running the test with '-D$autoFixCommand' will automatically
   /// update the test to match new expectations.
-  final String autoFixCommand;
+  final String? autoFixCommand;
 
   /// If set, the test can be fixed by running
   ///
@@ -391,7 +391,7 @@
   Result.crash(error, StackTrace trace)
       : this(null, Expectation.Crash, error, trace: trace);
 
-  Result.fail(O output, [error, StackTrace trace])
+  Result.fail(O output, [error, StackTrace? trace])
       : this(output, Expectation.Fail, error, trace: trace);
 
   bool get isPass => outcome == Expectation.Pass;
@@ -420,7 +420,8 @@
 Future<Null> runChain(CreateContext f, Map<String, String> environment,
     Set<String> selectors, String jsonText) {
   return withErrorHandling(() async {
-    Chain suite = new Suite.fromJsonMap(Uri.base, json.decode(jsonText));
+    Chain suite =
+        new Suite.fromJsonMap(Uri.base, json.decode(jsonText)) as Chain;
     print("Running ${suite.name}");
     ChainContext context = await f(suite, environment);
     return context.run(suite, selectors);
diff --git a/pkg/testing/lib/src/discover.dart b/pkg/testing/lib/src/discover.dart
index 3fd09e9..6b761ab 100644
--- a/pkg/testing/lib/src/discover.dart
+++ b/pkg/testing/lib/src/discover.dart
@@ -20,10 +20,10 @@
     <String>["-c", "--packages=${packageConfig.toFilePath()}"];
 
 Stream<FileBasedTestDescription> listTests(List<Uri> testRoots,
-    {Pattern pattern}) {
+    {Pattern? pattern}) {
   StreamController<FileBasedTestDescription> controller =
       new StreamController<FileBasedTestDescription>();
-  Map<Uri, StreamSubscription> subscriptions = <Uri, StreamSubscription>{};
+  Map<Uri, StreamSubscription?> subscriptions = <Uri, StreamSubscription>{};
   for (Uri testRootUri in testRoots) {
     subscriptions[testRootUri] = null;
     Directory testRoot = new Directory.fromUri(testRootUri);
@@ -32,8 +32,9 @@
         Stream<FileSystemEntity> stream =
             testRoot.list(recursive: true, followLinks: false);
         var subscription = stream.listen((FileSystemEntity entity) {
-          FileBasedTestDescription description = FileBasedTestDescription
-              .from(testRootUri, entity, pattern: pattern);
+          FileBasedTestDescription? description = FileBasedTestDescription.from(
+              testRootUri, entity,
+              pattern: pattern);
           if (description != null) {
             controller.add(description);
           }
@@ -59,7 +60,7 @@
 }
 
 Uri computePackageConfig() {
-  String path = Platform.packageConfig;
+  String? path = Platform.packageConfig;
   if (path != null) return Uri.base.resolve(path);
   return Uri.base.resolve(".packages");
 }
@@ -72,7 +73,7 @@
     : null;
 
 Uri computeDartSdk() {
-  String dartSdkPath = Platform.environment["DART_SDK"] ?? _dartSdk;
+  String? dartSdkPath = Platform.environment["DART_SDK"] ?? _dartSdk;
   if (dartSdkPath != null) {
     return Uri.base.resolveUri(new Uri.file(dartSdkPath));
   } else {
@@ -83,7 +84,7 @@
 }
 
 Future<Process> startDart(Uri program,
-    [List<String> arguments, List<String> vmArguments]) {
+    [List<String>? arguments, List<String>? vmArguments]) {
   List<String> allArguments = <String>[];
   allArguments.addAll(vmArguments ?? dartArguments);
   allArguments.add(program.toFilePath());
diff --git a/pkg/testing/lib/src/error_handling.dart b/pkg/testing/lib/src/error_handling.dart
index 452e4b7..872f2f0 100644
--- a/pkg/testing/lib/src/error_handling.dart
+++ b/pkg/testing/lib/src/error_handling.dart
@@ -12,13 +12,14 @@
 
 import 'log.dart';
 
-Future<T> withErrorHandling<T>(Future<T> f(), {Logger logger}) async {
+Future<T?> withErrorHandling<T>(Future<T> f(), {Logger? logger}) async {
   final ReceivePort port = new ReceivePort();
   try {
     return await f();
   } catch (e, trace) {
     exitCode = 1;
     stderr.writeln(e);
+    // ignore: unnecessary_null_comparison
     if (trace != null) {
       stderr.writeln(trace);
     }
diff --git a/pkg/testing/lib/src/expectation.dart b/pkg/testing/lib/src/expectation.dart
index 58efce8..153adee 100644
--- a/pkg/testing/lib/src/expectation.dart
+++ b/pkg/testing/lib/src/expectation.dart
@@ -42,7 +42,7 @@
 
   String toString() => name;
 
-  static Expectation fromGroup(ExpectationGroup group) {
+  static Expectation? fromGroup(ExpectationGroup group) {
     switch (group) {
       case ExpectationGroup.Crash:
         return Expectation.Crash;
@@ -57,7 +57,6 @@
       case ExpectationGroup.Timeout:
         return Expectation.Timeout;
     }
-    throw "Unhandled group: '$group'.";
   }
 }
 
@@ -89,8 +88,8 @@
     Map<String, Expectation> internalMap =
         new Map<String, Expectation>.from(Default.internalMap);
     for (Map map in data) {
-      String name;
-      String group;
+      String? name;
+      String? group;
       map.forEach((_key, _value) {
         String key = _key;
         String value = _value;
@@ -113,12 +112,12 @@
       if (group == null) {
         throw "No group provided in '$map'";
       }
-      Expectation expectation = new Expectation(name, groupFromString(group));
-      name = name.toLowerCase();
+      Expectation expectation = new Expectation(name!, groupFromString(group!));
+      name = name!.toLowerCase();
       if (internalMap.containsKey(name)) {
         throw "Duplicated expectation name: '$name'.";
       }
-      internalMap[name] = expectation;
+      internalMap[name!] = expectation;
     }
     return new ExpectationSet(internalMap);
   }
diff --git a/pkg/testing/lib/src/log.dart b/pkg/testing/lib/src/log.dart
index 7e6fafd..d46ab0a 100644
--- a/pkg/testing/lib/src/log.dart
+++ b/pkg/testing/lib/src/log.dart
@@ -78,19 +78,19 @@
 class StdoutLogger implements Logger {
   const StdoutLogger();
 
-  void logTestStart(int completed, int failed, int total, Suite suite,
-      TestDescription description) {}
+  void logTestStart(int completed, int failed, int total, Suite? suite,
+      TestDescription? description) {}
 
-  void logTestComplete(int completed, int failed, int total, Suite suite,
-      TestDescription description) {
+  void logTestComplete(int completed, int failed, int total, Suite? suite,
+      TestDescription? description) {
     String message = formatProgress(completed, failed, total);
     if (suite != null) {
-      message += ": ${formatTestDescription(suite, description)}";
+      message += ": ${formatTestDescription(suite, description!)}";
     }
     logProgress(message);
   }
 
-  void logStepStart(int completed, int failed, int total, Suite suite,
+  void logStepStart(int completed, int failed, int total, Suite? suite,
       TestDescription description, Step step) {
     String message = formatProgress(completed, failed, total);
     if (suite != null) {
@@ -102,7 +102,7 @@
     logProgress(message);
   }
 
-  void logStepComplete(int completed, int failed, int total, Suite suite,
+  void logStepComplete(int completed, int failed, int total, Suite? suite,
       TestDescription description, Step step) {
     if (!step.isAsync) return;
     String message = formatProgress(completed, failed, total);
@@ -152,7 +152,7 @@
   void logUnexpectedResult(Suite suite, TestDescription description,
       Result result, Set<Expectation> expectedOutcomes) {
     print("${eraseLine}UNEXPECTED: ${suite.name}/${description.shortName}");
-    Uri statusFile = suite.statusFile;
+    Uri? statusFile = suite.statusFile;
     if (statusFile != null) {
       String path = statusFile.toFilePath();
       if (result.outcome == Expectation.Pass) {
@@ -186,6 +186,7 @@
 
   void logUncaughtError(error, StackTrace stackTrace) {
     logMessage(error);
+    // ignore: unnecessary_null_comparison
     if (stackTrace != null) {
       logMessage(stackTrace);
     }
diff --git a/pkg/testing/lib/src/multitest.dart b/pkg/testing/lib/src/multitest.dart
index d87ac1b..62bb63e 100644
--- a/pkg/testing/lib/src/multitest.dart
+++ b/pkg/testing/lib/src/multitest.dart
@@ -51,8 +51,8 @@
 
     nextTest:
     await for (TestDescription test in stream) {
-      FileBasedTestDescription multitest;
-      String contents;
+      FileBasedTestDescription? multitest;
+      String? contents;
       if (test is FileBasedTestDescription) {
         contents = await test.file.readAsString();
         if (contents.contains(multitestMarker)) {
@@ -73,11 +73,11 @@
         "none": new Set<String>(),
       };
       int lineNumber = 0;
-      for (String line in splitLines(contents)) {
+      for (String line in splitLines(contents!)) {
         lineNumber++;
         int index = line.indexOf(multitestMarker);
-        String subtestName;
-        List<String> subtestOutcomesList;
+        String? subtestName;
+        List<String>? subtestOutcomesList;
         if (index != -1) {
           String annotationText =
               line.substring(index + _multitestMarkerLength).trim();
@@ -102,7 +102,7 @@
           lines.add(line);
           Set<String> subtestOutcomes =
               outcomes.putIfAbsent(subtestName, () => new Set<String>());
-          if (subtestOutcomesList.length != 1 ||
+          if (subtestOutcomesList!.length != 1 ||
               subtestOutcomesList.single != "continued") {
             for (String outcome in subtestOutcomesList) {
               if (validOutcomes.contains(outcome)) {
@@ -125,8 +125,9 @@
       Directory generated =
           new Directory.fromUri(root.resolve(multitest.shortName));
       generated = await generated.create(recursive: true);
-      for (String name in testsAsLines.keys) {
-        List<String> lines = testsAsLines[name];
+      for (MapEntry<String, List<String>> entry in testsAsLines.entries) {
+        String name = entry.key;
+        List<String> lines = entry.value;
         Uri uri = generated.uri.resolve("${name}_generated.dart");
         FileBasedTestDescription subtest =
             new FileBasedTestDescription(root, new File.fromUri(uri));
diff --git a/pkg/testing/lib/src/run.dart b/pkg/testing/lib/src/run.dart
index 7ce9bd0..4567236 100644
--- a/pkg/testing/lib/src/run.dart
+++ b/pkg/testing/lib/src/run.dart
@@ -34,10 +34,10 @@
 
 import 'run_tests.dart' show CommandLine;
 
-Future<TestRoot> computeTestRoot(String configurationPath, Uri base) {
+Future<TestRoot> computeTestRoot(String? configurationPath, Uri? base) {
   Uri configuration = configurationPath == null
       ? Uri.base.resolve("testing.json")
-      : base.resolve(configurationPath);
+      : base!.resolve(configurationPath);
   return TestRoot.fromUri(configuration);
 }
 
@@ -50,8 +50,8 @@
 /// `testing.json` isn't located in the current working directory and is a path
 /// relative to [me] which defaults to `Platform.script`.
 Future<Null> runMe(List<String> arguments, CreateContext f,
-    {String configurationPath,
-    Uri me,
+    {String? configurationPath,
+    Uri? me,
     int shards = 1,
     int shard = 0,
     Logger logger: const StdoutLogger()}) {
@@ -98,7 +98,7 @@
 /// `testing.json` isn't located in the current working directory and is a path
 /// relative to `Uri.base`.
 Future<Null> run(List<String> arguments, List<String> suiteNames,
-    [String configurationPath]) {
+    [String? configurationPath]) {
   return withErrorHandling(() async {
     TestRoot root = await computeTestRoot(configurationPath, Uri.base);
     List<Suite> suites = root.suites
@@ -106,7 +106,7 @@
         .toList();
     SuiteRunner runner = new SuiteRunner(suites, <String, String>{},
         const <String>[], new Set<String>(), new Set<String>());
-    String program = await runner.generateDartProgram();
+    String? program = await runner.generateDartProgram();
     await runner.analyze(root.packages);
     if (program != null) {
       await runProgram(program, root.packages);
@@ -125,7 +125,7 @@
       errorsAreFatal: false,
       checked: true,
       packageConfig: packages);
-  List error;
+  List? error;
   var subscription = isolate.errors.listen((data) {
     error = data;
     exitPort.close();
@@ -137,7 +137,7 @@
   subscription.cancel();
   return error == null
       ? null
-      : new Future<Null>.error(error[0], new StackTrace.fromString(error[1]));
+      : new Future<Null>.error(error![0], new StackTrace.fromString(error![1]));
 }
 
 class SuiteRunner {
@@ -162,7 +162,7 @@
         (selectedSuites.isEmpty || selectedSuites.contains(suite.name));
   }
 
-  Future<String> generateDartProgram() async {
+  Future<String?> generateDartProgram() async {
     testUris.clear();
     StringBuffer imports = new StringBuffer();
     StringBuffer dart = new StringBuffer();
@@ -238,10 +238,10 @@
   }
 
   Stream<FileBasedTestDescription> listDescriptions() async* {
-    for (Dart suite in suites.where((Suite suite) => suite is Dart)) {
+    for (Dart suite in suites.whereType<Dart>()) {
       await for (FileBasedTestDescription description
           in listTests(<Uri>[suite.uri], pattern: "")) {
-        testUris.add(await Isolate.resolvePackageUri(description.uri));
+        testUris.add((await Isolate.resolvePackageUri(description.uri))!);
         if (shouldRunSuite(suite)) {
           String path = description.file.uri.path;
           if (suite.exclude.any((RegExp r) => path.contains(r))) continue;
@@ -254,19 +254,19 @@
   }
 
   Stream<Chain> listChainSuites() async* {
-    for (Chain suite in suites.where((Suite suite) => suite is Chain)) {
-      testUris.add(await Isolate.resolvePackageUri(suite.source));
+    for (Chain suite in suites.whereType<Chain>()) {
+      testUris.add((await Isolate.resolvePackageUri(suite.source))!);
       if (shouldRunSuite(suite)) {
         yield suite;
       }
     }
   }
 
-  Iterable<Suite> listTestDartSuites() {
-    return suites.where((Suite suite) => suite is TestDart);
+  Iterable<TestDart> listTestDartSuites() {
+    return suites.whereType<TestDart>();
   }
 
-  Iterable<Suite> listAnalyzerSuites() {
-    return suites.where((Suite suite) => suite is Analyze);
+  Iterable<Analyze> listAnalyzerSuites() {
+    return suites.whereType<Analyze>();
   }
 }
diff --git a/pkg/testing/lib/src/run_tests.dart b/pkg/testing/lib/src/run_tests.dart
index b4ccd3b..03d5d3d 100644
--- a/pkg/testing/lib/src/run_tests.dart
+++ b/pkg/testing/lib/src/run_tests.dart
@@ -64,7 +64,7 @@
 
   Iterable<String> get selectors => arguments;
 
-  Future<Uri> get configuration async {
+  Future<Uri?> get configuration async {
     const String configPrefix = "--config=";
     List<String> configurationPaths = options
         .where((String option) => option.startsWith(configPrefix))
@@ -111,7 +111,7 @@
     }
     const StdoutLogger()
         .logMessage("Reading configuration file '$configurationPath'.");
-    Uri configuration =
+    Uri? configuration =
         await Isolate.resolvePackageUri(Uri.base.resolve(configurationPath));
     if (configuration == null ||
         !await new File.fromUri(configuration).exists()) {
@@ -147,7 +147,7 @@
         enableVerboseOutput();
       }
       Map<String, String> environment = cl.environment;
-      Uri configuration = await cl.configuration;
+      Uri? configuration = await cl.configuration;
       if (configuration == null) return;
       if (!isVerbose) {
         print("Use --verbose to display more details.");
@@ -155,7 +155,7 @@
       TestRoot root = await TestRoot.fromUri(configuration);
       SuiteRunner runner = new SuiteRunner(
           root.suites, environment, cl.selectors, cl.selectedSuites, cl.skip);
-      String program = await runner.generateDartProgram();
+      String? program = await runner.generateDartProgram();
       bool hasAnalyzerSuites = await runner.analyze(root.packages);
       Stopwatch sw = new Stopwatch()..start();
       if (program == null) {
@@ -178,7 +178,7 @@
         try {
           await runGuarded(() {
             print("Running test $name");
-            return tests[name]();
+            return tests[name]!();
           }, printLineOnStdout: sb.writeln);
           const StdoutLogger().logMessage(sb);
         } catch (e) {
diff --git a/pkg/testing/lib/src/stdio_process.dart b/pkg/testing/lib/src/stdio_process.dart
index 16bbfdf..587a07f 100644
--- a/pkg/testing/lib/src/stdio_process.dart
+++ b/pkg/testing/lib/src/stdio_process.dart
@@ -41,13 +41,13 @@
   }
 
   static Future<StdioProcess> run(String executable, List<String> arguments,
-      {String input,
-      Duration timeout: const Duration(seconds: 60),
+      {String? input,
+      Duration? timeout: const Duration(seconds: 60),
       bool suppressOutput: true,
       bool runInShell: false}) async {
     Process process =
         await Process.start(executable, arguments, runInShell: runInShell);
-    Timer timer;
+    Timer? timer;
     StringBuffer sb = new StringBuffer();
     if (timeout != null) {
       timer = new Timer(timeout, () {
@@ -75,8 +75,8 @@
       stdoutStream = stdoutStream.transform(transformToStdio(io.stdout));
       stderrStream = stderrStream.transform(transformToStdio(io.stderr));
     }
-    Future<List<String>> stdoutFuture = stdoutStream.toList();
-    Future<List<String>> stderrFuture = stderrStream.toList();
+    Future<List<String>> stdoutFuture = stdoutStream.toList() as Future<List<String>>;
+    Future<List<String>> stderrFuture = stderrStream.toList() as Future<List<String>>;
     int exitCode = await process.exitCode;
     timer?.cancel();
     sb.writeAll(await stdoutFuture);
diff --git a/pkg/testing/lib/src/suite.dart b/pkg/testing/lib/src/suite.dart
index 54aa7c8..b159797 100644
--- a/pkg/testing/lib/src/suite.dart
+++ b/pkg/testing/lib/src/suite.dart
@@ -14,7 +14,7 @@
 
   final String kind;
 
-  final Uri statusFile;
+  final Uri? statusFile;
 
   Suite(this.name, this.kind, this.statusFile);
 
diff --git a/pkg/testing/lib/src/test_dart/path.dart b/pkg/testing/lib/src/test_dart/path.dart
index 3c5fbea..f0b5b51 100644
--- a/pkg/testing/lib/src/test_dart/path.dart
+++ b/pkg/testing/lib/src/test_dart/path.dart
@@ -192,7 +192,7 @@
   Path makeCanonical() {
     bool isAbs = isAbsolute;
     List segs = segments();
-    String drive;
+    String? drive;
     if (isAbs && !segs.isEmpty && segs[0].length == 2 && segs[0][1] == ':') {
       drive = segs[0];
       segs.removeRange(0, 1);
@@ -267,7 +267,7 @@
   }
 
   List<String> segments() {
-    List result = _path.split('/');
+    List<String> result = _path.split('/');
     if (isAbsolute) result.removeRange(0, 1);
     if (hasTrailingSeparator) result.removeLast();
     return result;
diff --git a/pkg/testing/lib/src/test_dart/status_expression.dart b/pkg/testing/lib/src/test_dart/status_expression.dart
index 200d241..395483a 100644
--- a/pkg/testing/lib/src/test_dart/status_expression.dart
+++ b/pkg/testing/lib/src/test_dart/status_expression.dart
@@ -64,7 +64,9 @@
     if (!testRegexp.hasMatch(expression)) {
       throw new FormatException("Syntax error in '$expression'");
     }
-    for (Match match in regexp.allMatches(expression)) tokens.add(match[0]);
+    for (Match match in regexp.allMatches(expression)) {
+      tokens.add(match[0]!);
+    }
     return tokens;
   }
 }
@@ -179,8 +181,8 @@
 // An iterator that allows peeking at the current token.
 class Scanner {
   List<String> tokens;
-  Iterator tokenIterator;
-  String current;
+  late Iterator tokenIterator;
+  String? current;
 
   Scanner(this.tokens) {
     tokenIterator = tokens.iterator;
@@ -241,11 +243,11 @@
       scanner.advance();
       return value;
     }
-    if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) {
+    if (!new RegExp(r"^\w+$").hasMatch(scanner.current!)) {
       throw new FormatException(
           "Expected identifier in expression, got ${scanner.current}");
     }
-    SetExpression value = new SetConstant(scanner.current);
+    SetExpression value = new SetConstant(scanner.current!);
     scanner.advance();
     return value;
   }
@@ -290,21 +292,21 @@
           "Expected \$ in expression, got ${scanner.current}");
     }
     scanner.advance();
-    if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) {
+    if (!new RegExp(r"^\w+$").hasMatch(scanner.current!)) {
       throw new FormatException(
           "Expected identifier in expression, got ${scanner.current}");
     }
-    TermVariable left = new TermVariable(scanner.current);
+    TermVariable left = new TermVariable(scanner.current!);
     scanner.advance();
     if (scanner.current == Token.EQUALS ||
         scanner.current == Token.NOT_EQUALS) {
       bool negate = scanner.current == Token.NOT_EQUALS;
       scanner.advance();
-      if (!new RegExp(r"^\w+$").hasMatch(scanner.current)) {
+      if (!new RegExp(r"^\w+$").hasMatch(scanner.current!)) {
         throw new FormatException(
             "Expected value in expression, got ${scanner.current}");
       }
-      TermConstant right = new TermConstant(scanner.current);
+      TermConstant right = new TermConstant(scanner.current!);
       scanner.advance();
       return new Comparison(left, right, negate);
     } else {
diff --git a/pkg/testing/lib/src/test_dart/status_file_parser.dart b/pkg/testing/lib/src/test_dart/status_file_parser.dart
index 1ce711f..a3d2f58 100644
--- a/pkg/testing/lib/src/test_dart/status_file_parser.dart
+++ b/pkg/testing/lib/src/test_dart/status_file_parser.dart
@@ -29,7 +29,7 @@
 class Section {
   final StatusFile statusFile;
 
-  final BooleanExpression condition;
+  final BooleanExpression? condition;
   final List<TestRule> testRules;
   final int lineNumber;
 
@@ -40,7 +40,7 @@
       : testRules = <TestRule>[];
 
   bool isEnabled(Map<String, String> environment) =>
-      condition == null || condition.evaluate(environment);
+      condition == null || condition!.evaluate(environment);
 
   String toString() {
     return "Section: $condition";
@@ -93,17 +93,17 @@
 
   lines.listen((String line) {
     lineNumber++;
-    Match match = SplitComment.firstMatch(line);
-    line = (match == null) ? "" : match[1];
+    Match? match = SplitComment.firstMatch(line);
+    line = (match == null) ? "" : match[1]!;
     line = line.trim();
     if (line.isEmpty) return;
 
     // Extract the comment to get the issue number if needed.
-    String comment = (match == null || match[2] == null) ? "" : match[2];
+    String comment = (match == null || match[2] == null) ? "" : match[2]!;
 
     match = HeaderPattern.firstMatch(line);
     if (match != null) {
-      String condition_string = match[1].trim();
+      String condition_string = match[1]!.trim();
       List<String> tokens = new Tokenizer(condition_string).tokenize();
       ExpressionParser parser = new ExpressionParser(new Scanner(tokens));
       currentSection =
@@ -114,21 +114,21 @@
 
     match = RulePattern.firstMatch(line);
     if (match != null) {
-      String name = match[1].trim();
+      String name = match[1]!.trim();
       // TODO(whesse): Handle test names ending in a wildcard (*).
-      String expression_string = match[2].trim();
+      String expression_string = match[2]!.trim();
       List<String> tokens = new Tokenizer(expression_string).tokenize();
       SetExpression expression =
           new ExpressionParser(new Scanner(tokens)).parseSetExpression();
 
       // Look for issue number in comment.
-      String issueString = null;
+      String? issueString = null;
       match = IssueNumberPattern.firstMatch(comment);
       if (match != null) {
         issueString = match[1];
         if (issueString == null) issueString = match[2];
       }
-      int issue = issueString != null ? int.parse(issueString) : null;
+      int? issue = issueString != null ? int.parse(issueString) : null;
       currentSection.testRules
           .add(new TestRule(name, expression, issue, lineNumber));
       return;
@@ -141,7 +141,7 @@
 class TestRule {
   String name;
   SetExpression expression;
-  int issue;
+  int? issue;
   int lineNumber;
 
   TestRule(this.name, this.expression, this.issue, this.lineNumber);
@@ -161,8 +161,8 @@
 
   Map<String, Set<Expectation>> _map;
   bool _preprocessed = false;
-  Map<String, RegExp> _regExpCache;
-  Map<String, List<RegExp>> _keyToRegExps;
+  Map<String, RegExp>? _regExpCache;
+  Map<String, List<RegExp>>? _keyToRegExps;
 
   /**
    * Create a TestExpectations object. See the [expectations] method
@@ -203,7 +203,7 @@
     _preprocessForMatching();
 
     _map.forEach((key, expectation) {
-      List regExps = _keyToRegExps[key];
+      List<RegExp> regExps = _keyToRegExps![key]!;
       if (regExps.length > splitFilename.length) return;
       for (var i = 0; i < regExps.length; i++) {
         if (!regExps[i].hasMatch(splitFilename[i])) return;
@@ -231,20 +231,19 @@
     _regExpCache = {};
 
     _map.forEach((key, expectations) {
-      if (_keyToRegExps[key] != null) return;
+      if (_keyToRegExps![key] != null) return;
       var splitKey = key.split('/');
-      var regExps = new List<RegExp>.filled(splitKey.length, null);
-      for (var i = 0; i < splitKey.length; i++) {
+      var regExps = new List<RegExp>.generate(splitKey.length, (int i) {
         var component = splitKey[i];
-        var regExp = _regExpCache[component];
+        var regExp = _regExpCache![component];
         if (regExp == null) {
           var pattern = "^${splitKey[i]}\$".replaceAll('*', '.*');
           regExp = new RegExp(pattern);
-          _regExpCache[component] = regExp;
+          _regExpCache![component] = regExp;
         }
-        regExps[i] = regExp;
-      }
-      _keyToRegExps[key] = regExps;
+        return regExp;
+      }, growable: false);
+      _keyToRegExps![key] = regExps;
     });
 
     _regExpCache = null;
diff --git a/pkg/testing/lib/src/test_description.dart b/pkg/testing/lib/src/test_description.dart
index 7480ee0..1fe0ac1 100644
--- a/pkg/testing/lib/src/test_description.dart
+++ b/pkg/testing/lib/src/test_description.dart
@@ -17,11 +17,11 @@
 class FileBasedTestDescription extends TestDescription {
   final Uri root;
   final File file;
-  final Uri output;
+  final Uri? output;
 
   /// If non-null, this is a generated multitest, and the set contains the
   /// expected outcomes.
-  Set<String> multitestExpectations;
+  Set<String>? multitestExpectations;
 
   FileBasedTestDescription(this.root, this.file, {this.output});
 
@@ -52,8 +52,8 @@
     sink.writeln('.main,');
   }
 
-  static FileBasedTestDescription from(Uri root, FileSystemEntity entity,
-      {Pattern pattern}) {
+  static FileBasedTestDescription? from(Uri root, FileSystemEntity entity,
+      {Pattern? pattern}) {
     if (entity is! File) return null;
     pattern ??= "_test.dart";
     String path = entity.uri.path;
diff --git a/pkg/testing/lib/src/test_root.dart b/pkg/testing/lib/src/test_root.dart
index 9e3b65e..554abee 100644
--- a/pkg/testing/lib/src/test_root.dart
+++ b/pkg/testing/lib/src/test_root.dart
@@ -49,7 +49,7 @@
 
   TestRoot(this.packages, this.suites);
 
-  Analyze get analyze => suites.last;
+  Analyze get analyze => suites.last as Analyze;
 
   List<Uri> get urisToAnalyze => analyze.uris;
 
diff --git a/pkg/testing/lib/src/zone_helper.dart b/pkg/testing/lib/src/zone_helper.dart
index d8d7d39..a378d7a 100644
--- a/pkg/testing/lib/src/zone_helper.dart
+++ b/pkg/testing/lib/src/zone_helper.dart
@@ -14,8 +14,8 @@
 import 'log.dart' show StdoutLogger;
 
 Future runGuarded(Future f(),
-    {void printLineOnStdout(line),
-    void handleLateError(error, StackTrace stackTrace)}) {
+    {void Function(String)? printLineOnStdout,
+    void Function(dynamic, StackTrace)? handleLateError}) {
   var printWrapper;
   if (printLineOnStdout != null) {
     printWrapper = (_1, _2, _3, String line) {
@@ -39,6 +39,7 @@
         // Ignored.
       }
       stderr
+          // ignore: unnecessary_null_comparison
           .write("$errorString\n" + (stackTrace == null ? "" : "$stackTrace"));
       stderr.flush();
       exit(255);
@@ -81,7 +82,7 @@
 /// Ping [isolate] to ensure control messages have been delivered.  Control
 /// messages are things like [Isolate.addErrorListener] and
 /// [Isolate.addOnExitListener].
-Future acknowledgeControlMessages(Isolate isolate, {Capability resume}) {
+Future acknowledgeControlMessages(Isolate isolate, {Capability? resume}) {
   ReceivePort ping = new ReceivePort();
   Isolate.current.ping(ping.sendPort);
   if (resume == null) {
diff --git a/pkg/testing/pubspec.yaml b/pkg/testing/pubspec.yaml
index f012946..d85f43f 100644
--- a/pkg/testing/pubspec.yaml
+++ b/pkg/testing/pubspec.yaml
@@ -4,4 +4,4 @@
 # This package is not intended for consumption on pub.dev. DO NOT publish.
 publish_to: none
 environment:
-  sdk: '>=2.0.0 <3.0.0'
+  sdk: '>=2.12.0 <3.0.0'
diff --git a/pkg/vm_service/CHANGELOG.md b/pkg/vm_service/CHANGELOG.md
index 3c1a964..b73b21b 100644
--- a/pkg/vm_service/CHANGELOG.md
+++ b/pkg/vm_service/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 7.2.0
+- Update to version `3.49` of the spec.
+- Added `CpuSamples` event kind, and `cpuSamples` property to `Event`.
+
 ## 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/canonical_tables.h b/runtime/vm/canonical_tables.h
index b0e25b5..ba9297e 100644
--- a/runtime/vm/canonical_tables.h
+++ b/runtime/vm/canonical_tables.h
@@ -131,7 +131,8 @@
     return concat.ToSymbol();
   }
 };
-typedef UnorderedHashSet<SymbolTraits> CanonicalStringSet;
+
+typedef UnorderedHashSet<SymbolTraits, AcqRelStorageTraits> CanonicalStringSet;
 
 class CanonicalTypeKey {
  public:
diff --git a/runtime/vm/clustered_snapshot.cc b/runtime/vm/clustered_snapshot.cc
index b7e0aef..9eec30d 100644
--- a/runtime/vm/clustered_snapshot.cc
+++ b/runtime/vm/clustered_snapshot.cc
@@ -114,6 +114,14 @@
   }
 
   static bool IsImmutable(const ArrayHandle& handle) { return false; }
+
+  static ObjectPtr At(ArrayHandle* array, intptr_t index) {
+    return array->At(index);
+  }
+
+  static void SetAt(ArrayHandle* array, intptr_t index, const Object& value) {
+    array->SetAt(index, value);
+  }
 };
 }  // namespace
 
@@ -5584,6 +5592,7 @@
                             DECLARE_OBJECT_STORE_FIELD,
                             DECLARE_OBJECT_STORE_FIELD,
                             DECLARE_OBJECT_STORE_FIELD,
+                            DECLARE_OBJECT_STORE_FIELD,
                             DECLARE_OBJECT_STORE_FIELD)
 #undef DECLARE_OBJECT_STORE_FIELD
 };
diff --git a/runtime/vm/hash_table.h b/runtime/vm/hash_table.h
index 06fbd70..318dbf4 100644
--- a/runtime/vm/hash_table.h
+++ b/runtime/vm/hash_table.h
@@ -33,6 +33,24 @@
   static bool IsImmutable(const ArrayHandle& handle) {
     return handle.ptr()->untag()->InVMIsolateHeap();
   }
+
+  static ObjectPtr At(ArrayHandle* array, intptr_t index) {
+    return array->At(index);
+  }
+
+  static void SetAt(ArrayHandle* array, intptr_t index, const Object& value) {
+    array->SetAt(index, value);
+  }
+};
+
+struct AcqRelStorageTraits : ArrayStorageTraits {
+  static ObjectPtr At(ArrayHandle* array, intptr_t index) {
+    return array->AtAcquire(index);
+  }
+
+  static void SetAt(ArrayHandle* array, intptr_t index, const Object& value) {
+    array->SetAtRelease(index, value);
+  }
 };
 
 class HashTableBase : public ValueObject {
@@ -162,19 +180,19 @@
   void Initialize() const {
     ASSERT(data_->Length() >= ArrayLengthForNumOccupied(0));
     *smi_handle_ = Smi::New(0);
-    data_->SetAt(kOccupiedEntriesIndex, *smi_handle_);
-    data_->SetAt(kDeletedEntriesIndex, *smi_handle_);
+    StorageTraits::SetAt(data_, kOccupiedEntriesIndex, *smi_handle_);
+    StorageTraits::SetAt(data_, kDeletedEntriesIndex, *smi_handle_);
 
 #if !defined(PRODUCT)
-    data_->SetAt(kNumGrowsIndex, *smi_handle_);
-    data_->SetAt(kNumLT5LookupsIndex, *smi_handle_);
-    data_->SetAt(kNumLT25LookupsIndex, *smi_handle_);
-    data_->SetAt(kNumGT25LookupsIndex, *smi_handle_);
-    data_->SetAt(kNumProbesIndex, *smi_handle_);
+    StorageTraits::SetAt(data_, kNumGrowsIndex, *smi_handle_);
+    StorageTraits::SetAt(data_, kNumLT5LookupsIndex, *smi_handle_);
+    StorageTraits::SetAt(data_, kNumLT25LookupsIndex, *smi_handle_);
+    StorageTraits::SetAt(data_, kNumGT25LookupsIndex, *smi_handle_);
+    StorageTraits::SetAt(data_, kNumProbesIndex, *smi_handle_);
 #endif  // !defined(PRODUCT)
 
     for (intptr_t i = kHeaderSize; i < data_->Length(); ++i) {
-      data_->SetAt(i, UnusedMarker());
+      StorageTraits::SetAt(data_, i, UnusedMarker());
     }
   }
 
@@ -298,14 +316,14 @@
   ObjectPtr GetPayload(intptr_t entry, intptr_t component) const {
     ASSERT(IsOccupied(entry));
     return WeakSerializationReference::Unwrap(
-        data_->At(PayloadIndex(entry, component)));
+        StorageTraits::At(data_, PayloadIndex(entry, component)));
   }
   void UpdatePayload(intptr_t entry,
                      intptr_t component,
                      const Object& value) const {
     ASSERT(IsOccupied(entry));
     ASSERT(0 <= component && component < kPayloadSize);
-    data_->SetAt(PayloadIndex(entry, component), value);
+    StorageTraits::SetAt(data_, PayloadIndex(entry, component), value);
   }
   // Deletes both the key and payload of the specified entry.
   void DeleteEntry(intptr_t entry) const {
@@ -411,22 +429,23 @@
   }
 
   ObjectPtr InternalGetKey(intptr_t entry) const {
-    return WeakSerializationReference::Unwrap(data_->At(KeyIndex(entry)));
+    return WeakSerializationReference::Unwrap(
+        StorageTraits::At(data_, KeyIndex(entry)));
   }
 
   void InternalSetKey(intptr_t entry, const Object& key) const {
-    data_->SetAt(KeyIndex(entry), key);
+    StorageTraits::SetAt(data_, KeyIndex(entry), key);
   }
 
   intptr_t GetSmiValueAt(intptr_t index) const {
     ASSERT(!data_->IsNull());
-    ASSERT(!data_->At(index)->IsHeapObject());
-    return Smi::Value(Smi::RawCast(data_->At(index)));
+    ASSERT(!StorageTraits::At(data_, index)->IsHeapObject());
+    return Smi::Value(Smi::RawCast(StorageTraits::At(data_, index)));
   }
 
   void SetSmiValueAt(intptr_t index, intptr_t value) const {
     *smi_handle_ = Smi::New(value);
-    data_->SetAt(index, *smi_handle_);
+    StorageTraits::SetAt(data_, index, *smi_handle_);
   }
 
   void AdjustSmiValueAt(intptr_t index, intptr_t delta) const {
@@ -450,10 +469,13 @@
 };
 
 // Table with unspecified iteration order. No payload overhead or metadata.
-template <typename KeyTraits, intptr_t kUserPayloadSize>
-class UnorderedHashTable : public HashTable<KeyTraits, kUserPayloadSize, 0> {
+template <typename KeyTraits,
+          intptr_t kUserPayloadSize,
+          typename StorageTraits = ArrayStorageTraits>
+class UnorderedHashTable
+    : public HashTable<KeyTraits, kUserPayloadSize, 0, StorageTraits> {
  public:
-  typedef HashTable<KeyTraits, kUserPayloadSize, 0> BaseTable;
+  typedef HashTable<KeyTraits, kUserPayloadSize, 0, StorageTraits> BaseTable;
   static const intptr_t kPayloadSize = kUserPayloadSize;
   explicit UnorderedHashTable(ArrayPtr data)
       : BaseTable(Thread::Current()->zone(), data) {}
@@ -776,10 +798,13 @@
   }
 };
 
-template <typename KeyTraits>
-class UnorderedHashSet : public HashSet<UnorderedHashTable<KeyTraits, 0> > {
+template <typename KeyTraits, typename TableStorageTraits = ArrayStorageTraits>
+class UnorderedHashSet
+    : public HashSet<UnorderedHashTable<KeyTraits, 0, TableStorageTraits>> {
+  using UnderlyingTable = UnorderedHashTable<KeyTraits, 0, TableStorageTraits>;
+
  public:
-  typedef HashSet<UnorderedHashTable<KeyTraits, 0> > BaseSet;
+  typedef HashSet<UnderlyingTable> BaseSet;
   explicit UnorderedHashSet(ArrayPtr data)
       : BaseSet(Thread::Current()->zone(), data) {
     ASSERT(data != Array::null());
@@ -791,7 +816,8 @@
   void Dump() const {
     Object& entry = Object::Handle();
     for (intptr_t i = 0; i < this->data_->Length(); i++) {
-      entry = WeakSerializationReference::Unwrap(this->data_->At(i));
+      entry = WeakSerializationReference::Unwrap(
+          TableStorageTraits::At(this->data_, i));
       if (entry.ptr() == BaseSet::UnusedMarker().ptr() ||
           entry.ptr() == BaseSet::DeletedMarker().ptr() || entry.IsSmi()) {
         // empty, deleted, num_used/num_deleted
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index dffed84..9a718d1 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -359,7 +359,7 @@
 #if !defined(DART_PRECOMPILED_RUNTIME)
       background_compiler_(new BackgroundCompiler(this)),
 #endif
-      symbols_lock_(new SafepointRwLock()),
+      symbols_mutex_(NOT_IN_PRODUCT("IsolateGroup::symbols_mutex_")),
       type_canonicalization_mutex_(
           NOT_IN_PRODUCT("IsolateGroup::type_canonicalization_mutex_")),
       type_arguments_canonicalization_mutex_(NOT_IN_PRODUCT(
@@ -2500,6 +2500,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
   }
 
@@ -2558,26 +2577,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/isolate.h b/runtime/vm/isolate.h
index 261b14f..d7c8e5d 100644
--- a/runtime/vm/isolate.h
+++ b/runtime/vm/isolate.h
@@ -508,7 +508,7 @@
   StoreBuffer* store_buffer() const { return store_buffer_.get(); }
   ClassTable* class_table() const { return class_table_.get(); }
   ObjectStore* object_store() const { return object_store_.get(); }
-  SafepointRwLock* symbols_lock() { return symbols_lock_.get(); }
+  Mutex* symbols_mutex() { return &symbols_mutex_; }
   Mutex* type_canonicalization_mutex() { return &type_canonicalization_mutex_; }
   Mutex* type_arguments_canonicalization_mutex() {
     return &type_arguments_canonicalization_mutex_;
@@ -868,7 +868,7 @@
 
   NOT_IN_PRECOMPILED(std::unique_ptr<BackgroundCompiler> background_compiler_);
 
-  std::unique_ptr<SafepointRwLock> symbols_lock_;
+  Mutex symbols_mutex_;
   Mutex type_canonicalization_mutex_;
   Mutex type_arguments_canonicalization_mutex_;
   Mutex subtype_test_cache_mutex_;
diff --git a/runtime/vm/object_store.cc b/runtime/vm/object_store.cc
index ca90802..dbc59c7 100644
--- a/runtime/vm/object_store.cc
+++ b/runtime/vm/object_store.cc
@@ -123,7 +123,7 @@
 #define EMIT_FIELD_NAME(type, name) #name "_",
         OBJECT_STORE_FIELD_LIST(
             EMIT_FIELD_NAME, EMIT_FIELD_NAME, EMIT_FIELD_NAME, EMIT_FIELD_NAME,
-            EMIT_FIELD_NAME, EMIT_FIELD_NAME, EMIT_FIELD_NAME)
+            EMIT_FIELD_NAME, EMIT_FIELD_NAME, EMIT_FIELD_NAME, EMIT_FIELD_NAME)
 #undef EMIT_FIELD_NAME
     };
     ObjectPtr* current = from();
diff --git a/runtime/vm/object_store.h b/runtime/vm/object_store.h
index cc50d4b..8148bc2 100644
--- a/runtime/vm/object_store.h
+++ b/runtime/vm/object_store.h
@@ -36,13 +36,14 @@
 //
 // R_ - needs getter only
 // RW - needs getter and setter
-// ARW - needs getter and setter with atomic access
+// ARW_RELAXED - needs getter and setter with relaxed atomic access
+// ARW_AR - needs getter and setter with acq/rel atomic access
 // LAZY_CORE - needs lazy init getter for a "dart:core" member
 // LAZY_ASYNC - needs lazy init getter for a "dart:async" member
 // LAZY_ISOLATE - needs lazy init getter for a "dart:isolate" member
 // LAZY_INTERNAL - needs lazy init getter for a "dart:_internal" member
-#define OBJECT_STORE_FIELD_LIST(R_, RW, ARW, LAZY_CORE, LAZY_ASYNC,            \
-                                LAZY_ISOLATE, LAZY_INTERNAL)                   \
+#define OBJECT_STORE_FIELD_LIST(R_, RW, ARW_RELAXED, ARW_AR, LAZY_CORE,        \
+                                LAZY_ASYNC, LAZY_ISOLATE, LAZY_INTERNAL)       \
   LAZY_CORE(Class, list_class)                                                 \
   LAZY_CORE(Type, non_nullable_list_rare_type)                                 \
   LAZY_CORE(Type, non_nullable_map_rare_type)                                  \
@@ -141,7 +142,7 @@
   RW(Class, float64x2_class)                                                   \
   RW(Class, error_class)                                                       \
   RW(Class, weak_property_class)                                               \
-  RW(Array, symbol_table)                                                      \
+  ARW_AR(Array, symbol_table)                                                  \
   RW(Array, canonical_types)                                                   \
   RW(Array, canonical_function_types)                                          \
   RW(Array, canonical_type_parameters)                                         \
@@ -177,8 +178,8 @@
   RW(Function, complete_on_async_return)                                       \
   RW(Function, complete_on_async_error)                                        \
   RW(Class, async_star_stream_controller)                                      \
-  ARW(Smi, future_timeout_future_index)                                        \
-  ARW(Smi, future_wait_future_index)                                           \
+  ARW_RELAXED(Smi, future_timeout_future_index)                                \
+  ARW_RELAXED(Smi, future_wait_future_index)                                   \
   RW(CompressedStackMaps, canonicalized_stack_map_entries)                     \
   RW(ObjectPool, global_object_pool)                                           \
   RW(Array, unique_dynamic_targets)                                            \
@@ -403,7 +404,7 @@
 #define DECLARE_GETTER_AND_SETTER(Type, name)                                  \
   DECLARE_GETTER(Type, name)                                                   \
   void set_##name(const Type& value) { name##_ = value.ptr(); }
-#define DECLARE_ATOMIC_GETTER_AND_SETTER(Type, name)                           \
+#define DECLARE_RELAXED_ATOMIC_GETTER_AND_SETTER(Type, name)                   \
   template <std::memory_order order = std::memory_order_relaxed>               \
   Type##Ptr name() const {                                                     \
     return name##_.load(order);                                                \
@@ -413,6 +414,10 @@
     name##_.store(value.ptr(), order);                                         \
   }                                                                            \
   DECLARE_OFFSET(name)
+#define DECLARE_ACQREL_ATOMIC_GETTER_AND_SETTER(Type, name)                    \
+  Type##Ptr name() const { return name##_.load(); }                            \
+  void set_##name(const Type& value) { name##_.store(value.ptr()); }           \
+  DECLARE_OFFSET(name)
 #define DECLARE_LAZY_INIT_GETTER(Type, name, init)                             \
   Type##Ptr name() {                                                           \
     if (name##_.load() == Type::null()) {                                      \
@@ -431,7 +436,8 @@
   DECLARE_LAZY_INIT_GETTER(Type, name, LazyInitInternalMembers)
   OBJECT_STORE_FIELD_LIST(DECLARE_GETTER,
                           DECLARE_GETTER_AND_SETTER,
-                          DECLARE_ATOMIC_GETTER_AND_SETTER,
+                          DECLARE_RELAXED_ATOMIC_GETTER_AND_SETTER,
+                          DECLARE_ACQREL_ATOMIC_GETTER_AND_SETTER,
                           DECLARE_LAZY_INIT_CORE_GETTER,
                           DECLARE_LAZY_INIT_ASYNC_GETTER,
                           DECLARE_LAZY_INIT_ISOLATE_GETTER,
@@ -439,7 +445,8 @@
 #undef DECLARE_OFFSET
 #undef DECLARE_GETTER
 #undef DECLARE_GETTER_AND_SETTER
-#undef DECLARE_ATOMIC_GETTER_AND_SETTER
+#undef DECLARE_RELAXED_ATOMIC_GETTER_AND_SETTER
+#undef DECLARE_ACQREL_ATOMIC_GETTER_AND_SETTER
 #undef DECLARE_LAZY_INIT_GETTER
 #undef DECLARE_LAZY_INIT_CORE_GETTER
 #undef DECLARE_LAZY_INIT_ASYNC_GETTER
@@ -512,6 +519,7 @@
                           DECLARE_LAZY_OBJECT_STORE_FIELD,
                           DECLARE_LAZY_OBJECT_STORE_FIELD,
                           DECLARE_LAZY_OBJECT_STORE_FIELD,
+                          DECLARE_LAZY_OBJECT_STORE_FIELD,
                           DECLARE_LAZY_OBJECT_STORE_FIELD)
 #undef DECLARE_OBJECT_STORE_FIELD
 #undef DECLARE_ATOMIC_OBJECT_STORE_FIELD
diff --git a/runtime/vm/profiler.cc b/runtime/vm/profiler.cc
index cb269c4..58b4696 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);
@@ -327,8 +329,13 @@
     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/symbols.cc b/runtime/vm/symbols.cc
index 441fb501..ae7d824 100644
--- a/runtime/vm/symbols.cc
+++ b/runtime/vm/symbols.cc
@@ -357,7 +357,9 @@
 
     // Most common case: The symbol is already in the table.
     {
-      SafepointReadRwLocker sl(thread, group->symbols_lock());
+      // We do allow lock-free concurrent read access to the symbol table.
+      // Both, the array in the ObjectStore as well as elements in the array
+      // are accessed via store-release/load-acquire barriers.
       data = object_store->symbol_table();
       CanonicalStringSet table(&key, &value, &data);
       symbol ^= table.GetOrNull(str);
@@ -365,7 +367,7 @@
     }
     // Otherwise we'll have to get exclusive access and get-or-insert it.
     if (symbol.IsNull()) {
-      SafepointWriteRwLocker sl(thread, group->symbols_lock());
+      SafepointMutexLocker ml(group->symbols_mutex());
       data = object_store->symbol_table();
       CanonicalStringSet table(&key, &value, &data);
       symbol ^= table.InsertNewOrGet(str);
@@ -410,7 +412,6 @@
       symbol ^= table.GetOrNull(str);
       table.Release();
     } else {
-      SafepointReadRwLocker sl(thread, group->symbols_lock());
       data = object_store->symbol_table();
       CanonicalStringSet table(&key, &value, &data);
       symbol ^= table.GetOrNull(str);
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/developer/service.dart b/sdk/lib/developer/service.dart
index 8715d25..9d6fe09 100644
--- a/sdk/lib/developer/service.dart
+++ b/sdk/lib/developer/service.dart
@@ -63,12 +63,13 @@
     // Port to receive response from service isolate.
     final RawReceivePort receivePort =
         new RawReceivePort(null, 'Service.getInfo');
-    final Completer<Uri?> uriCompleter = new Completer<Uri?>();
-    receivePort.handler = (Uri? uri) => uriCompleter.complete(uri);
+    final Completer<String?> completer = new Completer<String?>();
+    receivePort.handler = (String? uriString) => completer.complete(uriString);
     // Request the information from the service isolate.
     _getServerInfo(receivePort.sendPort);
     // Await the response from the service isolate.
-    Uri? uri = await uriCompleter.future;
+    String? uriString = await completer.future;
+    Uri? uri = uriString == null ? null : Uri.parse(uriString);
     // Close the port.
     receivePort.close();
     return new ServiceProtocolInfo(uri);
@@ -85,12 +86,13 @@
     // Port to receive response from service isolate.
     final RawReceivePort receivePort =
         new RawReceivePort(null, 'Service.controlWebServer');
-    final Completer<Uri> uriCompleter = new Completer<Uri>();
-    receivePort.handler = (Uri uri) => uriCompleter.complete(uri);
+    final Completer<String?> completer = new Completer<String?>();
+    receivePort.handler = (String? uriString) => completer.complete(uriString);
     // Request the information from the service isolate.
     _webServerControl(receivePort.sendPort, enable, silenceOutput);
     // Await the response from the service isolate.
-    Uri uri = await uriCompleter.future;
+    String? uriString = await completer.future;
+    Uri? uri = uriString == null ? null : Uri.parse(uriString);
     // Close the port.
     receivePort.close();
     return new ServiceProtocolInfo(uri);
diff --git a/sdk/lib/libraries.yaml b/sdk/lib/libraries.yaml
index 6262221..9a1a1d8 100644
--- a/sdk/lib/libraries.yaml
+++ b/sdk/lib/libraries.yaml
@@ -5,7 +5,7 @@
 # Note: if you edit this file, you must also generate libraries.json in this
 # directory:
 #
-#     python3 ./tools/yaml2json.py sdk/lib/libraries.yaml sdk/lib/libraries.json
+#     dart tools/yaml2json.dart sdk/lib/libraries.yaml sdk/lib/libraries.json
 #
 # We currently have several different files that needs to be updated when
 # changing libraries, sources, and patch files.  See
diff --git a/sdk/lib/vmservice/vmservice.dart b/sdk/lib/vmservice/vmservice.dart
index d492dcf..dacc9d5 100644
--- a/sdk/lib/vmservice/vmservice.dart
+++ b/sdk/lib/vmservice/vmservice.dart
@@ -366,7 +366,7 @@
           return;
         }
         final uri = await webServerControl(enable, silenceOutput);
-        sp.send(uri);
+        sp.send(uri?.toString());
         break;
       case Constants.SERVER_INFO_MESSAGE_ID:
         final serverInformation = VMServiceEmbedderHooks.serverInformation;
@@ -375,7 +375,7 @@
           return;
         }
         final uri = await serverInformation();
-        sp.send(uri);
+        sp.send(uri?.toString());
         break;
     }
   }
diff --git a/sdk/lib/vmservice_libraries.yaml b/sdk/lib/vmservice_libraries.yaml
index 40015c3..d5e049c 100644
--- a/sdk/lib/vmservice_libraries.yaml
+++ b/sdk/lib/vmservice_libraries.yaml
@@ -5,7 +5,7 @@
 # Note: if you edit this file, you must also generate libraries.json in this
 # directory:
 #
-#     python3 ./tools/yaml2json.py sdk/lib/vmservice_libraries.yaml sdk/lib/vmservice_libraries.json
+#     dart tools/yaml2json.dart sdk/lib/vmservice_libraries.yaml sdk/lib/vmservice_libraries.json
 #
 # We currently have several different files that needs to be updated when
 # changing libraries, sources, and patch files.  See
diff --git a/tests/lib/js/external_nonjs_static_test.dart b/tests/lib/js/external_nonjs_static_test.dart
index 47b6252..913d202 100644
--- a/tests/lib/js/external_nonjs_static_test.dart
+++ b/tests/lib/js/external_nonjs_static_test.dart
@@ -98,4 +98,61 @@
   // [web] Only JS interop members may be 'external'.
 }
 
+extension ExtensionNonJS on NonJSClass {
+  external var field;
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+  external final finalField;
+  //             ^
+  // [web] JS interop class required for 'external' extension members.
+  external static var staticField;
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+  external static final staticFinalField;
+  //                    ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external get getter;
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+  external set setter(_);
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external static get staticGetter;
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+  external static set staticSetter(_);
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external method();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+  external static staticMethod();
+  //              ^
+  // [web] JS interop class required for 'external' extension members.
+  external optionalParameterMethod([int? a, int b = 0]);
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+  external overridenMethod();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+
+  nonExternalMethod() => 1;
+  static nonExternalStaticMethod() => 2;
+}
+
+class NonJSClass {
+  void overridenMethod() => 5;
+}
+
+extension ExtensionGenericNonJS<T> on GenericNonJSClass<T> {
+  external T method();
+  //         ^
+  // [web] JS interop class required for 'external' extension members.
+}
+
+class GenericNonJSClass<T> {}
+
 main() {}
diff --git a/tests/lib/js/external_static_test.dart b/tests/lib/js/external_static_test.dart
index 8fac26db..0a1c8db 100644
--- a/tests/lib/js/external_static_test.dart
+++ b/tests/lib/js/external_static_test.dart
@@ -86,4 +86,147 @@
   // [web] Only JS interop members may be 'external'.
 }
 
+extension ExtensionNonJS on NonJSClass {
+  external var field;
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+  external final finalField;
+  //             ^
+  // [web] JS interop class required for 'external' extension members.
+  external static var staticField;
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+  external static final staticFinalField;
+  //                    ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external get getter;
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+  external set setter(_);
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external static get staticGetter;
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+  external static set staticSetter(_);
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external method();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+  external static staticMethod();
+  //              ^
+  // [web] JS interop class required for 'external' extension members.
+  external optionalParameterMethod([int? a, int b = 0]);
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+  external overridenMethod();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+
+  @JS('fieldAnnotation')
+  external var annotatedField;
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+  @JS('memberAnnotation')
+  external annotatedMethod();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+
+  nonExternalMethod() => 1;
+  static nonExternalStaticMethod() => 2;
+}
+
+class NonJSClass {
+  void overridenMethod() => 5;
+}
+
+extension ExtensionGenericNonJS<T> on GenericNonJSClass<T> {
+  external T method();
+  //         ^
+  // [web] JS interop class required for 'external' extension members.
+}
+
+class GenericNonJSClass<T> {}
+
+extension ExtensionJS on JSClass {
+  external var field;
+  external final finalField;
+  external static var staticField;
+  external static final staticFinalField;
+
+  external get getter;
+  external set setter(_);
+
+  external static get staticGetter;
+  external static set staticSetter(_);
+
+  external method();
+  external static staticMethod();
+  external optionalParameterMethod([int? a, int b = 0]);
+
+  @JS('fieldAnnotation')
+  external var annotatedField;
+
+  @JS('memberAnnotation')
+  external annotatedMethod();
+
+  nonExternalMethod() => 1;
+  static nonExternalStaticMethod() => 2;
+}
+
+@JS()
+class JSClass {}
+
+extension ExtensionGenericJS<T> on GenericJSClass<T> {
+  external T method();
+}
+
+@JS()
+class GenericJSClass<T> {}
+
+extension ExtensionAnonymousJS on AnonymousJSClass {
+  external var field;
+  external get getter;
+  external set setter(_);
+  external method();
+}
+
+@JS()
+@anonymous
+class AnonymousJSClass {}
+
+extension ExtensionAbstractJS on AbstractJSClass {
+  external var field;
+  external get getter;
+  external set setter(_);
+  external method();
+}
+
+@JS()
+abstract class AbstractJSClass {}
+
+extension ExtensionAnnotatedJS on AnnotatedJSClass {
+  external var field;
+  external get getter;
+  external set setter(_);
+  external method();
+}
+
+@JS('Annotation')
+class AnnotatedJSClass {}
+
+extension ExtensionPrivateJS on _privateJSClass {
+  external var field;
+  external get getter;
+  external set setter(_);
+  external method();
+}
+
+@JS()
+class _privateJSClass {}
+
 main() {}
diff --git a/tests/lib/js/parameters_static_test.dart b/tests/lib/js/parameters_static_test.dart
index 7fa7ddc..b6c68fe 100644
--- a/tests/lib/js/parameters_static_test.dart
+++ b/tests/lib/js/parameters_static_test.dart
@@ -58,4 +58,13 @@
   // [web] Named parameters for JS interop functions are only allowed in a factory constructor of an @anonymous JS class.
 }
 
+extension ExtensionFoo on Foo {
+  external int singleNamedArg({int? a});
+  //                                ^
+  // [web] Named parameters for JS interop functions are only allowed in a factory constructor of an @anonymous JS class.
+  external int mixedNamedArgs(int a, {int? b});
+  //                                       ^
+  // [web] Named parameters for JS interop functions are only allowed in a factory constructor of an @anonymous JS class.
+}
+
 main() {}
diff --git a/tests/lib_2/js/external_nonjs_static_test.dart b/tests/lib_2/js/external_nonjs_static_test.dart
index 6635d27..1de7e61 100644
--- a/tests/lib_2/js/external_nonjs_static_test.dart
+++ b/tests/lib_2/js/external_nonjs_static_test.dart
@@ -72,4 +72,45 @@
   // [web] Only JS interop members may be 'external'.
 }
 
+extension ExtensionNonJS on NonJSClass {
+  external get getter;
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+  external set setter(_);
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external static get staticGetter;
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+  external static set staticSetter(_);
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external method();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+  external static staticMethod();
+  //              ^
+  // [web] JS interop class required for 'external' extension members.
+  external overridenMethod();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+
+  nonExternalMethod() => 1;
+  static nonExternalStaticMethod() => 2;
+}
+
+class NonJSClass {
+  void overridenMethod() => 5;
+}
+
+extension ExtensionGenericNonJS<T> on GenericNonJSClass<T> {
+  external T method();
+  //         ^
+  // [web] JS interop class required for 'external' extension members.
+}
+
+class GenericNonJSClass<T> {}
+
 main() {}
diff --git a/tests/lib_2/js/external_static_test.dart b/tests/lib_2/js/external_static_test.dart
index 8ac09be..3f4c60b7 100644
--- a/tests/lib_2/js/external_static_test.dart
+++ b/tests/lib_2/js/external_static_test.dart
@@ -66,4 +66,114 @@
   // [web] Only JS interop members may be 'external'.
 }
 
+extension ExtensionNonJS on NonJSClass {
+  external get getter;
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+  external set setter(_);
+  //           ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external static get staticGetter;
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+  external static set staticSetter(_);
+  //                  ^
+  // [web] JS interop class required for 'external' extension members.
+
+  external method();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+  external static staticMethod();
+  //              ^
+  // [web] JS interop class required for 'external' extension members.
+  external overridenMethod();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+
+  @JS('memberAnnotation')
+  external annotatedMethod();
+  //       ^
+  // [web] JS interop class required for 'external' extension members.
+
+  nonExternalMethod() => 1;
+  static nonExternalStaticMethod() => 2;
+}
+
+class NonJSClass {
+  void overridenMethod() => 5;
+}
+
+extension ExtensionGenericNonJS<T> on GenericNonJSClass<T> {
+  external T method();
+  //         ^
+  // [web] JS interop class required for 'external' extension members.
+}
+
+class GenericNonJSClass<T> {}
+
+extension ExtensionJS on JSClass {
+  external get getter;
+  external set setter(_);
+
+  external static get staticGetter;
+  external static set staticSetter(_);
+
+  external method();
+  external static staticMethod();
+
+  @JS('memberAnnotation')
+  external annotatedMethod();
+
+  nonExternalMethod() => 1;
+  static nonExternalStaticMethod() => 2;
+}
+
+@JS()
+class JSClass {}
+
+extension ExtensionGenericJS<T> on GenericJSClass<T> {
+  external T method();
+}
+
+@JS()
+class GenericJSClass<T> {}
+
+extension ExtensionAnonymousJS on AnonymousJSClass {
+  external get getter;
+  external set setter(_);
+  external method();
+}
+
+@JS()
+@anonymous
+class AnonymousJSClass {}
+
+extension ExtensionAbstractJS on AbstractJSClass {
+  external get getter;
+  external set setter(_);
+  external method();
+}
+
+@JS()
+abstract class AbstractJSClass {}
+
+extension ExtensionAnnotatedJS on AnnotatedJSClass {
+  external get getter;
+  external set setter(_);
+  external method();
+}
+
+@JS('Annotation')
+class AnnotatedJSClass {}
+
+extension ExtensionPrivateJS on _privateJSClass {
+  external get getter;
+  external set setter(_);
+  external method();
+}
+
+@JS()
+class _privateJSClass {}
+
 main() {}
diff --git a/tests/standalone/io/file_fuzz_test.dart b/tests/standalone/io/file_fuzz_test.dart
index f7f89c8..813a2bf 100644
--- a/tests/standalone/io/file_fuzz_test.dart
+++ b/tests/standalone/io/file_fuzz_test.dart
@@ -14,9 +14,10 @@
 import "package:async_helper/async_helper.dart";
 
 fuzzSyncMethods() {
+  var temp = Directory.systemTemp.createTempSync('dart_file_fuzz');
   typeMapping.forEach((k, v) {
     File? file;
-    doItSync(() => file = new File(v as String));
+    doItSync(() => file = new File('${temp.path}/${v as String}'));
     if (file == null) return;
     final f = file!;
     doItSync(f.existsSync);
@@ -36,14 +37,16 @@
       doItSync(() => f.readAsLinesSync(encoding: v2 as Encoding));
     });
   });
+  temp.deleteSync(recursive: true);
 }
 
 fuzzAsyncMethods() {
   asyncStart();
   var futures = <Future>[];
+  var temp = Directory.systemTemp.createTempSync('dart_file_fuzz');
   typeMapping.forEach((k, v) {
     File? file;
-    doItSync(() => file = new File(v as String));
+    doItSync(() => file = new File('${temp.path}/${v as String}'));
     if (file == null) return;
     final f = file!;
     futures.add(doItAsync(f.exists));
@@ -62,7 +65,10 @@
       futures.add(doItAsync(() => f.readAsLines(encoding: v2 as Encoding)));
     });
   });
-  Future.wait(futures).then((_) => asyncEnd());
+  Future.wait(futures).then((_) {
+    temp.deleteSync(recursive: true);
+    asyncEnd();
+  });
 }
 
 fuzzSyncRandomAccessMethods() {
diff --git a/tests/standalone/io/raw_datagram_read_all_test.dart b/tests/standalone/io/raw_datagram_read_all_test.dart
index 73d11c4..225da8e 100644
--- a/tests/standalone/io/raw_datagram_read_all_test.dart
+++ b/tests/standalone/io/raw_datagram_read_all_test.dart
@@ -29,7 +29,7 @@
         var datagram = receiver.receive()!;
         Expect.listEquals([0], datagram.data);
         if (timer != null) timer.cancel();
-        timer = new Timer(const Duration(milliseconds: 200), () {
+        timer = new Timer(const Duration(seconds: 1), () {
           Expect.isNull(receiver.receive());
           receiver.close();
           asyncEnd();
diff --git a/tests/standalone_2/io/file_fuzz_test.dart b/tests/standalone_2/io/file_fuzz_test.dart
index 60a7e5b..95a704c 100644
--- a/tests/standalone_2/io/file_fuzz_test.dart
+++ b/tests/standalone_2/io/file_fuzz_test.dart
@@ -15,9 +15,10 @@
 import "package:async_helper/async_helper.dart";
 
 fuzzSyncMethods() {
+  var temp = Directory.systemTemp.createTempSync('dart_file_fuzz');
   typeMapping.forEach((k, v) {
     File f;
-    doItSync(() => f = new File(v));
+    doItSync(() => f = new File('${temp.path}/$v'));
     if (f == null) return;
     doItSync(f.existsSync);
     doItSync(f.createSync);
@@ -36,14 +37,16 @@
       doItSync(() => f.readAsLinesSync(encoding: v2));
     });
   });
+  temp.deleteSync(recursive: true);
 }
 
 fuzzAsyncMethods() {
   asyncStart();
   var futures = <Future>[];
+  var temp = Directory.systemTemp.createTempSync('dart_file_fuzz');
   typeMapping.forEach((k, v) {
     File f;
-    doItSync(() => f = new File(v));
+    doItSync(() => f = new File('${temp.path}/$v'));
     if (f == null) return;
     futures.add(doItAsync(f.exists));
     futures.add(doItAsync(f.delete));
@@ -61,7 +64,10 @@
       futures.add(doItAsync(() => f.readAsLines(encoding: v2)));
     });
   });
-  Future.wait(futures).then((_) => asyncEnd());
+  Future.wait(futures).then((_) {
+    temp.deleteSync(recursive: true);
+    asyncEnd();
+  });
 }
 
 fuzzSyncRandomAccessMethods() {
diff --git a/tests/standalone_2/io/raw_datagram_read_all_test.dart b/tests/standalone_2/io/raw_datagram_read_all_test.dart
index d8f9fab..8b7a7d7 100644
--- a/tests/standalone_2/io/raw_datagram_read_all_test.dart
+++ b/tests/standalone_2/io/raw_datagram_read_all_test.dart
@@ -31,7 +31,7 @@
         var datagram = receiver.receive();
         Expect.listEquals([0], datagram.data);
         if (timer != null) timer.cancel();
-        timer = new Timer(const Duration(milliseconds: 200), () {
+        timer = new Timer(const Duration(seconds: 1), () {
           Expect.isNull(receiver.receive());
           receiver.close();
           asyncEnd();
diff --git a/tools/VERSION b/tools/VERSION
index 3b8e28a..2f31731 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 358
+PRERELEASE 359
 PRERELEASE_PATCH 0
\ No newline at end of file
diff --git a/tools/yaml2json.dart b/tools/yaml2json.dart
index 8ce4907..e8b6a07 100644
--- a/tools/yaml2json.dart
+++ b/tools/yaml2json.dart
@@ -2,8 +2,6 @@
 // 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 'dart:io' show File, exit, stderr;
 
 import 'dart:isolate' show RawReceivePort;
@@ -12,7 +10,7 @@
 
 import 'package:yaml/yaml.dart' show loadYaml;
 
-main(List<String> arguments) async {
+main(List<String> arguments) {
   var port = new RawReceivePort();
   if (arguments.length != 2) {
     stderr.writeln("Usage: yaml2json.dart input.yaml output.json");
@@ -20,7 +18,7 @@
   }
   Uri input = Uri.base.resolve(arguments[0]);
   Uri output = Uri.base.resolve(arguments[1]);
-  Map yaml = loadYaml(await new File.fromUri(input).readAsString());
+  Map yaml = loadYaml(new File.fromUri(input).readAsStringSync());
   Map<String, dynamic> result = new Map<String, dynamic>();
   result["comment:0"] = "NOTE: THIS FILE IS GENERATED. DO NOT EDIT.";
   result["comment:1"] =
@@ -29,6 +27,6 @@
     result[key] = yaml[key];
   }
   File file = new File.fromUri(output);
-  await file.writeAsString(const JsonEncoder.withIndent("  ").convert(result));
+  file.writeAsStringSync(const JsonEncoder.withIndent("  ").convert(result));
   port.close();
 }
diff --git a/tools/yaml2json.py b/tools/yaml2json.py
deleted file mode 100755
index 251f657..0000000
--- a/tools/yaml2json.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (c) 2017, 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 os
-import subprocess
-import sys
-
-import utils
-
-
-def Main():
-    args = sys.argv[1:]
-    yaml2json_dart = os.path.relpath(
-        os.path.join(os.path.dirname(__file__), "yaml2json.dart"))
-    command = [utils.CheckedInSdkExecutable(), yaml2json_dart] + args
-
-    with utils.CoreDumpArchiver(args):
-        exit_code = subprocess.call(command)
-
-    utils.DiagnoseExitCode(exit_code, command)
-    return exit_code
-
-
-if __name__ == '__main__':
-    sys.exit(Main())