Version 2.15.0-190.0.dev

Merge commit '7990144badcb4b32fd1a99b0964d50e9011264a8' into 'dev'
diff --git a/DEPS b/DEPS
index bbeda37f..72abd08 100644
--- a/DEPS
+++ b/DEPS
@@ -90,7 +90,7 @@
   "collection_rev": "a4c941ab94044d118b2086a3f261c30377604127",
   "convert_rev": "e063fdca4bebffecbb5e6aa5525995120982d9ce",
   "crypto_rev": "b5024e4de2b1c474dd558bef593ddbf0bfade152",
-  "csslib_rev": "6338de25a09d098a62c9a1992c175e9ceb5b994a",
+  "csslib_rev": "6f35da3d93eb56eb25925779d235858d4090ce6f",
 
   # Note: Updates to dart_style have to be coordinated with the infrastructure
   # team so that the internal formatter `tools/sdks/dart-sdk/bin/dart format`
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 e95acec..6207ad4 100644
--- a/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart
@@ -6471,9 +6471,9 @@
         Message Function(
             String name, String name2, String string3)>(
     problemMessageTemplate:
-        r"""JS interop class '#name' conflicts with natively supported class '#name2' in '#string3'.""",
+        r"""Non-static JS interop class '#name' conflicts with natively supported class '#name2' in '#string3'.""",
     correctionMessageTemplate:
-        r"""Try making the @JS class into an @anonymous class or use js_util on the JS object.""",
+        r"""Try replacing it with a static JS interop class using `@staticInterop` with extension methods, or use js_util to interact with the native object of type '#name2'.""",
     withArguments: _withArgumentsJsInteropNativeClassInAnnotation);
 
 // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
@@ -6493,8 +6493,8 @@
   if (string3.isEmpty) throw 'No string provided';
   return new Message(codeJsInteropNativeClassInAnnotation,
       problemMessage:
-          """JS interop class '${name}' conflicts with natively supported class '${name2}' in '${string3}'.""",
-      correctionMessage: """Try making the @JS class into an @anonymous class or use js_util on the JS object.""",
+          """Non-static JS interop class '${name}' conflicts with natively supported class '${name2}' in '${string3}'.""",
+      correctionMessage: """Try replacing it with a static JS interop class using `@staticInterop` with extension methods, or use js_util to interact with the native object of type '${name2}'.""",
       arguments: {'name': name, 'name2': name2, 'string3': string3});
 }
 
@@ -6521,6 +6521,66 @@
     correctionMessage: r"""Try annotating the member with `external`.""");
 
 // DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Template<Message Function(String name)>
+    templateJsInteropStaticInteropWithInstanceMembers =
+    const Template<Message Function(String name)>(
+        problemMessageTemplate:
+            r"""JS interop class '#name' with `@staticInterop` annotation cannot declare instance members.""",
+        correctionMessageTemplate:
+            r"""Try moving the instance member to a static extension.""",
+        withArguments: _withArgumentsJsInteropStaticInteropWithInstanceMembers);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Code<Message Function(String name)>
+    codeJsInteropStaticInteropWithInstanceMembers =
+    const Code<Message Function(String name)>(
+  "JsInteropStaticInteropWithInstanceMembers",
+);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+Message _withArgumentsJsInteropStaticInteropWithInstanceMembers(String name) {
+  if (name.isEmpty) throw 'No name provided';
+  name = demangleMixinApplicationName(name);
+  return new Message(codeJsInteropStaticInteropWithInstanceMembers,
+      problemMessage:
+          """JS interop class '${name}' with `@staticInterop` annotation cannot declare instance members.""",
+      correctionMessage: """Try moving the instance member to a static extension.""",
+      arguments: {'name': name});
+}
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Template<Message Function(String name, String name2)>
+    templateJsInteropStaticInteropWithNonStaticSupertype =
+    const Template<Message Function(String name, String name2)>(
+        problemMessageTemplate:
+            r"""JS interop class '#name' has an `@staticInterop` annotation, but has supertype '#name2', which is non-static.""",
+        correctionMessageTemplate:
+            r"""Try marking the supertype as a static interop class using `@staticInterop`.""",
+        withArguments:
+            _withArgumentsJsInteropStaticInteropWithNonStaticSupertype);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+const Code<Message Function(String name, String name2)>
+    codeJsInteropStaticInteropWithNonStaticSupertype =
+    const Code<Message Function(String name, String name2)>(
+  "JsInteropStaticInteropWithNonStaticSupertype",
+);
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
+Message _withArgumentsJsInteropStaticInteropWithNonStaticSupertype(
+    String name, String name2) {
+  if (name.isEmpty) throw 'No name provided';
+  name = demangleMixinApplicationName(name);
+  if (name2.isEmpty) throw 'No name provided';
+  name2 = demangleMixinApplicationName(name2);
+  return new Message(codeJsInteropStaticInteropWithNonStaticSupertype,
+      problemMessage:
+          """JS interop class '${name}' has an `@staticInterop` annotation, but has supertype '${name2}', which is non-static.""",
+      correctionMessage: """Try marking the supertype as a static interop class using `@staticInterop`.""",
+      arguments: {'name': name, 'name2': name2});
+}
+
+// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
 const Template<
     Message Function(String name)> templateLabelNotFound = const Template<
         Message Function(String name)>(
diff --git a/pkg/_js_interop_checks/lib/js_interop_checks.dart b/pkg/_js_interop_checks/lib/js_interop_checks.dart
index 975fca7..24937db 100644
--- a/pkg/_js_interop_checks/lib/js_interop_checks.dart
+++ b/pkg/_js_interop_checks/lib/js_interop_checks.dart
@@ -19,6 +19,8 @@
         messageJsInteropNonExternalConstructor,
         messageJsInteropNonExternalMember,
         templateJsInteropDartClassExtendsJSClass,
+        templateJsInteropStaticInteropWithInstanceMembers,
+        templateJsInteropStaticInteropWithNonStaticSupertype,
         templateJsInteropJSClassExtendsDartClass,
         templateJsInteropNativeClassInAnnotation;
 
@@ -30,6 +32,7 @@
   final Map<String, Class> _nativeClasses;
   bool _classHasJSAnnotation = false;
   bool _classHasAnonymousAnnotation = false;
+  bool _classHasStaticInteropAnnotation = false;
   bool _libraryHasJSAnnotation = false;
   Map<Reference, Extension>? _libraryExtensionsIndex;
 
@@ -99,6 +102,7 @@
   void visitClass(Class cls) {
     _classHasJSAnnotation = hasJSInteropAnnotation(cls);
     _classHasAnonymousAnnotation = hasAnonymousAnnotation(cls);
+    _classHasStaticInteropAnnotation = hasStaticInteropAnnotation(cls);
     var superclass = cls.superclass;
     if (superclass != null && superclass != _coreTypes.objectClass) {
       var superHasJSAnnotation = hasJSInteropAnnotation(superclass);
@@ -116,11 +120,35 @@
             cls.fileOffset,
             cls.name.length,
             cls.fileUri);
+      } else if (_classHasStaticInteropAnnotation) {
+        if (!hasStaticInteropAnnotation(superclass)) {
+          _diagnosticsReporter.report(
+              templateJsInteropStaticInteropWithNonStaticSupertype
+                  .withArguments(cls.name, superclass.name),
+              cls.fileOffset,
+              cls.name.length,
+              cls.fileUri);
+        }
+      }
+    }
+    // Validate that superinterfaces are all annotated as static as well. Note
+    // that mixins are already disallowed and therefore are not checked here.
+    if (_classHasStaticInteropAnnotation) {
+      for (var supertype in cls.implementedTypes) {
+        if (!hasStaticInteropAnnotation(supertype.classNode)) {
+          _diagnosticsReporter.report(
+              templateJsInteropStaticInteropWithNonStaticSupertype
+                  .withArguments(cls.name, supertype.classNode.name),
+              cls.fileOffset,
+              cls.name.length,
+              cls.fileUri);
+        }
       }
     }
     // Since this is a breaking check, it is language-versioned.
     if (cls.enclosingLibrary.languageVersion >= Version(2, 13) &&
         _classHasJSAnnotation &&
+        !_classHasStaticInteropAnnotation &&
         !_classHasAnonymousAnnotation &&
         _libraryIsGlobalNamespace) {
       var jsClass = getJSName(cls);
@@ -221,6 +249,30 @@
         _checkNoNamedParameters(procedure.function);
       }
     }
+
+    if (_classHasStaticInteropAnnotation &&
+        procedure.isInstanceMember &&
+        !procedure.isFactory) {
+      _diagnosticsReporter.report(
+          templateJsInteropStaticInteropWithInstanceMembers
+              .withArguments(procedure.enclosingClass!.name),
+          procedure.fileOffset,
+          procedure.name.text.length,
+          procedure.fileUri);
+    }
+  }
+
+  @override
+  void visitField(Field field) {
+    if (_classHasStaticInteropAnnotation && field.isInstanceMember) {
+      _diagnosticsReporter.report(
+          templateJsInteropStaticInteropWithInstanceMembers
+              .withArguments(field.enclosingClass!.name),
+          field.fileOffset,
+          field.name.text.length,
+          field.fileUri);
+    }
+    super.visitField(field);
   }
 
   @override
diff --git a/pkg/_js_interop_checks/lib/src/js_interop.dart b/pkg/_js_interop_checks/lib/src/js_interop.dart
index 60a8dfd..26c0c7d 100644
--- a/pkg/_js_interop_checks/lib/src/js_interop.dart
+++ b/pkg/_js_interop_checks/lib/src/js_interop.dart
@@ -9,11 +9,16 @@
 bool hasJSInteropAnnotation(Annotatable a) =>
     a.annotations.any(_isPublicJSAnnotation);
 
-/// Returns true iff the node has an `@anonymous(...)` annotation from
-/// `package:js` or from the internal `dart:_js_annotations`.
+/// Returns true iff the node has an `@anonymous` annotation from `package:js`
+/// or from the internal `dart:_js_annotations`.
 bool hasAnonymousAnnotation(Annotatable a) =>
     a.annotations.any(_isAnonymousAnnotation);
 
+/// Returns true iff the node has an `@staticInterop` annotation from
+/// `package:js` or from the internal `dart:_js_annotations`.
+bool hasStaticInteropAnnotation(Annotatable a) =>
+    a.annotations.any(_isStaticInteropAnnotation);
+
 /// If [a] has a `@JS('...')` annotation, returns the value inside the
 /// parentheses.
 ///
@@ -52,26 +57,26 @@
 final _internalJs = Uri.parse('dart:_js_annotations');
 final _jsHelper = Uri.parse('dart:_js_helper');
 
-/// Returns true if [value] is the `JS` annotation from `package:js` or from
-/// `dart:_js_annotations`.
-bool _isPublicJSAnnotation(Expression value) {
+/// Returns true if [value] is the interop annotation whose class is
+/// [annotationClassName] from `package:js` or from `dart:_js_annotations`.
+bool _isInteropAnnotation(Expression value, String annotationClassName) {
   var c = _annotationClass(value);
   return c != null &&
-      c.name == 'JS' &&
+      c.name == annotationClassName &&
       (c.enclosingLibrary.importUri == _packageJs ||
           c.enclosingLibrary.importUri == _internalJs);
 }
 
-/// Returns true if [value] is the `anyonymous` annotation from `package:js` or
-/// from `dart:_js_annotations`.
-bool _isAnonymousAnnotation(Expression value) {
-  var c = _annotationClass(value);
-  return c != null &&
-      c.name == '_Anonymous' &&
-      (c.enclosingLibrary.importUri == _packageJs ||
-          c.enclosingLibrary.importUri == _internalJs);
-}
+bool _isPublicJSAnnotation(Expression value) =>
+    _isInteropAnnotation(value, 'JS');
 
+bool _isAnonymousAnnotation(Expression value) =>
+    _isInteropAnnotation(value, '_Anonymous');
+
+bool _isStaticInteropAnnotation(Expression value) =>
+    _isInteropAnnotation(value, '_StaticInterop');
+
+/// Returns true if [value] is the `Native` annotation from `dart:_js_helper`.
 bool _isNativeAnnotation(Expression value) {
   var c = _annotationClass(value);
   return c != null &&
@@ -85,6 +90,7 @@
 ///
 /// - `@JS()` would return the "JS" class in "package:js".
 /// - `@anonymous` would return the "_Anonymous" class in "package:js".
+/// - `@staticInterop` would return the "_StaticInterop" class in "package:js".
 /// - `@Native` would return the "Native" class in "dart:_js_helper".
 ///
 /// This function works regardless of whether the CFE is evaluating constants,
diff --git a/pkg/analysis_server/test/analysis/get_hover_test.dart b/pkg/analysis_server/test/analysis/get_hover_test.dart
index 927950d..576895b 100644
--- a/pkg/analysis_server/test/analysis/get_hover_test.dart
+++ b/pkg/analysis_server/test/analysis/get_hover_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analysis_server/protocol/protocol_generated.dart';
+import 'package:analysis_server/src/protocol_server.dart';
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
@@ -248,6 +249,110 @@
     }
   }
 
+  Future<void> test_constructorReference_named() async {
+    addTestFile('''
+class A<T> {
+  /// doc aaa
+  /// doc bbb
+  A.named();
+}
+
+void f() {
+  A<double>.named;
+}
+''');
+    var hover = await prepareHover('named;');
+    // element
+    expect(hover.containingLibraryName, 'bin/test.dart');
+    expect(hover.containingLibraryPath, testFile);
+    expect(hover.containingClassDescription, 'A');
+    expect(hover.dartdoc, 'doc aaa\ndoc bbb');
+    expect(hover.elementDescription, 'A<double> A.named()');
+    expect(hover.elementKind, 'constructor');
+    // types
+    expect(hover.staticType, isNull);
+    expect(hover.propagatedType, isNull);
+    // no parameter
+    expect(hover.parameter, isNull);
+  }
+
+  Future<void> test_constructorReference_unnamed_declared() async {
+    addTestFile('''
+class A<T> {
+  /// doc aaa
+  /// doc bbb
+  A();
+}
+
+void f() {
+  A<double>.new;
+}
+''');
+    var hover = await prepareHover('new;');
+    // element
+    expect(hover.containingLibraryName, 'bin/test.dart');
+    expect(hover.containingLibraryPath, testFile);
+    expect(hover.containingClassDescription, 'A');
+    expect(hover.dartdoc, 'doc aaa\ndoc bbb');
+    expect(hover.elementDescription, 'A<double> A()');
+    expect(hover.elementKind, 'constructor');
+    // types
+    expect(hover.staticType, isNull);
+    expect(hover.propagatedType, isNull);
+    // no parameter
+    expect(hover.parameter, isNull);
+  }
+
+  Future<void> test_constructorReference_unnamed_declared_new() async {
+    addTestFile('''
+class A<T> {
+  /// doc aaa
+  /// doc bbb
+  A.new();
+}
+
+void f() {
+  A<double>.new;
+}
+''');
+    var hover = await prepareHover('new;');
+    // element
+    expect(hover.containingLibraryName, 'bin/test.dart');
+    expect(hover.containingLibraryPath, testFile);
+    expect(hover.containingClassDescription, 'A');
+    expect(hover.dartdoc, 'doc aaa\ndoc bbb');
+    expect(hover.elementDescription, 'A<double> A()');
+    expect(hover.elementKind, 'constructor');
+    // types
+    expect(hover.staticType, isNull);
+    expect(hover.propagatedType, isNull);
+    // no parameter
+    expect(hover.parameter, isNull);
+  }
+
+  Future<void> test_constructorReference_unnamed_synthetic() async {
+    addTestFile('''
+class A<T> {}
+
+void f() {
+  A<double>.new;
+}
+''');
+    var hover = await prepareHover('new;');
+    // element
+    expect(hover.containingLibraryName, 'bin/test.dart');
+    expect(hover.containingLibraryPath, testFile);
+    expect(hover.containingClassDescription, 'A');
+    expect(hover.dartdoc, isNull);
+    expect(hover.elementDescription, 'A<double> A()');
+    expect(hover.elementKind, 'constructor');
+    // types
+    expect(hover.staticType, isNull);
+    expect(hover.propagatedType, isNull);
+    // no parameter
+    expect(hover.parameter, isNull);
+  }
+
   Future<void> test_dartdoc_block() async {
     addTestFile('''
 /**
@@ -402,6 +507,85 @@
     expect(hover.parameter, isNull);
   }
 
+  Future<void> test_functionReference_classMethod_instance() async {
+    addTestFile('''
+class A<T> {
+  /// doc aaa
+  /// doc bbb
+  int foo<U>(T t, U u) => 0;
+}
+
+void f(A<int> a) {
+  a.foo<double>;
+}
+''');
+    var hover = await prepareHover('foo<double>');
+    // element
+    expect(hover.containingLibraryName, 'bin/test.dart');
+    expect(hover.containingLibraryPath, testFile);
+    expect(hover.containingClassDescription, 'A');
+    expect(hover.dartdoc, '''doc aaa\ndoc bbb''');
+    expect(hover.elementDescription, 'int foo<U>(int t, U u)');
+    expect(hover.elementKind, 'method');
+    // types
+    expect(hover.staticType, isNull);
+    expect(hover.propagatedType, isNull);
+    // no parameter
+    expect(hover.parameter, isNull);
+  }
+
+  Future<void> test_functionReference_classMethod_static() async {
+    addTestFile('''
+class A<T> {
+  /// doc aaa
+  /// doc bbb
+  static int foo<U>(U u) => 0;
+}
+
+void f() {
+  A.foo<double>;
+}
+''');
+    var hover = await prepareHover('foo<double>');
+    // element
+    expect(hover.containingLibraryName, 'bin/test.dart');
+    expect(hover.containingLibraryPath, testFile);
+    expect(hover.containingClassDescription, 'A');
+    expect(hover.dartdoc, '''doc aaa\ndoc bbb''');
+    expect(hover.elementDescription, 'int foo<U>(U u)');
+    expect(hover.elementKind, 'method');
+    // types
+    expect(hover.staticType, isNull);
+    expect(hover.propagatedType, isNull);
+    // no parameter
+    expect(hover.parameter, isNull);
+  }
+
+  Future<void> test_functionReference_topLevelFunction() async {
+    addTestFile('''
+/// doc aaa
+/// doc bbb
+int foo<T>(T a) => 0;
+
+void f() {
+  foo<double>;
+}
+''');
+    var hover = await prepareHover('foo<double>');
+    // element
+    expect(hover.containingLibraryName, 'bin/test.dart');
+    expect(hover.containingLibraryPath, testFile);
+    expect(hover.containingClassDescription, isNull);
+    expect(hover.dartdoc, '''doc aaa\ndoc bbb''');
+    expect(hover.elementDescription, 'int foo<T>(T a)');
+    expect(hover.elementKind, 'function');
+    // types
+    expect(hover.staticType, isNull);
+    expect(hover.propagatedType, isNull);
+    // no parameter
+    expect(hover.parameter, isNull);
+  }
+
   Future<void> test_getter_synthetic() async {
     addTestFile('''
 library my.library;
diff --git a/pkg/analysis_server/test/services/completion/dart/imported_reference_contributor_test.dart b/pkg/analysis_server/test/services/completion/dart/imported_reference_contributor_test.dart
index 5148c7d..05f16bd 100644
--- a/pkg/analysis_server/test/services/completion/dart/imported_reference_contributor_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/imported_reference_contributor_test.dart
@@ -4771,6 +4771,24 @@
     assertNotSuggested('C2');
   }
 
+  Future<void> test_TypeArgumentList_functionReference() async {
+    addTestSource('''
+class A {}
+
+void foo<T>() {}
+
+void f() {
+  foo<^>;
+}
+''');
+    await computeSuggestions();
+
+    expect(replacementOffset, completionOffset);
+    expect(replacementLength, 0);
+    assertSuggestClass('Object');
+    assertNotSuggested('A');
+  }
+
   Future<void> test_TypeArgumentList_recursive() async {
     resolveSource('/home/test/lib/a.dart', '''
 class A {}
diff --git a/pkg/analysis_server/test/services/completion/dart/local_reference_contributor_test.dart b/pkg/analysis_server/test/services/completion/dart/local_reference_contributor_test.dart
index 678619f..243cdb5 100644
--- a/pkg/analysis_server/test/services/completion/dart/local_reference_contributor_test.dart
+++ b/pkg/analysis_server/test/services/completion/dart/local_reference_contributor_test.dart
@@ -6148,6 +6148,24 @@
     assertSuggestClass('C2');
   }
 
+  Future<void> test_TypeArgumentList_functionReference() async {
+    addTestSource('''
+class A {}
+
+void foo<T>() {}
+
+void f() {
+  foo<^>;
+}
+''');
+    await computeSuggestions();
+
+    expect(replacementOffset, completionOffset);
+    expect(replacementLength, 0);
+    assertSuggestClass('A');
+    assertNotSuggested('Object');
+  }
+
   Future<void> test_TypeParameter_classDeclaration() async {
     addTestSource('''
 class A<T> {
diff --git a/pkg/analysis_server/test/src/cider/fixes_test.dart b/pkg/analysis_server/test/src/cider/fixes_test.dart
index ff775e5..63179b7 100644
--- a/pkg/analysis_server/test/src/cider/fixes_test.dart
+++ b/pkg/analysis_server/test/src/cider/fixes_test.dart
@@ -73,11 +73,10 @@
   }
 
   Future<void> test_importLibrary_withClass() async {
-    var a_path = '/workspace/dart/test/lib/a.dart';
-    newFile(a_path, content: r'''
+    var a = newFile('/workspace/dart/test/lib/a.dart', content: r'''
 class Test {}
 ''');
-    fileResolver.resolve(path: a_path);
+    fileResolver.resolve(path: a.path);
 
     await _compute(r'''
 void f(Test a) {}^
@@ -91,11 +90,10 @@
   }
 
   Future<void> test_importLibrary_withEnum() async {
-    var a_path = '/workspace/dart/test/lib/a.dart';
-    newFile(a_path, content: r'''
+    var a = newFile('/workspace/dart/test/lib/a.dart', content: r'''
 enum Test {a, b, c}
 ''');
-    fileResolver.resolve(path: a_path);
+    fileResolver.resolve(path: a.path);
 
     await _compute(r'''
 void f(Test a) {}^
@@ -109,13 +107,12 @@
   }
 
   Future<void> test_importLibrary_withExtension() async {
-    var a_path = '/workspace/dart/test/lib/a.dart';
-    newFile(a_path, content: r'''
+    var a = newFile('/workspace/dart/test/lib/a.dart', content: r'''
 extension E on int {
   void foo() {}
 }
 ''');
-    fileResolver.resolve(path: a_path);
+    fileResolver.resolve(path: a.path);
 
     await _compute(r'''
 void f() {
@@ -133,11 +130,10 @@
   }
 
   Future<void> test_importLibrary_withFunction() async {
-    var a_path = '/workspace/dart/test/lib/a.dart';
-    newFile(a_path, content: r'''
+    var a = newFile('/workspace/dart/test/lib/a.dart', content: r'''
 void foo() {}
 ''');
-    fileResolver.resolve(path: a_path);
+    fileResolver.resolve(path: a.path);
 
     await _compute(r'''
 void f() {
@@ -155,11 +151,10 @@
   }
 
   Future<void> test_importLibrary_withMixin() async {
-    var a_path = '/workspace/dart/test/lib/a.dart';
-    newFile(a_path, content: r'''
+    var a = newFile('/workspace/dart/test/lib/a.dart', content: r'''
 mixin Test {}
 ''');
-    fileResolver.resolve(path: a_path);
+    fileResolver.resolve(path: a.path);
 
     await _compute(r'''
 void f(Test a) {}^
@@ -173,11 +168,10 @@
   }
 
   Future<void> test_importLibrary_withTopLevelVariable() async {
-    var a_path = '/workspace/dart/test/lib/a.dart';
-    newFile(a_path, content: r'''
+    var a = newFile('/workspace/dart/test/lib/a.dart', content: r'''
 var a = 0;
 ''');
-    fileResolver.resolve(path: a_path);
+    fileResolver.resolve(path: a.path);
 
     await _compute(r'''
 void f() {
diff --git a/pkg/analyzer/lib/src/dart/constant/evaluation.dart b/pkg/analyzer/lib/src/dart/constant/evaluation.dart
index 70f5d10..a2e14e7 100644
--- a/pkg/analyzer/lib/src/dart/constant/evaluation.dart
+++ b/pkg/analyzer/lib/src/dart/constant/evaluation.dart
@@ -867,8 +867,8 @@
         return null;
       }
     }
-    // validate prefixed identifier
-    return _getConstantValue(node, node.staticElement);
+    // Validate prefixed identifier.
+    return _getConstantValue(node, node.identifier);
   }
 
   @override
@@ -902,7 +902,7 @@
         return prefixResult.stringLength(typeSystem);
       }
     }
-    return _getConstantValue(node, node.propertyName.staticElement);
+    return _getConstantValue(node, node.propertyName);
   }
 
   @override
@@ -971,7 +971,7 @@
       return _instantiateFunctionType(node, value);
     }
 
-    return _getConstantValue(node, node.staticElement);
+    return _getConstantValue(node, node);
   }
 
   @override
@@ -1171,9 +1171,11 @@
   }
 
   /// Return the constant value of the static constant represented by the given
-  /// [element]. The [node] is the node to be used if an error needs to be
+  /// [identifier]. The [node] is the node to be used if an error needs to be
   /// reported.
-  DartObjectImpl? _getConstantValue(Expression node, Element? element) {
+  DartObjectImpl? _getConstantValue(
+      Expression node, SimpleIdentifier identifier) {
+    var element = identifier.staticElement;
     element = element?.declaration;
     var variableElement =
         element is PropertyAccessorElement ? element.variable : element;
@@ -1196,7 +1198,7 @@
         if (value == null) {
           return value;
         }
-        return _instantiateFunctionType(node, value);
+        return _instantiateFunctionType(identifier, value);
       }
     } else if (variableElement is ConstructorElement) {
       return DartObjectImpl(
@@ -1207,11 +1209,12 @@
     } else if (variableElement is ExecutableElement) {
       var function = element as ExecutableElement;
       if (function.isStatic) {
-        return DartObjectImpl(
+        var rawType = DartObjectImpl(
           typeSystem,
-          node.typeOrThrow,
+          function.type,
           FunctionState(function),
         );
+        return _instantiateFunctionType(identifier, rawType);
       }
     } else if (variableElement is ClassElement) {
       var type = variableElement.instantiate(
@@ -1264,10 +1267,7 @@
   /// type-instantiated with those [node]'s tear-off type argument types,
   /// otherwise returns [value].
   DartObjectImpl? _instantiateFunctionType(
-      Expression node, DartObjectImpl value) {
-    if (node is! SimpleIdentifier) {
-      return value;
-    }
+      SimpleIdentifier node, DartObjectImpl value) {
     var functionElement = value.toFunctionValue();
     if (functionElement is! ExecutableElement) {
       return value;
diff --git a/pkg/analyzer/lib/src/dart/constant/potentially_constant.dart b/pkg/analyzer/lib/src/dart/constant/potentially_constant.dart
index 6f0eed7..aaa8ef5 100644
--- a/pkg/analyzer/lib/src/dart/constant/potentially_constant.dart
+++ b/pkg/analyzer/lib/src/dart/constant/potentially_constant.dart
@@ -94,7 +94,7 @@
     }
 
     if (node is TypedLiteral) {
-      return _typeLiteral(node);
+      return _typedLiteral(node);
     }
 
     if (node is ParenthesizedExpression) {
@@ -187,6 +187,22 @@
       return;
     }
 
+    if (node is ConstructorReference) {
+      _typeArgumentList(node.constructorName.type2.typeArguments);
+      return;
+    }
+
+    if (node is FunctionReference) {
+      _typeArgumentList(node.typeArguments);
+      collect(node.function);
+      return;
+    }
+
+    if (node is TypeLiteral) {
+      _typeArgumentList(node.type.typeArguments);
+      return;
+    }
+
     nodes.add(node);
   }
 
@@ -261,6 +277,7 @@
         return;
       }
     }
+    // TODO(srawlins): collect type arguments.
     nodes.add(node);
   }
 
@@ -292,7 +309,18 @@
     nodes.add(node);
   }
 
-  void _typeLiteral(TypedLiteral node) {
+  void _typeArgumentList(TypeArgumentList? typeArgumentList) {
+    var typeArguments = typeArgumentList?.arguments;
+    if (typeArguments != null) {
+      for (var typeArgument in typeArguments) {
+        if (!isPotentiallyConstantTypeExpression(typeArgument)) {
+          nodes.add(typeArgument);
+        }
+      }
+    }
+  }
+
+  void _typedLiteral(TypedLiteral node) {
     if (!node.isConst) {
       nodes.add(node);
       return;
@@ -302,6 +330,8 @@
       var typeArguments = node.typeArguments?.arguments;
       if (typeArguments != null && typeArguments.length == 1) {
         var elementType = typeArguments[0];
+        // TODO(srawlins): Change to "potentially" constant type expression as
+        // per https://github.com/dart-lang/sdk/issues/47302?
         if (!isConstantTypeExpression(elementType)) {
           nodes.add(elementType);
         }
@@ -317,6 +347,8 @@
       var typeArguments = node.typeArguments?.arguments;
       if (typeArguments != null && typeArguments.length == 1) {
         var elementType = typeArguments[0];
+        // TODO(srawlins): Change to "potentially" constant type expression as
+        // per https://github.com/dart-lang/sdk/issues/47302?
         if (!isConstantTypeExpression(elementType)) {
           nodes.add(elementType);
         }
@@ -350,7 +382,7 @@
 
   _ConstantTypeChecker({required this.potentially});
 
-  /// Return `true` if the [node] is a constant type expression.
+  /// Return `true` if the [node] is a (potentially) constant type expression.
   bool check(TypeAnnotation? node) {
     if (potentially) {
       if (node is NamedType) {
diff --git a/pkg/analyzer/test/src/dart/constant/evaluation_test.dart b/pkg/analyzer/test/src/dart/constant/evaluation_test.dart
index 9044238..c2ad9fe 100644
--- a/pkg/analyzer/test/src/dart/constant/evaluation_test.dart
+++ b/pkg/analyzer/test/src/dart/constant/evaluation_test.dart
@@ -566,7 +566,48 @@
     var result = _evaluateConstant('g');
     assertType(result.type, 'void Function(int)');
     assertElement(result.toFunctionValue(), findElement.topFunction('f'));
-    _assertTypeArguments(result, null);
+    _assertTypeArguments(result, ['int']);
+  }
+
+  test_visitPrefixedIdentifier_genericFunction_instantiatedNonIdentifier() async {
+    await resolveTestCode('''
+void f<T>(T a) {}
+const b = false;
+const g1 = f;
+const g2 = f;
+const void Function(int) h = b ? g1 : g2;
+''');
+    var result = _evaluateConstant('h');
+    assertType(result.type, 'void Function(int)');
+    assertElement(result.toFunctionValue(), findElement.topFunction('f'));
+    _assertTypeArguments(result, ['int']);
+  }
+
+  test_visitPrefixedIdentifier_genericFunction_instantiatedPrefixed() async {
+    await resolveTestCode('''
+import '' as self;
+void f<T>(T a) {}
+const g = f;
+const void Function(int) h = self.g;
+''');
+    var result = _evaluateConstant('h');
+    assertType(result.type, 'void Function(int)');
+    assertElement(result.toFunctionValue(), findElement.topFunction('f'));
+    _assertTypeArguments(result, ['int']);
+  }
+
+  test_visitPropertyAccess_genericFunction_instantiated() async {
+    await resolveTestCode('''
+import '' as self;
+class C {
+  static void f<T>(T a) {}
+}
+const void Function(int) g = self.C.f;
+''');
+    var result = _evaluateConstant('g');
+    assertType(result.type, 'void Function(int)');
+    assertElement(result.toFunctionValue(), findElement.method('f'));
+    _assertTypeArguments(result, ['int']);
   }
 
   test_visitSimpleIdentifier_className() async {
@@ -587,6 +628,17 @@
     var result = _evaluateConstant('g');
     assertType(result.type, 'void Function(int)');
     assertElement(result.toFunctionValue(), findElement.topFunction('f'));
+    _assertTypeArguments(result, ['int']);
+  }
+
+  test_visitSimpleIdentifier_genericFunction_nonGeneric() async {
+    await resolveTestCode('''
+void f(int a) {}
+const void Function(int) g = f;
+''');
+    var result = _evaluateConstant('g');
+    assertType(result.type, 'void Function(int)');
+    assertElement(result.toFunctionValue(), findElement.topFunction('f'));
     _assertTypeArguments(result, null);
   }
 
diff --git a/pkg/analyzer/test/src/dart/constant/potentially_constant_test.dart b/pkg/analyzer/test/src/dart/constant/potentially_constant_test.dart
index 0d881c1..fc1371d 100644
--- a/pkg/analyzer/test/src/dart/constant/potentially_constant_test.dart
+++ b/pkg/analyzer/test/src/dart/constant/potentially_constant_test.dart
@@ -458,6 +458,50 @@
             ]);
   }
 
+  test_constructorReference_explicitTypeArguments() async {
+    await _assertConst('''
+class A {
+  final B Function() x;
+  const A(): x = B<int>.new;
+}
+
+class B<T> {}
+''', () => findNode.constructorReference('B<int>.new'));
+  }
+
+  test_constructorReference_noTypeArguments() async {
+    await _assertConst('''
+class A {
+  final B Function() x;
+  const A(): x = B.new;
+}
+
+class B {}
+''', () => findNode.constructorReference('B.new'));
+  }
+
+  test_functionReference_explicitTypeArguments() async {
+    await _assertConst('''
+class A {
+  final int Function(int) x;
+  const A(): x = id<int>;
+}
+
+X id<X>(X x) => x;
+''', () => findNode.functionReference('id<int>'));
+  }
+
+  test_functionReference_noTypeArguments() async {
+    await _assertConst('''
+class A {
+  final int Function(int) x;
+  const A(): x = id;
+}
+
+X id<X>(X x) => x;
+''', () => findNode.simple('id;'));
+  }
+
   test_ifElement_then() async {
     await _assertConst(r'''
 const a = 0;
@@ -1177,6 +1221,15 @@
 ''', () => _xInitializer());
   }
 
+  test_typeLiteral() async {
+    await _assertConst('''
+class A {
+  Type x;
+  const A(): x = List<int>;
+}
+''', () => findNode.typeLiteral('List<int>'));
+  }
+
   _assertConst(String code, AstNode Function() getNode) async {
     await resolveTestCode(code);
     var node = getNode();
diff --git a/pkg/analyzer/test/src/fasta/message_coverage_test.dart b/pkg/analyzer/test/src/fasta/message_coverage_test.dart
index f21e632..726ed9d 100644
--- a/pkg/analyzer/test/src/fasta/message_coverage_test.dart
+++ b/pkg/analyzer/test/src/fasta/message_coverage_test.dart
@@ -10,8 +10,8 @@
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
-import 'package:yaml/yaml.dart';
 
+import '../../../tool/messages/error_code_info.dart';
 import '../../generated/parser_test_base.dart';
 
 main() {
@@ -35,45 +35,25 @@
     return visitor.generatedNames;
   }
 
-  /// Given the path to the file 'messages.yaml', return a list of the top-level
-  /// keys defined in that file that define an 'analyzerCode'.
-  List<String> getMappedCodes(String messagesPath) {
-    String content = io.File(messagesPath).readAsStringSync();
-    YamlDocument document = loadYamlDocument(content);
-    expect(document, isNotNull);
+  /// Return a list of the front end messages that define an 'analyzerCode'.
+  List<String> getMappedCodes() {
     Set<String> codes = <String>{};
-    YamlNode contents = document.contents;
-    if (contents is YamlMap) {
-      for (String name in contents.keys) {
-        Object value = contents[name];
-        if (value is YamlMap) {
-          if (value['analyzerCode'] != null) {
-            codes.add(name);
-          }
-        }
+    for (var entry in frontEndMessages.entries) {
+      var name = entry.key;
+      var errorCodeInfo = entry.value;
+      if (errorCodeInfo.analyzerCode.isNotEmpty) {
+        codes.add(name);
       }
     }
     return codes.toList();
   }
 
-  /// Given the path to the file 'messages.yaml', return a list of the analyzer
-  /// codes defined in that file.
-  List<String> getReferencedCodes(String messagesPath) {
-    String content = io.File(messagesPath).readAsStringSync();
-    YamlDocument document = loadYamlDocument(content);
-    expect(document, isNotNull);
+  /// Return a list of the analyzer codes defined in the front end's
+  /// `messages.yaml` file.
+  List<String> getReferencedCodes() {
     Set<String> codes = <String>{};
-    YamlNode contents = document.contents;
-    if (contents is YamlMap) {
-      for (String name in contents.keys) {
-        Object value = contents[name];
-        if (value is YamlMap) {
-          var code = value['analyzerCode']?.toString();
-          if (code != null) {
-            codes.add(code);
-          }
-        }
-      }
+    for (var errorCodeInfo in frontEndMessages.values) {
+      codes.addAll(errorCodeInfo.analyzerCode);
     }
     return codes.toList();
   }
@@ -109,8 +89,7 @@
         path.join(frontEndPath, 'lib', 'src', 'fasta', 'parser', 'parser.dart');
     Set<String> generatedNames = getGeneratedNames(parserPath);
 
-    String messagesPath = path.join(frontEndPath, 'messages.yaml');
-    List<String> mappedCodes = getMappedCodes(messagesPath);
+    List<String> mappedCodes = getMappedCodes();
 
     generatedNames.removeAll(mappedCodes);
     if (generatedNames.isEmpty) {
@@ -133,9 +112,7 @@
         path.join(analyzerPath, 'lib', 'src', 'fasta', 'error_converter.dart');
     List<String> translatedCodes = getTranslatedCodes(astBuilderPath);
 
-    String messagesPath =
-        path.join(path.dirname(analyzerPath), 'front_end', 'messages.yaml');
-    List<String> referencedCodes = getReferencedCodes(messagesPath);
+    List<String> referencedCodes = getReferencedCodes();
 
     List<String> untranslated = <String>[];
     for (String referencedCode in referencedCodes) {
diff --git a/pkg/analyzer/tool/messages/error_code_info.dart b/pkg/analyzer/tool/messages/error_code_info.dart
index 2097815..9a81b54 100644
--- a/pkg/analyzer/tool/messages/error_code_info.dart
+++ b/pkg/analyzer/tool/messages/error_code_info.dart
@@ -3,6 +3,96 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:convert';
+import 'dart:io';
+
+import 'package:analyzer_utilities/package_root.dart' as pkg_root;
+import 'package:path/path.dart';
+import 'package:yaml/yaml.dart' show loadYaml;
+
+/// Information about all the classes derived from `ErrorCode` that are code
+/// generated based on the contents of the analyzer and front end
+/// `messages.yaml` files.
+const List<ErrorClassInfo> errorClasses = [
+  ErrorClassInfo(
+      filePath: 'lib/src/analysis_options/error/option_codes.g.dart',
+      name: 'AnalysisOptionsErrorCode',
+      type: 'COMPILE_TIME_ERROR',
+      severity: 'ERROR'),
+  ErrorClassInfo(
+      filePath: 'lib/src/analysis_options/error/option_codes.g.dart',
+      name: 'AnalysisOptionsHintCode',
+      type: 'HINT',
+      severity: 'INFO'),
+  ErrorClassInfo(
+      filePath: 'lib/src/analysis_options/error/option_codes.g.dart',
+      name: 'AnalysisOptionsWarningCode',
+      type: 'STATIC_WARNING',
+      severity: 'WARNING'),
+  ErrorClassInfo(
+      filePath: 'lib/src/error/codes.g.dart',
+      name: 'CompileTimeErrorCode',
+      superclass: 'AnalyzerErrorCode',
+      type: 'COMPILE_TIME_ERROR',
+      extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
+  ErrorClassInfo(
+      filePath: 'lib/src/error/codes.g.dart',
+      name: 'LanguageCode',
+      type: 'COMPILE_TIME_ERROR'),
+  ErrorClassInfo(
+      filePath: 'lib/src/error/codes.g.dart',
+      name: 'StaticWarningCode',
+      superclass: 'AnalyzerErrorCode',
+      type: 'STATIC_WARNING',
+      severity: 'WARNING',
+      extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
+  ErrorClassInfo(
+      filePath: 'lib/src/dart/error/ffi_code.g.dart',
+      name: 'FfiCode',
+      superclass: 'AnalyzerErrorCode',
+      type: 'COMPILE_TIME_ERROR',
+      extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
+  ErrorClassInfo(
+      filePath: 'lib/src/dart/error/hint_codes.g.dart',
+      name: 'HintCode',
+      superclass: 'AnalyzerErrorCode',
+      type: 'HINT',
+      extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
+  ErrorClassInfo(
+      filePath: 'lib/src/dart/error/syntactic_errors.g.dart',
+      name: 'ParserErrorCode',
+      type: 'SYNTACTIC_ERROR',
+      severity: 'ERROR',
+      includeCfeMessages: true),
+  ErrorClassInfo(
+      filePath: 'lib/src/manifest/manifest_warning_code.g.dart',
+      name: 'ManifestWarningCode',
+      type: 'STATIC_WARNING',
+      severity: 'WARNING'),
+  ErrorClassInfo(
+      filePath: 'lib/src/pubspec/pubspec_warning_code.g.dart',
+      name: 'PubspecWarningCode',
+      type: 'STATIC_WARNING',
+      severity: 'WARNING'),
+];
+
+/// Decoded messages from the analyzer's `messages.yaml` file.
+final Map<String, Map<String, ErrorCodeInfo>> analyzerMessages =
+    _loadAnalyzerMessages();
+
+/// The path to the `analyzer` package.
+final String analyzerPkgPath =
+    normalize(join(pkg_root.packageRoot, 'analyzer'));
+
+/// A set of tables mapping between front end and analyzer error codes.
+final CfeToAnalyzerErrorCodeTables cfeToAnalyzerErrorCodeTables =
+    CfeToAnalyzerErrorCodeTables._(frontEndMessages);
+
+/// Decoded messages from the front end's `messages.yaml` file.
+final Map<String, ErrorCodeInfo> frontEndMessages = _loadFrontEndMessages();
+
+/// The path to the `front_end` package.
+final String frontEndPkgPath =
+    normalize(join(pkg_root.packageRoot, 'front_end'));
 
 /// Decodes a YAML object (obtained from `pkg/analyzer/messages.yaml`) into a
 /// two-level map of [ErrorCodeInfo], indexed first by class name and then by
@@ -32,6 +122,20 @@
   return result;
 }
 
+/// Loads analyzer messages from the analyzer's `messages.yaml` file.
+Map<String, Map<String, ErrorCodeInfo>> _loadAnalyzerMessages() {
+  Map<dynamic, dynamic> messagesYaml =
+      loadYaml(File(join(analyzerPkgPath, 'messages.yaml')).readAsStringSync());
+  return decodeAnalyzerMessagesYaml(messagesYaml);
+}
+
+/// Loads front end messages from the front end's `messages.yaml` file.
+Map<String, ErrorCodeInfo> _loadFrontEndMessages() {
+  Map<dynamic, dynamic> messagesYaml =
+      loadYaml(File(join(frontEndPkgPath, 'messages.yaml')).readAsStringSync());
+  return decodeCfeMessagesYaml(messagesYaml);
+}
+
 /// Data tables mapping between CFE errors and their corresponding automatically
 /// generated analyzer errors.
 class CfeToAnalyzerErrorCodeTables {
@@ -59,7 +163,7 @@
   /// automatically generated, and whose values are the front end error name.
   final Map<ErrorCodeInfo, String> infoToFrontEndCode = {};
 
-  CfeToAnalyzerErrorCodeTables(Map<String, ErrorCodeInfo> messages) {
+  CfeToAnalyzerErrorCodeTables._(Map<String, ErrorCodeInfo> messages) {
     for (var entry in messages.entries) {
       var errorCodeInfo = entry.value;
       var index = errorCodeInfo.index;
@@ -110,6 +214,59 @@
   }
 }
 
+/// Information about a code generated class derived from `ErrorCode`.
+class ErrorClassInfo {
+  /// A list of additional import URIs that are needed by the code generated
+  /// for this class.
+  final List<String> extraImports;
+
+  /// The file path (relative to the root of `pkg/analyzer`) of the generated
+  /// file containing this class.
+  final String filePath;
+
+  /// True if this class should contain error messages extracted from the front
+  /// end's `messages.yaml` file.
+  ///
+  /// Note: at the moment we only support extracting front end error messages to
+  /// a single error class.
+  final bool includeCfeMessages;
+
+  /// The name of this class.
+  final String name;
+
+  /// The severity of errors in this class, or `null` if the severity should be
+  /// based on the [type] of the error.
+  final String? severity;
+
+  /// The superclass of this class.
+  final String superclass;
+
+  /// The type of errors in this class.
+  final String type;
+
+  const ErrorClassInfo(
+      {this.extraImports = const [],
+      required this.filePath,
+      this.includeCfeMessages = false,
+      required this.name,
+      this.severity,
+      this.superclass = 'ErrorCode',
+      required this.type});
+
+  /// Generates the code to compute the severity of errors of this class.
+  String get severityCode {
+    var severity = this.severity;
+    if (severity == null) {
+      return '$typeCode.severity';
+    } else {
+      return 'ErrorSeverity.$severity';
+    }
+  }
+
+  /// Generates the code to compute the type of errors of this class.
+  String get typeCode => 'ErrorType.$type';
+}
+
 /// In-memory representation of error code information obtained from either a
 /// `messages.yaml` file.  Supports both the analyzer and front_end message file
 /// formats.
diff --git a/pkg/analyzer/tool/messages/generate.dart b/pkg/analyzer/tool/messages/generate.dart
index 7f70d5c..93b703b 100644
--- a/pkg/analyzer/tool/messages/generate.dart
+++ b/pkg/analyzer/tool/messages/generate.dart
@@ -21,7 +21,6 @@
 import 'package:analyzer_utilities/package_root.dart' as pkg_root;
 import 'package:analyzer_utilities/tools.dart';
 import 'package:path/path.dart';
-import 'package:yaml/yaml.dart' show loadYaml;
 
 import 'error_code_info.dart';
 
@@ -33,94 +32,14 @@
     ..printSummary();
 }
 
-/// Information about all the classes derived from `ErrorCode` that should be
-/// generated.
-const List<_ErrorClassInfo> _errorClasses = [
-  _ErrorClassInfo(
-      filePath: 'lib/src/analysis_options/error/option_codes.g.dart',
-      name: 'AnalysisOptionsErrorCode',
-      type: 'COMPILE_TIME_ERROR',
-      severity: 'ERROR'),
-  _ErrorClassInfo(
-      filePath: 'lib/src/analysis_options/error/option_codes.g.dart',
-      name: 'AnalysisOptionsHintCode',
-      type: 'HINT',
-      severity: 'INFO'),
-  _ErrorClassInfo(
-      filePath: 'lib/src/analysis_options/error/option_codes.g.dart',
-      name: 'AnalysisOptionsWarningCode',
-      type: 'STATIC_WARNING',
-      severity: 'WARNING'),
-  _ErrorClassInfo(
-      filePath: 'lib/src/error/codes.g.dart',
-      name: 'CompileTimeErrorCode',
-      superclass: 'AnalyzerErrorCode',
-      type: 'COMPILE_TIME_ERROR',
-      extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
-  _ErrorClassInfo(
-      filePath: 'lib/src/error/codes.g.dart',
-      name: 'LanguageCode',
-      type: 'COMPILE_TIME_ERROR'),
-  _ErrorClassInfo(
-      filePath: 'lib/src/error/codes.g.dart',
-      name: 'StaticWarningCode',
-      superclass: 'AnalyzerErrorCode',
-      type: 'STATIC_WARNING',
-      severity: 'WARNING',
-      extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
-  _ErrorClassInfo(
-      filePath: 'lib/src/dart/error/ffi_code.g.dart',
-      name: 'FfiCode',
-      superclass: 'AnalyzerErrorCode',
-      type: 'COMPILE_TIME_ERROR',
-      extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
-  _ErrorClassInfo(
-      filePath: 'lib/src/dart/error/hint_codes.g.dart',
-      name: 'HintCode',
-      superclass: 'AnalyzerErrorCode',
-      type: 'HINT',
-      extraImports: ['package:analyzer/src/error/analyzer_error_code.dart']),
-  _ErrorClassInfo(
-      filePath: 'lib/src/dart/error/syntactic_errors.g.dart',
-      name: 'ParserErrorCode',
-      type: 'SYNTACTIC_ERROR',
-      severity: 'ERROR',
-      includeCfeMessages: true),
-  _ErrorClassInfo(
-      filePath: 'lib/src/manifest/manifest_warning_code.g.dart',
-      name: 'ManifestWarningCode',
-      type: 'STATIC_WARNING',
-      severity: 'WARNING'),
-  _ErrorClassInfo(
-      filePath: 'lib/src/pubspec/pubspec_warning_code.g.dart',
-      name: 'PubspecWarningCode',
-      type: 'STATIC_WARNING',
-      severity: 'WARNING'),
-];
-
 /// A list of all targets generated by this code generator.
 final List<GeneratedContent> allTargets = _analyzerGeneratedFiles();
 
-/// The path to the `analyzer` package.
-final String analyzerPkgPath =
-    normalize(join(pkg_root.packageRoot, 'analyzer'));
-
-/// The path to the `front_end` package.
-final String frontEndPkgPath =
-    normalize(join(pkg_root.packageRoot, 'front_end'));
-
-/// Decoded messages from the anlayzer's `messages.yaml` file.
-final Map<String, Map<String, ErrorCodeInfo>> _analyzerMessages =
-    _loadAnalyzerMessages();
-
-/// Decoded messages from the front end's `messages.yaml` file.
-final Map<String, ErrorCodeInfo> _frontEndMessages = _loadFrontEndMessages();
-
 /// Generates a list of [GeneratedContent] objects describing all the analyzer
 /// files that need to be generated.
 List<GeneratedContent> _analyzerGeneratedFiles() {
-  var classesByFile = <String, List<_ErrorClassInfo>>{};
-  for (var errorClassInfo in _errorClasses) {
+  var classesByFile = <String, List<ErrorClassInfo>>{};
+  for (var errorClassInfo in errorClasses) {
     (classesByFile[errorClassInfo.filePath] ??= []).add(errorClassInfo);
   }
   return [
@@ -133,25 +52,9 @@
   ];
 }
 
-/// Loads analyzer messages from the analyzer's `messages.yaml` file.
-Map<String, Map<String, ErrorCodeInfo>> _loadAnalyzerMessages() {
-  Map<dynamic, dynamic> messagesYaml =
-      loadYaml(File(join(analyzerPkgPath, 'messages.yaml')).readAsStringSync());
-  return decodeAnalyzerMessagesYaml(messagesYaml);
-}
-
-/// Loads front end messages from the front end's `messages.yaml` file.
-Map<String, ErrorCodeInfo> _loadFrontEndMessages() {
-  Map<dynamic, dynamic> messagesYaml =
-      loadYaml(File(join(frontEndPkgPath, 'messages.yaml')).readAsStringSync());
-  return decodeCfeMessagesYaml(messagesYaml);
-}
-
 /// Code generator for analyzer error classes.
 class _AnalyzerErrorGenerator {
-  final List<_ErrorClassInfo> errorClasses;
-  final CfeToAnalyzerErrorCodeTables tables =
-      CfeToAnalyzerErrorCodeTables(_frontEndMessages);
+  final List<ErrorClassInfo> errorClasses;
   final out = StringBuffer('''
 // 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
@@ -193,8 +96,9 @@
       out.writeln();
       out.write('class ${errorClass.name} extends ${errorClass.superclass} {');
       var entries = [
-        ..._analyzerMessages[errorClass.name]!.entries,
-        if (errorClass.includeCfeMessages) ...tables.analyzerCodeToInfo.entries
+        ...analyzerMessages[errorClass.name]!.entries,
+        if (errorClass.includeCfeMessages)
+          ...cfeToAnalyzerErrorCodeTables.analyzerCodeToInfo.entries
       ];
       for (var entry in entries..sort((a, b) => a.key.compareTo(b.key))) {
         var errorName = entry.key;
@@ -234,66 +138,22 @@
 
   void _generateFastaAnalyzerErrorCodeList() {
     out.writeln('final fastaAnalyzerErrorCodes = <ErrorCode?>[');
-    for (var entry in tables.indexToInfo) {
-      var name = tables.infoToAnalyzerCode[entry];
+    for (var entry in cfeToAnalyzerErrorCodeTables.indexToInfo) {
+      var name = cfeToAnalyzerErrorCodeTables.infoToAnalyzerCode[entry];
       out.writeln('${name == null ? 'null' : 'ParserErrorCode.$name'},');
     }
     out.writeln('];');
   }
 }
 
-class _ErrorClassInfo {
-  final List<String> extraImports;
-
-  final String filePath;
-
-  final bool includeCfeMessages;
-
-  final String name;
-
-  final String? severity;
-
-  final String superclass;
-
-  final String type;
-
-  const _ErrorClassInfo(
-      {this.extraImports = const [],
-      required this.filePath,
-      this.includeCfeMessages = false,
-      required this.name,
-      this.severity,
-      this.superclass = 'ErrorCode',
-      required this.type});
-
-  String get severityCode {
-    var severity = this.severity;
-    if (severity == null) {
-      return '$typeCode.severity';
-    } else {
-      return 'ErrorSeverity.$severity';
-    }
-  }
-
-  String get typeCode => 'ErrorType.$type';
-}
-
 class _SyntacticErrorGenerator {
-  final Map<String, ErrorCodeInfo> cfeMessages;
-  final CfeToAnalyzerErrorCodeTables tables;
-  final Map<String, Map<String, ErrorCodeInfo>> analyzerMessages;
   final String errorConverterSource;
   final String parserSource;
 
   factory _SyntacticErrorGenerator() {
-    String frontEndPkgPath = normalize(join(pkg_root.packageRoot, 'front_end'));
     String frontEndSharedPkgPath =
         normalize(join(pkg_root.packageRoot, '_fe_analyzer_shared'));
 
-    Map<dynamic, dynamic> cfeMessagesYaml = loadYaml(
-        File(join(frontEndPkgPath, 'messages.yaml')).readAsStringSync());
-    Map<dynamic, dynamic> analyzerMessagesYaml = loadYaml(
-        File(join(analyzerPkgPath, 'messages.yaml')).readAsStringSync());
     String errorConverterSource = File(join(analyzerPkgPath,
             joinAll(posix.split('lib/src/fasta/error_converter.dart'))))
         .readAsStringSync();
@@ -301,22 +161,17 @@
             joinAll(posix.split('lib/src/parser/parser.dart'))))
         .readAsStringSync();
 
-    return _SyntacticErrorGenerator._(
-        decodeCfeMessagesYaml(cfeMessagesYaml),
-        decodeAnalyzerMessagesYaml(analyzerMessagesYaml),
-        errorConverterSource,
-        parserSource);
+    return _SyntacticErrorGenerator._(errorConverterSource, parserSource);
   }
 
-  _SyntacticErrorGenerator._(this.cfeMessages, this.analyzerMessages,
-      this.errorConverterSource, this.parserSource)
-      : tables = CfeToAnalyzerErrorCodeTables(cfeMessages);
+  _SyntacticErrorGenerator._(this.errorConverterSource, this.parserSource);
 
   void checkForManualChanges() {
     // Check for ParserErrorCodes that could be removed from
     // error_converter.dart now that those ParserErrorCodes are auto generated.
     int converterCount = 0;
-    for (var errorCode in tables.infoToAnalyzerCode.values) {
+    for (var errorCode
+        in cfeToAnalyzerErrorCodeTables.infoToAnalyzerCode.values) {
       if (errorConverterSource.contains('"$errorCode"')) {
         if (converterCount == 0) {
           print('');
@@ -355,18 +210,19 @@
     }
 
     // Remove entries that have already been translated
-    for (ErrorCodeInfo entry in tables.infoToAnalyzerCode.keys) {
+    for (ErrorCodeInfo entry
+        in cfeToAnalyzerErrorCodeTables.infoToAnalyzerCode.keys) {
       messageToName.remove(messageFromEntryTemplate(entry));
     }
 
     // Print the # of autogenerated ParserErrorCodes.
-    print('${tables.infoToAnalyzerCode.length} of '
+    print('${cfeToAnalyzerErrorCodeTables.infoToAnalyzerCode.length} of '
         '${messageToName.length} ParserErrorCodes generated.');
 
     // List the ParserErrorCodes that could easily be auto generated
     // but have not been already.
     final analyzerToFasta = <String, List<String>>{};
-    cfeMessages.forEach((fastaName, entry) {
+    frontEndMessages.forEach((fastaName, entry) {
       final analyzerName = messageToName[messageFromEntryTemplate(entry)];
       if (analyzerName != null) {
         analyzerToFasta
@@ -407,7 +263,8 @@
           }
         }
         if (fastaErrorCode != null &&
-            tables.frontEndCodeToInfo[fastaErrorCode] == null) {
+            cfeToAnalyzerErrorCodeTables.frontEndCodeToInfo[fastaErrorCode] ==
+                null) {
           untranslatedFastaErrorCodes.add(fastaErrorCode);
         }
       }
@@ -420,7 +277,7 @@
       for (String fastaErrorCode in sorted) {
         String analyzerCode = '';
         String problemMessage = '';
-        var entry = cfeMessages[fastaErrorCode];
+        var entry = frontEndMessages[fastaErrorCode];
         if (entry != null) {
           // TODO(paulberry): handle multiple analyzer codes
           if (entry.index == null && entry.analyzerCode.length == 1) {
diff --git a/pkg/analyzer/tool/messages/generate_test.dart b/pkg/analyzer/tool/messages/generate_test.dart
index 462df57..02a43cf 100644
--- a/pkg/analyzer/tool/messages/generate_test.dart
+++ b/pkg/analyzer/tool/messages/generate_test.dart
@@ -8,6 +8,7 @@
 import 'package:analyzer_utilities/tools.dart';
 import 'package:path/path.dart';
 
+import 'error_code_info.dart';
 import 'generate.dart';
 
 main() async {
diff --git a/pkg/front_end/messages.status b/pkg/front_end/messages.status
index 9feaf38..8f81677 100644
--- a/pkg/front_end/messages.status
+++ b/pkg/front_end/messages.status
@@ -523,6 +523,10 @@
 JsInteropExternalMemberNotJSAnnotated/example: Fail # Web compiler specific
 JsInteropIndexNotSupported/analyzerCode: Fail # Web compiler specific
 JsInteropIndexNotSupported/example: Fail # Web compiler specific
+JsInteropStaticInteropWithInstanceMembers/analyzerCode: Fail # Web compiler specific
+JsInteropStaticInteropWithInstanceMembers/example: Fail # Web compiler specific
+JsInteropStaticInteropWithNonStaticSupertype/analyzerCode: Fail # Web compiler specific
+JsInteropStaticInteropWithNonStaticSupertype/example: Fail # Web compiler specific
 JsInteropJSClassExtendsDartClass/analyzerCode: Fail # Web compiler specific
 JsInteropJSClassExtendsDartClass/example: Fail # Web compiler specific
 JsInteropNamedParameters/analyzerCode: Fail # Web compiler specific
diff --git a/pkg/front_end/messages.yaml b/pkg/front_end/messages.yaml
index dc77400..cae1d1b 100644
--- a/pkg/front_end/messages.yaml
+++ b/pkg/front_end/messages.yaml
@@ -5050,6 +5050,14 @@
   problemMessage: "JS interop classes do not support [] and []= operator methods."
   correctionMessage: "Try replacing with a normal method."
 
+JsInteropStaticInteropWithInstanceMembers:
+  problemMessage: "JS interop class '#name' with `@staticInterop` annotation cannot declare instance members."
+  correctionMessage: "Try moving the instance member to a static extension."
+
+JsInteropStaticInteropWithNonStaticSupertype:
+  problemMessage: "JS interop class '#name' has an `@staticInterop` annotation, but has supertype '#name2', which is non-static."
+  correctionMessage: "Try marking the supertype as a static interop class using `@staticInterop`."
+
 JsInteropJSClassExtendsDartClass:
   problemMessage: "JS interop class '#name' cannot extend Dart class '#name2'."
   correctionMessage: "Try removing the JS interop annotation or adding it to the parent class."
@@ -5059,8 +5067,8 @@
   correctionMessage: "Try replacing them with normal or optional parameters."
 
 JsInteropNativeClassInAnnotation:
-  problemMessage: "JS interop class '#name' conflicts with natively supported class '#name2' in '#string3'."
-  correctionMessage: "Try making the @JS class into an @anonymous class or use js_util on the JS object."
+  problemMessage: "Non-static JS interop class '#name' conflicts with natively supported class '#name2' in '#string3'."
+  correctionMessage: "Try replacing it with a static JS interop class using `@staticInterop` with extension methods, or use js_util to interact with the native object of type '#name2'."
 
 JsInteropNonExternalConstructor:
   problemMessage: "JS interop classes do not support non-external constructors."
diff --git a/pkg/front_end/test/spell_checking_list_messages.txt b/pkg/front_end/test/spell_checking_list_messages.txt
index 5d363a0..e3c5e27 100644
--- a/pkg/front_end/test/spell_checking_list_messages.txt
+++ b/pkg/front_end/test/spell_checking_list_messages.txt
@@ -37,6 +37,7 @@
 guides
 h
 https
+interact
 interop
 intervening
 js_util
@@ -65,6 +66,7 @@
 sdksummary
 solutions
 stacktrace
+staticinterop
 stringokempty
 struct<#name
 structs
diff --git a/pkg/js/lib/js.dart b/pkg/js/lib/js.dart
index 9de2f52..1c099c3 100644
--- a/pkg/js/lib/js.dart
+++ b/pkg/js/lib/js.dart
@@ -25,6 +25,10 @@
   const _Anonymous();
 }
 
+// class _StaticInterop {
+//   const _StaticInterop();
+// }
+
 /// An annotation that indicates a [JS] annotated class is structural and does
 /// not have a known JavaScript prototype.
 ///
@@ -33,3 +37,13 @@
 /// desugars to creating a JavaScript object literal with name-value pairs
 /// corresponding to the parameter names and values.
 const _Anonymous anonymous = _Anonymous();
+
+/// [staticInterop] enables the [JS] annotated class to be treated as a "static"
+/// interop class.
+///
+/// These classes allow interop with native types, like the ones in `dart:html`.
+/// These classes should not contain any instance members, inherited or
+/// otherwise, and should instead use static extension members.
+// TODO(47324, 47325): Uncomment these annotations once erasure and subtyping
+// are ready.
+// const _StaticInterop staticInterop = _StaticInterop();
diff --git a/runtime/bin/thread_android.cc b/runtime/bin/thread_android.cc
index d386ceb..60c911a 100644
--- a/runtime/bin/thread_android.cc
+++ b/runtime/bin/thread_android.cc
@@ -86,8 +86,11 @@
   uword parameter = data->parameter();
   delete data;
 
-  // Set the thread name.
-  pthread_setname_np(pthread_self(), name);
+  // Set the thread name. There is 16 bytes limit on the name (including \0).
+  // pthread_setname_np ignores names that are too long rather than truncating.
+  char truncated_name[16];
+  snprintf(truncated_name, sizeof(truncated_name), "%s", name);
+  pthread_setname_np(pthread_self(), truncated_name);
 
   // Call the supplied thread start function handing it its parameters.
   function(parameter);
diff --git a/runtime/bin/thread_linux.cc b/runtime/bin/thread_linux.cc
index b671d0c..1a46a3e 100644
--- a/runtime/bin/thread_linux.cc
+++ b/runtime/bin/thread_linux.cc
@@ -86,8 +86,11 @@
   uword parameter = data->parameter();
   delete data;
 
-  // Set the thread name.
-  pthread_setname_np(pthread_self(), name);
+  // Set the thread name. There is 16 bytes limit on the name (including \0).
+  // pthread_setname_np ignores names that are too long rather than truncating.
+  char truncated_name[16];
+  snprintf(truncated_name, sizeof(truncated_name), "%s", name);
+  pthread_setname_np(pthread_self(), truncated_name);
 
   // Call the supplied thread start function handing it its parameters.
   function(parameter);
diff --git a/runtime/observatory/lib/elements.dart b/runtime/observatory/lib/elements.dart
index d49b31c..80f325c 100644
--- a/runtime/observatory/lib/elements.dart
+++ b/runtime/observatory/lib/elements.dart
@@ -87,7 +87,6 @@
 export 'package:observatory/src/elements/strongly_reachable_instances.dart';
 export 'package:observatory/src/elements/subtypetestcache_ref.dart';
 export 'package:observatory/src/elements/subtypetestcache_view.dart';
-export 'package:observatory/src/elements/timeline/dashboard.dart';
 export 'package:observatory/src/elements/timeline_page.dart';
 export 'package:observatory/src/elements/type_arguments_ref.dart';
 export 'package:observatory/src/elements/unknown_ref.dart';
diff --git a/runtime/observatory/lib/src/app/application.dart b/runtime/observatory/lib/src/app/application.dart
index e7eae4d..15defac 100644
--- a/runtime/observatory/lib/src/app/application.dart
+++ b/runtime/observatory/lib/src/app/application.dart
@@ -171,7 +171,6 @@
     _pageRegistry.add(new PortsPage(this));
     _pageRegistry.add(new LoggingPage(this));
     _pageRegistry.add(new TimelinePage(this));
-    _pageRegistry.add(new TimelineDashboardPage(this));
     _pageRegistry.add(new ProcessSnapshotPage(this));
     // Note that ErrorPage must be the last entry in the list as it is
     // the catch all.
diff --git a/runtime/observatory/lib/src/app/page.dart b/runtime/observatory/lib/src/app/page.dart
index 9adaad7..e8c69d6 100644
--- a/runtime/observatory/lib/src/app/page.dart
+++ b/runtime/observatory/lib/src/app/page.dart
@@ -974,38 +974,6 @@
   bool canVisit(Uri uri) => uri.path == 'timeline';
 }
 
-class TimelineDashboardPage extends Page {
-  TimelineDashboardPage(app) : super(app);
-
-  DivElement container = new DivElement();
-
-  void onInstall() {
-    if (element == null) {
-      element = container;
-    }
-  }
-
-  void _visit(Uri uri) {
-    assert(canVisit(uri));
-    app.vm.load().then((_) {
-      container.children = <Element>[
-        new TimelineDashboardElement(
-                app.vm, _timelineRepository, app.notifications,
-                queue: app.queue)
-            .element
-      ];
-    });
-  }
-
-  @override
-  void onUninstall() {
-    super.onUninstall();
-    container.children = const [];
-  }
-
-  bool canVisit(Uri uri) => uri.path == 'timeline-dashboard';
-}
-
 class ProcessSnapshotPage extends Page {
   ProcessSnapshotPage(app) : super(app);
 
diff --git a/runtime/observatory/lib/src/elements/css/shared.css b/runtime/observatory/lib/src/elements/css/shared.css
index f8c0756..1fc1bfb 100644
--- a/runtime/observatory/lib/src/elements/css/shared.css
+++ b/runtime/observatory/lib/src/elements/css/shared.css
@@ -1379,6 +1379,72 @@
   flex-shrink: 0;
 }
 
+/* process-snapshot */
+
+.process-snapshot .statusMessage {
+  font-size: 150%;
+  font-weight: bold;
+}
+.process-snapshot .statusBox {
+  height: 100%;
+  padding: 1em;
+}
+.process-snapshot .explanation {
+  display: block;
+  display: -webkit-box;
+  -webkit-line-clamp: 4;
+  -webkit-box-orient: vertical;
+  max-height: 80px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.process-snapshot .virtual-tree {
+  position: absolute;
+  height: auto;
+  top: 250px;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}
+.process-snapshot .tree-item {
+  box-sizing: border-box;
+  line-height: 30px;
+  height: 30px;
+  padding-left: 5%;
+  padding-right: 5%;
+
+  display: flex;
+  flex-direction: row;
+}
+.process-snapshot .tree-item > .size,
+.process-snapshot .tree-item > .percentage {
+  text-align: right;
+  margin-left: 0.25em;
+  margin-right: 0.25em;
+  flex-basis: 5em;
+  flex-grow: 0;
+  flex-shrink: 0;
+}
+.process-snapshot .tree-item > .edge {
+  margin-left: 0.25em;
+  margin-right: 0.25em;
+  flex-basis: 0;
+  flex-grow: 1;
+  flex-shrink: 1;
+}
+.process-snapshot .tree-item > .name {
+  margin-left: 0.5em;
+  margin-right: 0.5em;
+  flex-basis: 0;
+  flex-grow: 4;
+  flex-shrink: 4;
+}
+.process-snapshot .tree-item > .link {
+  margin-left: 0.25em;
+  margin-right: 0.25em;
+  flex-grow: 0;
+  flex-shrink: 0;
+}
 
 /* icdata-ref */
 
diff --git a/runtime/observatory/lib/src/elements/heap_snapshot.dart b/runtime/observatory/lib/src/elements/heap_snapshot.dart
index 0aae569..c8a5829 100644
--- a/runtime/observatory/lib/src/elements/heap_snapshot.dart
+++ b/runtime/observatory/lib/src/elements/heap_snapshot.dart
@@ -73,45 +73,6 @@
   HeapSnapshotTreeMode.classesTableDiff,
 ];
 
-abstract class DiffTreeMap<T> extends TreeMap<T> {
-  int getSizeA(T node);
-  int getSizeB(T node);
-
-  // We need to sum gains and losses separately because they both contribute
-  // area to the tree map tiles, i.e., losses don't have negative area in the
-  // visualization. For this reason, common is not necessarily
-  // max(sizeA,sizeB)-min(sizeA,sizeB), gain is not necessarily
-  // abs(sizeB-sizeA), etc.
-  int getGain(T node);
-  int getLoss(T node);
-  int getCommon(T node);
-
-  String getName(T node);
-  String getType(T node);
-
-  int getArea(T node) => getCommon(node) + getGain(node) + getLoss(node);
-  String getLabel(T node) {
-    var name = getName(node);
-    var sizeA = Utils.formatSize(getSizeA(node));
-    var sizeB = Utils.formatSize(getSizeB(node));
-    return "$name [$sizeA → $sizeB]";
-  }
-
-  String getBackground(T node) {
-    int l = getLoss(node);
-    int c = getCommon(node);
-    int g = getGain(node);
-    int a = l + c + g;
-    if (a == 0) {
-      return "white";
-    }
-    // Stripes of green, white and red whose areas are poritional to loss, common and gain.
-    String stop1 = (l / a * 100).toString();
-    String stop2 = ((l + c) / a * 100).toString();
-    return "linear-gradient(to right, #66FF99 $stop1%, white $stop1% $stop2%, #FF6680 $stop2%)";
-  }
-}
-
 class DominatorTreeMap extends NormalTreeMap<SnapshotObject> {
   HeapSnapshotElement element;
   DominatorTreeMap(this.element);
diff --git a/runtime/observatory/lib/src/elements/process_snapshot.dart b/runtime/observatory/lib/src/elements/process_snapshot.dart
index 3c64441..4aef2cf 100644
--- a/runtime/observatory/lib/src/elements/process_snapshot.dart
+++ b/runtime/observatory/lib/src/elements/process_snapshot.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 import 'dart:html';
+import 'dart:math' as Math;
 import 'dart:convert';
 import 'package:observatory/models.dart' as M;
 import 'package:observatory/object_graph.dart';
@@ -25,6 +26,25 @@
 import 'package:observatory/repositories.dart';
 import 'package:observatory/utils.dart';
 
+enum ProcessSnapshotTreeMode {
+  treeMap,
+  tree,
+  treeMapDiff,
+  treeDiff,
+}
+
+// Note the order of these lists is reflected in the UI, and the first option
+// is the default.
+const _viewModes = [
+  ProcessSnapshotTreeMode.treeMap,
+  ProcessSnapshotTreeMode.tree,
+];
+
+const _diffModes = [
+  ProcessSnapshotTreeMode.treeMapDiff,
+  ProcessSnapshotTreeMode.treeDiff,
+];
+
 class ProcessItemTreeMap extends NormalTreeMap<Map> {
   ProcessSnapshotElement element;
   ProcessItemTreeMap(this.element);
@@ -43,6 +63,148 @@
   void onDetails(Map node) {}
 }
 
+class ProcessItemDiff {
+  Map? _a;
+  Map? _b;
+  ProcessItemDiff? parent;
+  List<ProcessItemDiff>? children;
+  int retainedGain = -1;
+  int retainedLoss = -1;
+  int retainedCommon = -1;
+
+  int get retainedSizeA => _a == null ? 0 : _a!["size"];
+  int get retainedSizeB => _b == null ? 0 : _b!["size"];
+  int get retainedSizeDiff => retainedSizeB - retainedSizeA;
+
+  int get shallowSizeA {
+    if (_a == null) return 0;
+    var s = _a!["size"];
+    for (var c in _a!["children"]) {
+      s -= c["size"];
+    }
+    return s;
+  }
+
+  int get shallowSizeB {
+    if (_b == null) return 0;
+    var s = _b!["size"];
+    for (var c in _b!["children"]) {
+      s -= c["size"];
+    }
+    return s;
+  }
+
+  int get shallowSizeDiff => shallowSizeB - shallowSizeA;
+
+  String get name => _a == null ? _b!["name"] : _a!["name"];
+
+  static ProcessItemDiff from(Map a, Map b) {
+    var root = new ProcessItemDiff();
+    root._a = a;
+    root._b = b;
+
+    // We must use an explicit stack instead of the call stack because the
+    // dominator tree can be arbitrarily deep. We need to compute the full
+    // tree to compute areas, so we do this eagerly to avoid having to
+    // repeatedly test for initialization.
+    var worklist = <ProcessItemDiff>[];
+    worklist.add(root);
+    // Compute children top-down.
+    for (var i = 0; i < worklist.length; i++) {
+      worklist[i]._computeChildren(worklist);
+    }
+    // Compute area botton-up.
+    for (var i = worklist.length - 1; i >= 0; i--) {
+      worklist[i]._computeArea();
+    }
+
+    return root;
+  }
+
+  void _computeChildren(List<ProcessItemDiff> worklist) {
+    assert(children == null);
+    children = <ProcessItemDiff>[];
+
+    // Matching children by name.
+    final childrenB = <String, Map>{};
+    if (_b != null)
+      for (var childB in _b!["children"]) {
+        childrenB[childB["name"]] = childB;
+      }
+    if (_a != null)
+      for (var childA in _a!["children"]) {
+        var childDiff = new ProcessItemDiff();
+        childDiff.parent = this;
+        childDiff._a = childA;
+        var qualifiedName = childA["name"];
+        var childB = childrenB[qualifiedName];
+        if (childB != null) {
+          childrenB.remove(qualifiedName);
+          childDiff._b = childB;
+        }
+        children!.add(childDiff);
+        worklist.add(childDiff);
+      }
+    for (var childB in childrenB.values) {
+      var childDiff = new ProcessItemDiff();
+      childDiff.parent = this;
+      childDiff._b = childB;
+      children!.add(childDiff);
+      worklist.add(childDiff);
+    }
+
+    if (children!.length == 0) {
+      // Compress.
+      children = const <ProcessItemDiff>[];
+    }
+  }
+
+  void _computeArea() {
+    int g = 0;
+    int l = 0;
+    int c = 0;
+    for (var child in children!) {
+      g += child.retainedGain;
+      l += child.retainedLoss;
+      c += child.retainedCommon;
+    }
+    int d = shallowSizeDiff;
+    if (d > 0) {
+      g += d;
+      c += shallowSizeA;
+    } else {
+      l -= d;
+      c += shallowSizeB;
+    }
+    assert(retainedSizeA + g - l == retainedSizeB);
+    retainedGain = g;
+    retainedLoss = l;
+    retainedCommon = c;
+  }
+}
+
+class ProcessItemDiffTreeMap extends DiffTreeMap<ProcessItemDiff> {
+  ProcessSnapshotElement element;
+  ProcessItemDiffTreeMap(this.element);
+
+  int getSizeA(ProcessItemDiff node) => node.retainedSizeA;
+  int getSizeB(ProcessItemDiff node) => node.retainedSizeB;
+  int getGain(ProcessItemDiff node) => node.retainedGain;
+  int getLoss(ProcessItemDiff node) => node.retainedLoss;
+  int getCommon(ProcessItemDiff node) => node.retainedCommon;
+
+  String getType(ProcessItemDiff node) => node.name;
+  String getName(ProcessItemDiff node) => node.name;
+  ProcessItemDiff? getParent(ProcessItemDiff node) => node.parent;
+  Iterable<ProcessItemDiff> getChildren(ProcessItemDiff node) => node.children!;
+  void onSelect(ProcessItemDiff node) {
+    element.diffSelection = node;
+    element._r.dirty();
+  }
+
+  void onDetails(ProcessItemDiff node) {}
+}
+
 class ProcessSnapshotElement extends CustomElement implements Renderable {
   late RenderingScheduler<ProcessSnapshotElement> _r;
 
@@ -56,8 +218,10 @@
 
   List<Map> _loadedSnapshots = <Map>[];
   Map? selection;
+  ProcessItemDiff? diffSelection;
   Map? _snapshotA;
   Map? _snapshotB;
+  ProcessSnapshotTreeMode _mode = ProcessSnapshotTreeMode.treeMap;
 
   factory ProcessSnapshotElement(
       M.VM vm, M.EventRepository events, M.NotificationRepository notifications,
@@ -160,6 +324,8 @@
     _loadedSnapshots.add(snapshot);
     _snapshotA = snapshot;
     _snapshotB = snapshot;
+    selection = null;
+    diffSelection = null;
     _r.dirty();
   }
 
@@ -210,27 +376,210 @@
                     ..classes = ['memberName']
                     ..children = _createSnapshotSelectA()
                 ],
-              // TODO(rmacnak): Diffing.
-              // new DivElement()
-              //  ..classes = ['memberItem']
-              //  ..children = <Element>[
-              //    new DivElement()
-              //      ..classes = ['memberName']
-              //      ..text = 'Snapshot B',
-              //    new DivElement()
-              //      ..classes = ['memberName']
-              //      ..children = _createSnapshotSelectB()
-              //  ],
+              new DivElement()
+                ..classes = ['memberItem']
+                ..children = <Element>[
+                  new DivElement()
+                    ..classes = ['memberName']
+                    ..text = 'Snapshot B',
+                  new DivElement()
+                    ..classes = ['memberName']
+                    ..children = _createSnapshotSelectB()
+                ],
+              new DivElement()
+                ..classes = ['memberItem']
+                ..children = <Element>[
+                  new DivElement()
+                    ..classes = ['memberName']
+                    ..text = (_snapshotA == _snapshotB) ? 'View ' : 'Compare ',
+                  new DivElement()
+                    ..classes = ['memberName']
+                    ..children = _createModeSelect()
+                ]
             ]
         ],
     ];
-    if (selection == null) {
-      selection = _snapshotA!["root"];
+
+    switch (_mode) {
+      case ProcessSnapshotTreeMode.treeMap:
+        if (selection == null) {
+          selection = _snapshotA!["root"];
+        }
+        _createTreeMap(report, new ProcessItemTreeMap(this), selection);
+        break;
+      case ProcessSnapshotTreeMode.tree:
+        if (selection == null) {
+          selection = _snapshotA!["root"];
+        }
+        _tree = new VirtualTreeElement(
+            _createItem, _updateItem, _getChildrenItem,
+            items: [selection], queue: _r.queue);
+        _tree!.expand(selection!);
+        report.add(_tree!.element);
+        break;
+      case ProcessSnapshotTreeMode.treeMapDiff:
+        if (diffSelection == null) {
+          diffSelection =
+              ProcessItemDiff.from(_snapshotA!["root"], _snapshotB!["root"]);
+        }
+        _createTreeMap(report, new ProcessItemDiffTreeMap(this), diffSelection);
+        break;
+      case ProcessSnapshotTreeMode.treeDiff:
+        var root =
+            ProcessItemDiff.from(_snapshotA!["root"], _snapshotB!["root"]);
+        _tree = new VirtualTreeElement(
+            _createItemDiff, _updateItemDiff, _getChildrenItemDiff,
+            items: [root], queue: _r.queue);
+        _tree!.expand(root);
+        report.add(_tree!.element);
+        break;
+      default:
+        throw new Exception('Unknown ProcessSnapshotTreeMode: $_mode');
     }
-    _createTreeMap(report, new ProcessItemTreeMap(this), selection);
+
     return report;
   }
 
+  VirtualTreeElement? _tree;
+
+  static HtmlElement _createItem(toggle) {
+    return new DivElement()
+      ..classes = ['tree-item']
+      ..children = <Element>[
+        new SpanElement()
+          ..classes = ['percentage']
+          ..title = 'percentage of total',
+        new SpanElement()
+          ..classes = ['size']
+          ..title = 'retained size',
+        new SpanElement()..classes = ['lines'],
+        new ButtonElement()
+          ..classes = ['expander']
+          ..onClick.listen((_) => toggle(autoToggleSingleChildNodes: true)),
+        new SpanElement()..classes = ['name'],
+      ];
+  }
+
+  static HtmlElement _createItemDiff(toggle) {
+    return new DivElement()
+      ..classes = ['tree-item']
+      ..children = <Element>[
+        new SpanElement()
+          ..classes = ['percentage']
+          ..title = 'percentage of total',
+        new SpanElement()
+          ..classes = ['size']
+          ..title = 'retained size A',
+        new SpanElement()
+          ..classes = ['percentage']
+          ..title = 'percentage of total',
+        new SpanElement()
+          ..classes = ['size']
+          ..title = 'retained size B',
+        new SpanElement()
+          ..classes = ['size']
+          ..title = 'retained size change',
+        new SpanElement()..classes = ['lines'],
+        new ButtonElement()
+          ..classes = ['expander']
+          ..onClick.listen((_) => toggle(autoToggleSingleChildNodes: true)),
+        new SpanElement()..classes = ['name']
+      ];
+  }
+
+  void _updateItem(HtmlElement element, node, int depth) {
+    var size = node["size"];
+    var rootSize = _snapshotA!["root"]["size"];
+    element.children[0].text =
+        Utils.formatPercentNormalized(size * 1.0 / rootSize);
+    element.children[1].text = Utils.formatSize(size);
+    _updateLines(element.children[2].children, depth);
+    if (_getChildrenItem(node).isNotEmpty) {
+      element.children[3].text = _tree!.isExpanded(node) ? 'â–¼' : 'â–º';
+    } else {
+      element.children[3].text = '';
+    }
+    element.children[4].text = node["name"];
+    element.children[4].title = node["description"];
+  }
+
+  void _updateItemDiff(HtmlElement element, nodeDynamic, int depth) {
+    ProcessItemDiff node = nodeDynamic;
+    element.children[0].text = Utils.formatPercentNormalized(
+        node.retainedSizeA * 1.0 / _snapshotA!["root"]["size"]);
+    element.children[1].text = Utils.formatSize(node.retainedSizeA);
+    element.children[2].text = Utils.formatPercentNormalized(
+        node.retainedSizeB * 1.0 / _snapshotB!["root"]["size"]);
+    element.children[3].text = Utils.formatSize(node.retainedSizeB);
+    element.children[4].text = (node.retainedSizeDiff > 0 ? '+' : '') +
+        Utils.formatSize(node.retainedSizeDiff);
+    element.children[4].style.color =
+        node.retainedSizeDiff > 0 ? "red" : "green";
+    _updateLines(element.children[5].children, depth);
+    if (_getChildrenItemDiff(node).isNotEmpty) {
+      element.children[6].text = _tree!.isExpanded(node) ? 'â–¼' : 'â–º';
+    } else {
+      element.children[6].text = '';
+    }
+    element.children[7]..text = node.name;
+  }
+
+  static Iterable _getChildrenItem(node) {
+    return new List<Map>.from(node["children"]);
+  }
+
+  static Iterable _getChildrenItemDiff(nodeDynamic) {
+    ProcessItemDiff node = nodeDynamic;
+    final list = node.children!.toList();
+    list.sort((a, b) => b.retainedSizeDiff - a.retainedSizeDiff);
+    return list;
+  }
+
+  static _updateLines(List<Element> lines, int n) {
+    n = Math.max(0, n);
+    while (lines.length > n) {
+      lines.removeLast();
+    }
+    while (lines.length < n) {
+      lines.add(new SpanElement());
+    }
+  }
+
+  static String modeToString(ProcessSnapshotTreeMode mode) {
+    switch (mode) {
+      case ProcessSnapshotTreeMode.treeMap:
+      case ProcessSnapshotTreeMode.treeMapDiff:
+        return 'Tree Map';
+      case ProcessSnapshotTreeMode.tree:
+      case ProcessSnapshotTreeMode.treeDiff:
+        return 'Tree';
+    }
+    throw new Exception('Unknown ProcessSnapshotTreeMode: $mode');
+  }
+
+  List<Element> _createModeSelect() {
+    var s;
+    var modes = _snapshotA == _snapshotB ? _viewModes : _diffModes;
+    if (!modes.contains(_mode)) {
+      _mode = modes[0];
+      _r.dirty();
+    }
+    return [
+      s = new SelectElement()
+        ..classes = ['analysis-select']
+        ..value = modeToString(_mode)
+        ..children = modes.map((mode) {
+          return new OptionElement(
+              value: modeToString(mode), selected: _mode == mode)
+            ..text = modeToString(mode);
+        }).toList(growable: false)
+        ..onChange.listen((_) {
+          _mode = modes[s.selectedIndex];
+          _r.dirty();
+        })
+    ];
+  }
+
   String snapshotToString(snapshot) {
     if (snapshot == null) return "None";
     return snapshot["root"]["name"] +
diff --git a/runtime/observatory/lib/src/elements/timeline/dashboard.dart b/runtime/observatory/lib/src/elements/timeline/dashboard.dart
deleted file mode 100644
index 5aca24a..0000000
--- a/runtime/observatory/lib/src/elements/timeline/dashboard.dart
+++ /dev/null
@@ -1,230 +0,0 @@
-// 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.
-
-/// This page is not directly reachable from the main Observatory ui.
-/// It is mainly mented to be used from editors as an integrated tool.
-///
-/// This page mainly targeting developers and not VM experts, so concepts like
-/// timeline streams are hidden away.
-///
-/// The page exposes two views over the timeline data.
-/// Both of them are filtered based on the optional argument `mode`.
-/// See [_TimelineView] for the explanation of the two possible values.
-
-import 'dart:async';
-import 'dart:html';
-import 'dart:convert';
-import 'package:observatory/models.dart' as M;
-import 'package:observatory/src/elements/helpers/nav_bar.dart';
-import 'package:observatory/src/elements/helpers/rendering_scheduler.dart';
-import 'package:observatory/src/elements/helpers/custom_element.dart';
-import 'package:observatory/src/elements/nav/notify.dart';
-
-/// The two possible views are available.
-/// * `string`
-///   The events are just filtered by `mode` and maintain their original
-///   timestamp.
-/// * `frame`
-///   The events are organized by frame.
-///   The events are shifted in order to give a high level view of the
-///   computation involved in a frame.
-///   The frame are concatenated one after the other taking care of not
-///   overlapping the related events.
-enum _TimelineView { strict, frame }
-
-class TimelineDashboardElement extends CustomElement implements Renderable {
-  late RenderingScheduler<TimelineDashboardElement> _r;
-
-  Stream<RenderedEvent<TimelineDashboardElement>> get onRendered =>
-      _r.onRendered;
-
-  late M.VM _vm;
-  late M.TimelineRepository _repository;
-  late M.NotificationRepository _notifications;
-  late M.TimelineFlags _flags;
-  _TimelineView _view = _TimelineView.strict;
-
-  M.VM get vm => _vm;
-  M.NotificationRepository get notifications => _notifications;
-
-  factory TimelineDashboardElement(M.VM vm, M.TimelineRepository repository,
-      M.NotificationRepository notifications,
-      {RenderingQueue? queue}) {
-    assert(vm != null);
-    assert(repository != null);
-    assert(notifications != null);
-    TimelineDashboardElement e = new TimelineDashboardElement.created();
-    e._r = new RenderingScheduler<TimelineDashboardElement>(e, queue: queue);
-    e._vm = vm;
-    e._repository = repository;
-    e._notifications = notifications;
-    if (vm.embedder == 'Flutter') {
-      e._view = _TimelineView.frame;
-    }
-    return e;
-  }
-
-  TimelineDashboardElement.created() : super.created('timeline-dashboard');
-
-  @override
-  attached() {
-    super.attached();
-    _r.enable();
-    _refresh();
-  }
-
-  @override
-  detached() {
-    super.detached();
-    _r.disable(notify: true);
-    children = <Element>[];
-  }
-
-  IFrameElement? _frame;
-  DivElement? _content;
-
-  void render() {
-    if (_frame == null) {
-      _frame = new IFrameElement();
-    }
-    if (_content == null) {
-      _content = new DivElement()..classes = ['content-centered-big'];
-    }
-    // ignore: unsafe_html
-    _frame!.src = _makeFrameUrl();
-    _content!.children = <Element>[
-      new HeadingElement.h2()
-        ..nodes = ([new Text("Timeline View")]
-          ..addAll(_createButtons())
-          ..addAll(_createTabs())),
-      new ParagraphElement()
-        ..text = (_view == _TimelineView.frame
-            ? 'Logical view of the computation involved in each frame '
-                '(timestamps may not be preserved)'
-            : 'Sequence of events generated during the execution '
-                '(timestamps are preserved)')
-    ];
-    if (children.isEmpty) {
-      children = <Element>[
-        navBar(<Element>[
-          new NavNotifyElement(_notifications, queue: _r.queue).element
-        ]),
-        _content!,
-        new DivElement()
-          ..classes = ['iframe']
-          ..children = <Element>[_frame!]
-      ];
-    }
-  }
-
-  List<Node> _createButtons() {
-    if (_flags == null) {
-      return [new Text('Loading')];
-    }
-    if (_suggestedProfile(_flags.profiles).streams.any((s) => !s.isRecorded)) {
-      return [
-        new ButtonElement()
-          ..classes = ['header_button']
-          ..text = 'Enable'
-          ..title = 'The Timeline is not fully enabled, click to enable'
-          ..onClick.listen((e) => _enable()),
-      ];
-    }
-    return [
-      new ButtonElement()
-        ..classes = ['header_button']
-        ..text = 'Load from VM'
-        ..title = 'Load the timeline'
-        ..onClick.listen((e) => _refresh()),
-      new ButtonElement()
-        ..classes = ['header_button']
-        ..text = 'Reset Timeline'
-        ..title = 'Reset the current timeline'
-        ..onClick.listen((e) => _clear()),
-      new ButtonElement()
-        ..classes = ['header_button', 'left-pad']
-        ..text = 'Save to File…'
-        ..title = 'Save the current Timeline to file'
-        ..onClick.listen((e) => _save()),
-      new ButtonElement()
-        ..classes = ['header_button']
-        ..text = 'Load from File…'
-        ..title = 'Load a saved timeline from file'
-        ..onClick.listen((e) => _load()),
-    ];
-  }
-
-  List<Element> _createTabs() {
-    if (_vm.embedder != 'Flutter') {
-      return const [];
-    }
-    return [
-      new SpanElement()
-        ..classes = ['tab_buttons']
-        ..children = <Element>[
-          new ButtonElement()
-            ..text = 'Frame View'
-            ..title = 'Logical view of the computation involved in each frame\n'
-                'Timestamps may not be preserved'
-            ..disabled = _view == _TimelineView.frame
-            ..onClick.listen((_) {
-              _view = _TimelineView.frame;
-              _r.dirty();
-            }),
-          new ButtonElement()
-            ..text = 'Time View'
-            ..title = 'Sequence of events generated during the execution\n'
-                'Timestamps are preserved'
-            ..disabled = _view == _TimelineView.strict
-            ..onClick.listen((_) {
-              _view = _TimelineView.strict;
-              _r.dirty();
-            }),
-        ]
-    ];
-  }
-
-  String _makeFrameUrl() {
-    final String mode = 'basic';
-    final String view = _view == _TimelineView.frame ? 'frame' : 'strict';
-    return 'timeline.html#mode=$mode&view=$view';
-  }
-
-  M.TimelineProfile _suggestedProfile(Iterable<M.TimelineProfile> profiles) {
-    return profiles
-        .where((profile) => profile.name == 'Flutter Developer')
-        .single;
-  }
-
-  Future _enable() async {
-    await _repository.setRecordedStreams(
-        vm, _suggestedProfile(_flags.profiles).streams);
-    _refresh();
-  }
-
-  Future _refresh() async {
-    _flags = await _repository.getFlags(vm);
-    _r.dirty();
-    final traceData =
-        Map<String, dynamic>.from(await _repository.getTimeline(vm));
-    return _postMessage('refresh', traceData);
-  }
-
-  Future _clear() async {
-    await _repository.clear(_vm);
-    return _postMessage('clear');
-  }
-
-  Future _save() => _postMessage('save');
-
-  Future _load() => _postMessage('load');
-
-  Future _postMessage(String method,
-      [Map<String, dynamic> params = const <String, dynamic>{}]) async {
-    var message = {'method': method, 'params': params};
-    _frame!.contentWindow!
-        .postMessage(json.encode(message), window.location.href);
-    return null;
-  }
-}
diff --git a/runtime/observatory/lib/src/elements/tree_map.dart b/runtime/observatory/lib/src/elements/tree_map.dart
index 3a3721d..79e18d4 100644
--- a/runtime/observatory/lib/src/elements/tree_map.dart
+++ b/runtime/observatory/lib/src/elements/tree_map.dart
@@ -192,3 +192,42 @@
     return "hsl($hue,60%,60%)";
   }
 }
+
+abstract class DiffTreeMap<T> extends TreeMap<T> {
+  int getSizeA(T node);
+  int getSizeB(T node);
+
+  // We need to sum gains and losses separately because they both contribute
+  // area to the tree map tiles, i.e., losses don't have negative area in the
+  // visualization. For this reason, common is not necessarily
+  // max(sizeA,sizeB)-min(sizeA,sizeB), gain is not necessarily
+  // abs(sizeB-sizeA), etc.
+  int getGain(T node);
+  int getLoss(T node);
+  int getCommon(T node);
+
+  String getName(T node);
+  String getType(T node);
+
+  int getArea(T node) => getCommon(node) + getGain(node) + getLoss(node);
+  String getLabel(T node) {
+    var name = getName(node);
+    var sizeA = Utils.formatSize(getSizeA(node));
+    var sizeB = Utils.formatSize(getSizeB(node));
+    return "$name [$sizeA → $sizeB]";
+  }
+
+  String getBackground(T node) {
+    int l = getLoss(node);
+    int c = getCommon(node);
+    int g = getGain(node);
+    int a = l + c + g;
+    if (a == 0) {
+      return "white";
+    }
+    // Stripes of green, white and red whose areas are poritional to loss, common and gain.
+    String stop1 = (l / a * 100).toString();
+    String stop2 = ((l + c) / a * 100).toString();
+    return "linear-gradient(to right, #66FF99 $stop1%, white $stop1% $stop2%, #FF6680 $stop2%)";
+  }
+}
diff --git a/runtime/observatory/observatory_sources.gni b/runtime/observatory/observatory_sources.gni
index 9ad1b96..88db18e 100644
--- a/runtime/observatory/observatory_sources.gni
+++ b/runtime/observatory/observatory_sources.gni
@@ -131,7 +131,6 @@
   "lib/src/elements/strongly_reachable_instances.dart",
   "lib/src/elements/subtypetestcache_ref.dart",
   "lib/src/elements/subtypetestcache_view.dart",
-  "lib/src/elements/timeline/dashboard.dart",
   "lib/src/elements/timeline_page.dart",
   "lib/src/elements/tree_map.dart",
   "lib/src/elements/type_arguments_ref.dart",
diff --git a/runtime/vm/dart_api_impl.cc b/runtime/vm/dart_api_impl.cc
index cfc1759..6ad719e 100644
--- a/runtime/vm/dart_api_impl.cc
+++ b/runtime/vm/dart_api_impl.cc
@@ -2077,7 +2077,7 @@
   Isolate* I = T->isolate();
   CHECK_API_SCOPE(T);
   CHECK_CALLBACK_STATE(T);
-  API_TIMELINE_BEGIN_END_BASIC(T);
+  API_TIMELINE_BEGIN_END(T);
   TransitionNativeToVM transition(T);
   if (I->message_handler()->HandleNextMessage() != MessageHandler::kOK) {
     return Api::NewHandle(T, T->StealStickyError());
@@ -2090,7 +2090,7 @@
   Isolate* I = T->isolate();
   CHECK_API_SCOPE(T);
   CHECK_CALLBACK_STATE(T);
-  API_TIMELINE_BEGIN_END_BASIC(T);
+  API_TIMELINE_BEGIN_END(T);
   TransitionNativeToVM transition(T);
   if (I->message_notify_callback() != NULL) {
     return Api::NewError("waitForEventSync is not supported by this embedder");
diff --git a/runtime/vm/dart_api_impl.h b/runtime/vm/dart_api_impl.h
index 683be98..47f9b7a 100644
--- a/runtime/vm/dart_api_impl.h
+++ b/runtime/vm/dart_api_impl.h
@@ -118,27 +118,17 @@
 #ifdef SUPPORT_TIMELINE
 #define API_TIMELINE_DURATION(thread)                                          \
   TimelineBeginEndScope api_tbes(thread, Timeline::GetAPIStream(), CURRENT_FUNC)
-#define API_TIMELINE_DURATION_BASIC(thread)                                    \
-  API_TIMELINE_DURATION(thread);                                               \
-  api_tbes.SetNumArguments(1);                                                 \
-  api_tbes.CopyArgument(0, "mode", "basic");
 
 #define API_TIMELINE_BEGIN_END(thread)                                         \
   TimelineBeginEndScope api_tbes(thread, Timeline::GetAPIStream(), CURRENT_FUNC)
 
-#define API_TIMELINE_BEGIN_END_BASIC(thread)                                   \
-  API_TIMELINE_BEGIN_END(thread);                                              \
-  api_tbes.SetNumArguments(1);                                                 \
-  api_tbes.CopyArgument(0, "mode", "basic");
 #else
 #define API_TIMELINE_DURATION(thread)                                          \
   do {                                                                         \
   } while (false)
-#define API_TIMELINE_DURATION_BASIC(thread) API_TIMELINE_DURATION(thread)
 #define API_TIMELINE_BEGIN_END(thread)                                         \
   do {                                                                         \
   } while (false)
-#define API_TIMELINE_BEGIN_END_BASIC(thread) API_TIMELINE_BEGIN_END(thread)
 #endif  // !PRODUCT
 
 class Api : AllStatic {
diff --git a/runtime/vm/heap/heap.cc b/runtime/vm/heap/heap.cc
index 903803a..980f2e4 100644
--- a/runtime/vm/heap/heap.cc
+++ b/runtime/vm/heap/heap.cc
@@ -463,7 +463,7 @@
       VMTagScope tagScope(thread, reason == GCReason::kIdle
                                       ? VMTag::kGCIdleTagId
                                       : VMTag::kGCNewSpaceTagId);
-      TIMELINE_FUNCTION_GC_DURATION_BASIC(thread, "CollectNewGeneration");
+      TIMELINE_FUNCTION_GC_DURATION(thread, "CollectNewGeneration");
       new_space_.Scavenge(reason);
       RecordAfterGC(GCType::kScavenge);
       PrintStats();
@@ -512,7 +512,7 @@
     VMTagScope tagScope(thread, reason == GCReason::kIdle
                                     ? VMTag::kGCIdleTagId
                                     : VMTag::kGCOldSpaceTagId);
-    TIMELINE_FUNCTION_GC_DURATION_BASIC(thread, "CollectOldGeneration");
+    TIMELINE_FUNCTION_GC_DURATION(thread, "CollectOldGeneration");
     old_space_.CollectGarbage(type == GCType::kMarkCompact, true /* finish */);
     RecordAfterGC(type);
     PrintStats();
@@ -610,7 +610,7 @@
 }
 
 void Heap::StartConcurrentMarking(Thread* thread) {
-  TIMELINE_FUNCTION_GC_DURATION_BASIC(thread, "StartConcurrentMarking");
+  TIMELINE_FUNCTION_GC_DURATION(thread, "StartConcurrentMarking");
   old_space_.CollectGarbage(/*compact=*/false, /*finalize=*/false);
 }
 
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index 9d49ce6..c1e6cbe 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -1383,17 +1383,6 @@
       }
     }
   } else {
-#ifndef PRODUCT
-    if (!Isolate::IsSystemIsolate(I)) {
-      // Mark all the user isolates as using a simplified timeline page of
-      // Observatory. The internal isolates will be filtered out from
-      // the Timeline due to absence of this argument. We still send them in
-      // order to maintain the original behavior of the full timeline and allow
-      // the developer to download complete dump files.
-      tbes.SetNumArguments(2);
-      tbes.CopyArgument(1, "mode", "basic");
-    }
-#endif
     const Object& msg_handler = Object::Handle(
         zone, DartLibraryCalls::HandleMessage(message->dest_port(), msg));
     if (msg_handler.IsError()) {
diff --git a/runtime/vm/os_thread_android.cc b/runtime/vm/os_thread_android.cc
index df257e3..8c09320 100644
--- a/runtime/vm/os_thread_android.cc
+++ b/runtime/vm/os_thread_android.cc
@@ -138,6 +138,7 @@
   delete data;
 
   // Set the thread name. There is 16 bytes limit on the name (including \0).
+  // pthread_setname_np ignores names that are too long rather than truncating.
   char truncated_name[16];
   snprintf(truncated_name, ARRAY_SIZE(truncated_name), "%s", name);
   pthread_setname_np(pthread_self(), truncated_name);
diff --git a/runtime/vm/os_thread_linux.cc b/runtime/vm/os_thread_linux.cc
index 167721a..409b03b 100644
--- a/runtime/vm/os_thread_linux.cc
+++ b/runtime/vm/os_thread_linux.cc
@@ -139,6 +139,7 @@
   delete data;
 
   // Set the thread name. There is 16 bytes limit on the name (including \0).
+  // pthread_setname_np ignores names that are too long rather than truncating.
   char truncated_name[16];
   snprintf(truncated_name, ARRAY_SIZE(truncated_name), "%s", name);
   pthread_setname_np(pthread_self(), truncated_name);
diff --git a/runtime/vm/timeline.h b/runtime/vm/timeline.h
index b6d1908..f4389ce 100644
--- a/runtime/vm/timeline.h
+++ b/runtime/vm/timeline.h
@@ -499,15 +499,10 @@
 
 #define TIMELINE_FUNCTION_GC_DURATION(thread, name)                            \
   TimelineBeginEndScope tbes(thread, Timeline::GetGCStream(), name);
-#define TIMELINE_FUNCTION_GC_DURATION_BASIC(thread, name)                      \
-  TIMELINE_FUNCTION_GC_DURATION(thread, name)                                  \
-  tbes.SetNumArguments(1);                                                     \
-  tbes.CopyArgument(0, "mode", "basic");
 #else
 #define TIMELINE_DURATION(thread, stream, name)
 #define TIMELINE_FUNCTION_COMPILATION_DURATION(thread, name, function)
 #define TIMELINE_FUNCTION_GC_DURATION(thread, name)
-#define TIMELINE_FUNCTION_GC_DURATION_BASIC(thread, name)
 #endif  // !PRODUCT
 
 // See |TimelineBeginEndScope|.
diff --git a/sdk/lib/js/_js_annotations.dart b/sdk/lib/js/_js_annotations.dart
index c188214..b110632 100644
--- a/sdk/lib/js/_js_annotations.dart
+++ b/sdk/lib/js/_js_annotations.dart
@@ -18,4 +18,10 @@
   const _Anonymous();
 }
 
-const _Anonymous anonymous = const _Anonymous();
+// class _StaticInterop {
+//   const _StaticInterop();
+// }
+
+const _Anonymous anonymous = _Anonymous();
+
+// const _StaticInterop staticInterop = _StaticInterop();
diff --git a/tests/lib/js/native_as_js_classes_static_test/default_library_namespace_test.dart b/tests/lib/js/native_as_js_classes_static_test/default_library_namespace_test.dart
index d230129..fa262ae 100644
--- a/tests/lib/js/native_as_js_classes_static_test/default_library_namespace_test.dart
+++ b/tests/lib/js/native_as_js_classes_static_test/default_library_namespace_test.dart
@@ -13,29 +13,29 @@
 @JS()
 class HTMLDocument {}
 //    ^
-// [web] JS interop class 'HTMLDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
+// [web] Non-static JS interop class 'HTMLDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
 
 // Test same annotation name as a native class.
 @JS('HTMLDocument')
 class HtmlDocument {}
 //    ^
-// [web] JS interop class 'HtmlDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
+// [web] Non-static JS interop class 'HtmlDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
 
 // Test annotation name with 'self' and 'window' prefixes.
 @JS('self.Window')
 class WindowWithSelf {}
 //    ^
-// [web] JS interop class 'WindowWithSelf' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'WindowWithSelf' conflicts with natively supported class 'Window' in 'dart:html'.
 
 @JS('window.Window')
 class WindowWithWindow {}
 //    ^
-// [web] JS interop class 'WindowWithWindow' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'WindowWithWindow' conflicts with natively supported class 'Window' in 'dart:html'.
 
 @JS('self.window.self.window.self.Window')
 class WindowWithMultipleSelfsAndWindows {}
 //    ^
-// [web] JS interop class 'WindowWithMultipleSelfsAndWindows' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'WindowWithMultipleSelfsAndWindows' conflicts with natively supported class 'Window' in 'dart:html'.
 
 // Test annotation with native class name but with a prefix that isn't 'self' or
 // 'window'.
@@ -47,14 +47,14 @@
 @JS()
 class DOMWindow {}
 //    ^
-// [web] JS interop class 'DOMWindow' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'DOMWindow' conflicts with natively supported class 'Window' in 'dart:html'.
 
 // Test same annotation name as a native class with multiple annotation names
 // dart:html.Window uses both "Window" and "DOMWindow".
 @JS('DOMWindow')
 class DomWindow {}
 //    ^
-// [web] JS interop class 'DomWindow' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'DomWindow' conflicts with natively supported class 'Window' in 'dart:html'.
 
 // Test different annotation name but with same class name as a @Native class.
 @JS('Foo')
diff --git a/tests/lib/js/native_as_js_classes_static_test/global_library_namespace_test.dart b/tests/lib/js/native_as_js_classes_static_test/global_library_namespace_test.dart
index fb55aa6..7911057 100644
--- a/tests/lib/js/native_as_js_classes_static_test/global_library_namespace_test.dart
+++ b/tests/lib/js/native_as_js_classes_static_test/global_library_namespace_test.dart
@@ -12,27 +12,27 @@
 @JS()
 class HTMLDocument {}
 //    ^
-// [web] JS interop class 'HTMLDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
+// [web] Non-static JS interop class 'HTMLDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
 
 @JS('HTMLDocument')
 class HtmlDocument {}
 //    ^
-// [web] JS interop class 'HtmlDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
+// [web] Non-static JS interop class 'HtmlDocument' conflicts with natively supported class 'HtmlDocument' in 'dart:html'.
 
 @JS('self.Window')
 class WindowWithSelf {}
 //    ^
-// [web] JS interop class 'WindowWithSelf' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'WindowWithSelf' conflicts with natively supported class 'Window' in 'dart:html'.
 
 @JS('window.Window')
 class WindowWithWindow {}
 //    ^
-// [web] JS interop class 'WindowWithWindow' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'WindowWithWindow' conflicts with natively supported class 'Window' in 'dart:html'.
 
 @JS('self.window.self.window.self.Window')
 class WindowWithMultipleSelfsAndWindows {}
 //    ^
-// [web] JS interop class 'WindowWithMultipleSelfsAndWindows' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'WindowWithMultipleSelfsAndWindows' conflicts with natively supported class 'Window' in 'dart:html'.
 
 @JS('foo.Window')
 class WindowWithDifferentPrefix {}
@@ -40,12 +40,12 @@
 @JS()
 class DOMWindow {}
 //    ^
-// [web] JS interop class 'DOMWindow' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'DOMWindow' conflicts with natively supported class 'Window' in 'dart:html'.
 
 @JS('DOMWindow')
 class DomWindow {}
 //    ^
-// [web] JS interop class 'DomWindow' conflicts with natively supported class 'Window' in 'dart:html'.
+// [web] Non-static JS interop class 'DomWindow' conflicts with natively supported class 'Window' in 'dart:html'.
 
 @JS('Foo')
 class Window {}
diff --git a/tests/lib/js/static_interop_test/member_test.dart b/tests/lib/js/static_interop_test/member_test.dart
new file mode 100644
index 0000000..f5bf11e
--- /dev/null
+++ b/tests/lib/js/static_interop_test/member_test.dart
@@ -0,0 +1,54 @@
+// 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.
+
+// Test that instance members are disallowed from static interop classes.
+
+@JS()
+library member_test;
+
+import 'package:js/js.dart';
+
+@JS()
+@staticInterop
+class StaticJSClass {
+  external StaticJSClass();
+  external StaticJSClass.namedConstructor();
+  external factory StaticJSClass.externalFactory();
+  factory StaticJSClass.redirectingFactory() = StaticJSClass;
+  factory StaticJSClass.factory() => StaticJSClass();
+
+  static String staticField = 'staticField';
+  static String get staticGetSet => staticField;
+  static set staticGetSet(String val) => staticField = val;
+  static String staticMethod() => 'staticMethod';
+
+  external static String externalStaticField;
+  external static String get externalStaticGetSet;
+  external static set externalStaticGetSet(String val);
+  external static String externalStaticMethod();
+
+  external int get getter;
+  //               ^
+  // [web] JS interop class 'StaticJSClass' with `@staticInterop` annotation cannot declare instance members.
+  external set setter(_);
+  //           ^
+  // [web] JS interop class 'StaticJSClass' with `@staticInterop` annotation cannot declare instance members.
+  external int method();
+  //           ^
+  // [web] JS interop class 'StaticJSClass' with `@staticInterop` annotation cannot declare instance members.
+  external int field;
+  //           ^
+  // [web] JS interop class 'StaticJSClass' with `@staticInterop` annotation cannot declare instance members.
+}
+
+extension StaticJSClassExtension on StaticJSClass {
+  external String externalField;
+  external String get externalGetSet;
+  external set externalGetSet(String val);
+  external String externalMethod();
+
+  String get getSet => this.externalGetSet;
+  set getSet(String val) => this.externalGetSet = val;
+  String method() => 'method';
+}
diff --git a/tests/lib/js/static_interop_test/supertype_test.dart b/tests/lib/js/static_interop_test/supertype_test.dart
new file mode 100644
index 0000000..88e0085
--- /dev/null
+++ b/tests/lib/js/static_interop_test/supertype_test.dart
@@ -0,0 +1,114 @@
+// 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.
+
+// Test that interop classes that inherit/implement other classes have the
+// appropriate static interop static errors.
+
+@JS()
+library supertype_test;
+
+import 'package:js/js.dart';
+
+// Static base interop class.
+@JS()
+@staticInterop
+class Static {}
+
+// Non-static base interop class.
+@JS()
+class NonStatic {
+  external int instanceMethod();
+}
+
+@JS()
+@staticInterop
+class NonStaticMarkedAsStatic {
+  external int instanceMethod();
+  //           ^
+  // [web] JS interop class 'NonStaticMarkedAsStatic' with `@staticInterop` annotation cannot declare instance members.
+}
+
+// Static interop classes can inherit other static interop classes in order to
+// inherit its extension methods.
+@JS()
+@staticInterop
+class StaticExtendsStatic extends Static {}
+
+// Static interop classes are disallowed from extending non-static interop
+// classes.
+@JS()
+@staticInterop
+class StaticExtendsNonStatic extends NonStatic {}
+//    ^
+// [web] JS interop class 'StaticExtendsNonStatic' has an `@staticInterop` annotation, but has supertype 'NonStatic', which is non-static.
+
+// Static interop classes can implement each other in order to inherit extension
+// methods. Note that a non-abstract static interop class can not implement a
+// non-static class by definition, as it would need to contain an
+// implementation.
+@JS()
+@staticInterop
+class StaticImplementsStatic implements Static {}
+
+// Abstract classes should behave the same way as concrete classes.
+@JS()
+@staticInterop
+abstract class StaticAbstract {}
+
+// Abstract classes with instance members should be non-static. The following
+// have abstract or concrete members, so they're considered non-static.
+@JS()
+abstract class NonStaticAbstract {
+  int abstractMethod();
+}
+
+@JS()
+@staticInterop
+abstract class NonStaticAbstractWithAbstractMembers {
+  int abstractMethod();
+  //  ^
+  // [web] JS interop class 'NonStaticAbstractWithAbstractMembers' with `@staticInterop` annotation cannot declare instance members.
+}
+
+@JS()
+@staticInterop
+abstract class NonStaticAbstractWithConcreteMembers {
+  external int instanceMethod();
+  //           ^
+  // [web] JS interop class 'NonStaticAbstractWithConcreteMembers' with `@staticInterop` annotation cannot declare instance members.
+}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractImplementsStaticAbstract
+    implements StaticAbstract {}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractExtendsStaticAbstract extends StaticAbstract {}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractImplementsNonStaticAbstract
+//             ^
+// [web] JS interop class 'StaticAbstractImplementsNonStaticAbstract' has an `@staticInterop` annotation, but has supertype 'NonStaticAbstract', which is non-static.
+    implements
+        NonStaticAbstract {}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractImplementsMultipleNonStatic
+//             ^
+// [web] JS interop class 'StaticAbstractImplementsMultipleNonStatic' has an `@staticInterop` annotation, but has supertype 'NonStatic', which is non-static.
+// [web] JS interop class 'StaticAbstractImplementsMultipleNonStatic' has an `@staticInterop` annotation, but has supertype 'NonStaticAbstract', which is non-static.
+    implements
+        NonStaticAbstract,
+        NonStatic {}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractExtendsNonStaticAbstract
+//             ^
+// [web] JS interop class 'StaticAbstractExtendsNonStaticAbstract' has an `@staticInterop` annotation, but has supertype 'NonStaticAbstract', which is non-static.
+    extends NonStaticAbstract {}
diff --git a/tests/lib_2/js/static_interop_test/member_test.dart b/tests/lib_2/js/static_interop_test/member_test.dart
new file mode 100644
index 0000000..f5bf11e
--- /dev/null
+++ b/tests/lib_2/js/static_interop_test/member_test.dart
@@ -0,0 +1,54 @@
+// 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.
+
+// Test that instance members are disallowed from static interop classes.
+
+@JS()
+library member_test;
+
+import 'package:js/js.dart';
+
+@JS()
+@staticInterop
+class StaticJSClass {
+  external StaticJSClass();
+  external StaticJSClass.namedConstructor();
+  external factory StaticJSClass.externalFactory();
+  factory StaticJSClass.redirectingFactory() = StaticJSClass;
+  factory StaticJSClass.factory() => StaticJSClass();
+
+  static String staticField = 'staticField';
+  static String get staticGetSet => staticField;
+  static set staticGetSet(String val) => staticField = val;
+  static String staticMethod() => 'staticMethod';
+
+  external static String externalStaticField;
+  external static String get externalStaticGetSet;
+  external static set externalStaticGetSet(String val);
+  external static String externalStaticMethod();
+
+  external int get getter;
+  //               ^
+  // [web] JS interop class 'StaticJSClass' with `@staticInterop` annotation cannot declare instance members.
+  external set setter(_);
+  //           ^
+  // [web] JS interop class 'StaticJSClass' with `@staticInterop` annotation cannot declare instance members.
+  external int method();
+  //           ^
+  // [web] JS interop class 'StaticJSClass' with `@staticInterop` annotation cannot declare instance members.
+  external int field;
+  //           ^
+  // [web] JS interop class 'StaticJSClass' with `@staticInterop` annotation cannot declare instance members.
+}
+
+extension StaticJSClassExtension on StaticJSClass {
+  external String externalField;
+  external String get externalGetSet;
+  external set externalGetSet(String val);
+  external String externalMethod();
+
+  String get getSet => this.externalGetSet;
+  set getSet(String val) => this.externalGetSet = val;
+  String method() => 'method';
+}
diff --git a/tests/lib_2/js/static_interop_test/supertype_test.dart b/tests/lib_2/js/static_interop_test/supertype_test.dart
new file mode 100644
index 0000000..88e0085
--- /dev/null
+++ b/tests/lib_2/js/static_interop_test/supertype_test.dart
@@ -0,0 +1,114 @@
+// 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.
+
+// Test that interop classes that inherit/implement other classes have the
+// appropriate static interop static errors.
+
+@JS()
+library supertype_test;
+
+import 'package:js/js.dart';
+
+// Static base interop class.
+@JS()
+@staticInterop
+class Static {}
+
+// Non-static base interop class.
+@JS()
+class NonStatic {
+  external int instanceMethod();
+}
+
+@JS()
+@staticInterop
+class NonStaticMarkedAsStatic {
+  external int instanceMethod();
+  //           ^
+  // [web] JS interop class 'NonStaticMarkedAsStatic' with `@staticInterop` annotation cannot declare instance members.
+}
+
+// Static interop classes can inherit other static interop classes in order to
+// inherit its extension methods.
+@JS()
+@staticInterop
+class StaticExtendsStatic extends Static {}
+
+// Static interop classes are disallowed from extending non-static interop
+// classes.
+@JS()
+@staticInterop
+class StaticExtendsNonStatic extends NonStatic {}
+//    ^
+// [web] JS interop class 'StaticExtendsNonStatic' has an `@staticInterop` annotation, but has supertype 'NonStatic', which is non-static.
+
+// Static interop classes can implement each other in order to inherit extension
+// methods. Note that a non-abstract static interop class can not implement a
+// non-static class by definition, as it would need to contain an
+// implementation.
+@JS()
+@staticInterop
+class StaticImplementsStatic implements Static {}
+
+// Abstract classes should behave the same way as concrete classes.
+@JS()
+@staticInterop
+abstract class StaticAbstract {}
+
+// Abstract classes with instance members should be non-static. The following
+// have abstract or concrete members, so they're considered non-static.
+@JS()
+abstract class NonStaticAbstract {
+  int abstractMethod();
+}
+
+@JS()
+@staticInterop
+abstract class NonStaticAbstractWithAbstractMembers {
+  int abstractMethod();
+  //  ^
+  // [web] JS interop class 'NonStaticAbstractWithAbstractMembers' with `@staticInterop` annotation cannot declare instance members.
+}
+
+@JS()
+@staticInterop
+abstract class NonStaticAbstractWithConcreteMembers {
+  external int instanceMethod();
+  //           ^
+  // [web] JS interop class 'NonStaticAbstractWithConcreteMembers' with `@staticInterop` annotation cannot declare instance members.
+}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractImplementsStaticAbstract
+    implements StaticAbstract {}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractExtendsStaticAbstract extends StaticAbstract {}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractImplementsNonStaticAbstract
+//             ^
+// [web] JS interop class 'StaticAbstractImplementsNonStaticAbstract' has an `@staticInterop` annotation, but has supertype 'NonStaticAbstract', which is non-static.
+    implements
+        NonStaticAbstract {}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractImplementsMultipleNonStatic
+//             ^
+// [web] JS interop class 'StaticAbstractImplementsMultipleNonStatic' has an `@staticInterop` annotation, but has supertype 'NonStatic', which is non-static.
+// [web] JS interop class 'StaticAbstractImplementsMultipleNonStatic' has an `@staticInterop` annotation, but has supertype 'NonStaticAbstract', which is non-static.
+    implements
+        NonStaticAbstract,
+        NonStatic {}
+
+@JS()
+@staticInterop
+abstract class StaticAbstractExtendsNonStaticAbstract
+//             ^
+// [web] JS interop class 'StaticAbstractExtendsNonStaticAbstract' has an `@staticInterop` annotation, but has supertype 'NonStaticAbstract', which is non-static.
+    extends NonStaticAbstract {}
diff --git a/tools/VERSION b/tools/VERSION
index 1568b43..818d3bd 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 189
+PRERELEASE 190
 PRERELEASE_PATCH 0
\ No newline at end of file