Version 2.14.0-329.0.dev

Merge commit '885709755699276d5ceb7a4434443a6ccfcab469' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index be8938e..eb40ba7 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -11,7 +11,7 @@
     "constraint, update this by running tools/generate_package_config.dart."
   ],
   "configVersion": 2,
-  "generated": "2021-07-14T10:43:41.119864",
+  "generated": "2021-07-15T08:50:03.843620",
   "generator": "tools/generate_package_config.dart",
   "packages": [
     {
diff --git a/pkg/analyzer/lib/src/dart/element/element.dart b/pkg/analyzer/lib/src/dart/element/element.dart
index 2bdca7a..740d067 100644
--- a/pkg/analyzer/lib/src/dart/element/element.dart
+++ b/pkg/analyzer/lib/src/dart/element/element.dart
@@ -610,6 +610,42 @@
   @override
   bool get isDartCoreObject => !isMixin && supertype == null;
 
+  bool get isEnumLike {
+    // Must be a concrete class.
+    if (isAbstract || isMixin) {
+      return false;
+    }
+
+    // With only private non-factory constructors.
+    for (var constructor in constructors) {
+      if (constructor.isPublic || constructor.isFactory) {
+        return false;
+      }
+    }
+
+    // With 2+ static const fields with the type of this class.
+    var numberOfElements = 0;
+    for (var field in fields) {
+      if (field.isStatic && field.isConst && field.type == thisType) {
+        numberOfElements++;
+      }
+    }
+    if (numberOfElements < 2) {
+      return false;
+    }
+
+    // No subclasses in the library.
+    for (var unit in library.units) {
+      for (var class_ in unit.classes) {
+        if (class_.supertype?.element == this) {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
   @override
   bool get isMixinApplication {
     return hasModifier(Modifier.MIXIN_APPLICATION);
diff --git a/pkg/analyzer/test/src/dart/element/class_element_test.dart b/pkg/analyzer/test/src/dart/element/class_element_test.dart
index dbe96a2..c6ed690 100644
--- a/pkg/analyzer/test/src/dart/element/class_element_test.dart
+++ b/pkg/analyzer/test/src/dart/element/class_element_test.dart
@@ -3,7 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/src/dart/element/element.dart';
 import 'package:analyzer/src/error/codes.dart';
+import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import '../resolution/context_collection_resolution.dart';
@@ -16,6 +18,132 @@
 
 @reflectiveTest
 class ClassElementTest extends PubPackageResolutionTest {
+  test_isEnumLike_false_constructors_hasFactory() async {
+    await assertNoErrorsInCode('''
+class A {
+  factory A._foo() => A._bar();
+  A._bar();
+}
+
+void f() {
+  A._foo();
+}
+''');
+    _assertIsEnumLike(findElement.class_('A'), false);
+  }
+
+  test_isEnumLike_false_constructors_hasPublic() async {
+    await assertNoErrorsInCode('''
+class A {}
+''');
+    _assertIsEnumLike(findElement.class_('A'), false);
+  }
+
+  test_isEnumLike_false_fields_empty() async {
+    await assertNoErrorsInCode('''
+class A {
+  const A._();
+}
+
+void f() {
+  A._();
+}
+''');
+    _assertIsEnumLike(findElement.class_('A'), false);
+  }
+
+  test_isEnumLike_false_isAbstract() async {
+    await assertNoErrorsInCode('''
+abstract class A {}
+''');
+    _assertIsEnumLike(findElement.class_('A'), false);
+  }
+
+  test_isEnumLike_false_isExtended() async {
+    await assertNoErrorsInCode('''
+class A {
+  static const one = A._();
+  static const two = A._();
+  const A._();
+}
+
+class B extends A {
+  B() : super._();
+}
+''');
+    _assertIsEnumLike(findElement.class_('A'), false);
+  }
+
+  test_isEnumLike_false_isMixin() async {
+    await assertNoErrorsInCode('''
+mixin M {}
+''');
+    _assertIsEnumLike(findElement.mixin('M'), false);
+  }
+
+  test_isEnumLike_true() async {
+    await assertNoErrorsInCode('''
+class A {
+  static const one = A._();
+  static const two = A._();
+  const A._();
+}
+''');
+    _assertIsEnumLike(findElement.class_('A'), true);
+  }
+
+  test_isEnumLike_true_hasInstanceField() async {
+    await assertNoErrorsInCode('''
+class A {
+  static const one = A._(1);
+  static const two = A._(2);
+  final int f;
+  const A._(this.f);
+}
+''');
+    _assertIsEnumLike(findElement.class_('A'), true);
+  }
+
+  test_isEnumLike_true_hasSuperclass() async {
+    await assertNoErrorsInCode('''
+class A {
+  const A();
+}
+
+class B extends A {
+  static const one = B._();
+  static const two = B._();
+  const B._();
+}
+''');
+    _assertIsEnumLike(findElement.class_('B'), true);
+  }
+
+  test_isEnumLike_true_hasSyntheticField() async {
+    await assertNoErrorsInCode('''
+class A {
+  static const one = A._();
+  static const two = A._();
+  const A._();
+  int get foo => 0;
+}
+''');
+    _assertIsEnumLike(findElement.class_('A'), true);
+  }
+
+  test_isEnumLike_true_isImplemented() async {
+    await assertNoErrorsInCode('''
+class A {
+  static const one = A._();
+  static const two = A._();
+  const A._();
+}
+
+class B implements A {}
+''');
+    _assertIsEnumLike(findElement.class_('A'), true);
+  }
+
   test_lookUpInheritedConcreteGetter_declared() async {
     await assertNoErrorsInCode('''
 class A {
@@ -1196,6 +1324,10 @@
       A._lookUpInheritedMethod('foo'),
     );
   }
+
+  static void _assertIsEnumLike(ClassElement element, bool expected) {
+    expect((element as ClassElementImpl).isEnumLike, expected);
+  }
 }
 
 extension on ClassElement {
diff --git a/pkg/analyzer/test/verify_diagnostics_test.dart b/pkg/analyzer/test/verify_diagnostics_test.dart
index 4b57873..d8d7076 100644
--- a/pkg/analyzer/test/verify_diagnostics_test.dart
+++ b/pkg/analyzer/test/verify_diagnostics_test.dart
@@ -420,6 +420,7 @@
 /// codes.
 @reflectiveTest
 class VerifyDiagnosticsTest {
+  @TestTimeout(Timeout.factor(4))
   test_diagnostics() async {
     Context pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
     List<CodePath> codePaths = computeCodePaths();
diff --git a/pkg/compiler/lib/src/common_elements.dart b/pkg/compiler/lib/src/common_elements.dart
index c8cd6d7..8fdefc4 100644
--- a/pkg/compiler/lib/src/common_elements.dart
+++ b/pkg/compiler/lib/src/common_elements.dart
@@ -217,7 +217,7 @@
   InterfaceType getConstantListTypeFor(InterfaceType sourceType);
 
   InterfaceType getConstantMapTypeFor(InterfaceType sourceType,
-      {bool hasProtoKey: false, bool onlyStringKeys: false});
+      {bool onlyStringKeys: false});
 
   InterfaceType getConstantSetTypeFor(InterfaceType sourceType);
 
@@ -362,7 +362,6 @@
 
   ClassEntity get constantMapClass;
   ClassEntity get constantStringMapClass;
-  ClassEntity get constantProtoMapClass;
   ClassEntity get generalConstantMapClass;
 
   ClassEntity get annotationCreatesClass;
@@ -1037,10 +1036,9 @@
 
   @override
   InterfaceType getConstantMapTypeFor(InterfaceType sourceType,
-      {bool hasProtoKey: false, bool onlyStringKeys: false}) {
-    ClassEntity classElement = onlyStringKeys
-        ? (hasProtoKey ? constantProtoMapClass : constantStringMapClass)
-        : generalConstantMapClass;
+      {bool onlyStringKeys: false}) {
+    ClassEntity classElement =
+        onlyStringKeys ? constantStringMapClass : generalConstantMapClass;
     if (dartTypes.treatAsRawType(sourceType)) {
       return _env.getRawType(classElement);
     } else {
@@ -1619,9 +1617,6 @@
   ClassEntity get constantStringMapClass =>
       _findHelperClass(constant_system.JavaScriptMapConstant.DART_STRING_CLASS);
   @override
-  ClassEntity get constantProtoMapClass =>
-      _findHelperClass(constant_system.JavaScriptMapConstant.DART_PROTO_CLASS);
-  @override
   ClassEntity get generalConstantMapClass => _findHelperClass(
       constant_system.JavaScriptMapConstant.DART_GENERAL_CLASS);
 
diff --git a/pkg/compiler/lib/src/constants/constant_system.dart b/pkg/compiler/lib/src/constants/constant_system.dart
index b10c628..d2d5b8f 100644
--- a/pkg/compiler/lib/src/constants/constant_system.dart
+++ b/pkg/compiler/lib/src/constants/constant_system.dart
@@ -157,23 +157,10 @@
     InterfaceType sourceType,
     List<ConstantValue> keys,
     List<ConstantValue> values) {
-  bool onlyStringKeys = true;
-  ConstantValue protoValue = null;
-  for (int i = 0; i < keys.length; i++) {
-    dynamic key = keys[i];
-    if (key.isString) {
-      if (key.stringValue == JavaScriptMapConstant.PROTO_PROPERTY) {
-        protoValue = values[i];
-      }
-    } else {
-      onlyStringKeys = false;
-      // Don't handle __proto__ values specially in the general map case.
-      protoValue = null;
-      break;
-    }
-  }
+  bool onlyStringKeys = keys.every((key) =>
+      key is StringConstantValue &&
+      key.stringValue != JavaScriptMapConstant.PROTO_PROPERTY);
 
-  bool hasProtoKey = (protoValue != null);
   InterfaceType keysType;
   if (commonElements.dartTypes.treatAsRawType(sourceType)) {
     keysType = commonElements.listType();
@@ -182,9 +169,8 @@
   }
   ListConstantValue keysList = createList(commonElements, keysType, keys);
   InterfaceType type = commonElements.getConstantMapTypeFor(sourceType,
-      hasProtoKey: hasProtoKey, onlyStringKeys: onlyStringKeys);
-  return new JavaScriptMapConstant(
-      type, keysList, values, protoValue, onlyStringKeys);
+      onlyStringKeys: onlyStringKeys);
+  return JavaScriptMapConstant(type, keysList, values, onlyStringKeys);
 }
 
 ConstantValue createSymbol(CommonElements commonElements, String text) {
@@ -1035,20 +1021,17 @@
   /// The dart class implementing constant map literals.
   static const String DART_CLASS = "ConstantMap";
   static const String DART_STRING_CLASS = "ConstantStringMap";
-  static const String DART_PROTO_CLASS = "ConstantProtoMap";
   static const String DART_GENERAL_CLASS = "GeneralConstantMap";
   static const String LENGTH_NAME = "_length";
   static const String JS_OBJECT_NAME = "_jsObject";
   static const String KEYS_NAME = "_keys";
-  static const String PROTO_VALUE = "_protoValue";
   static const String JS_DATA_NAME = "_jsData";
 
   final ListConstantValue keyList;
-  final ConstantValue protoValue;
   final bool onlyStringKeys;
 
   JavaScriptMapConstant(InterfaceType type, ListConstantValue keyList,
-      List<ConstantValue> values, this.protoValue, this.onlyStringKeys)
+      List<ConstantValue> values, this.onlyStringKeys)
       : this.keyList = keyList,
         super(type, keyList.entries, values);
   @override
diff --git a/pkg/compiler/lib/src/constants/values.dart b/pkg/compiler/lib/src/constants/values.dart
index f33c13b..86636d0 100644
--- a/pkg/compiler/lib/src/constants/values.dart
+++ b/pkg/compiler/lib/src/constants/values.dart
@@ -741,8 +741,7 @@
   int get length => keys.length;
 
   ConstantValue lookup(ConstantValue key) {
-    var lookupMap = _lookupMap ??=
-        new Map<ConstantValue, ConstantValue>.fromIterables(keys, values);
+    var lookupMap = _lookupMap ??= Map.fromIterables(keys, values);
     return lookupMap[key];
   }
 
diff --git a/pkg/compiler/lib/src/js_backend/backend_impact.dart b/pkg/compiler/lib/src/js_backend/backend_impact.dart
index d50ddc1b..4dcff21 100644
--- a/pkg/compiler/lib/src/js_backend/backend_impact.dart
+++ b/pkg/compiler/lib/src/js_backend/backend_impact.dart
@@ -284,7 +284,6 @@
   BackendImpact get constantMapLiteral {
     return _constantMapLiteral ??= new BackendImpact(instantiatedClasses: [
       _commonElements.constantMapClass,
-      _commonElements.constantProtoMapClass,
       _commonElements.constantStringMapClass,
       _commonElements.generalConstantMapClass,
     ]);
diff --git a/pkg/compiler/lib/src/js_backend/constant_emitter.dart b/pkg/compiler/lib/src/js_backend/constant_emitter.dart
index dd67c4a..b05bc22 100644
--- a/pkg/compiler/lib/src/js_backend/constant_emitter.dart
+++ b/pkg/compiler/lib/src/js_backend/constant_emitter.dart
@@ -330,10 +330,6 @@
           constant_system.JavaScriptMapConstant.KEYS_NAME) {
         arguments.add(_constantReferenceGenerator(constant.keyList));
       } else if (field.name ==
-          constant_system.JavaScriptMapConstant.PROTO_VALUE) {
-        assert(constant.protoValue != null);
-        arguments.add(_constantReferenceGenerator(constant.protoValue));
-      } else if (field.name ==
           constant_system.JavaScriptMapConstant.JS_DATA_NAME) {
         arguments.add(jsGeneralMap());
       } else {
@@ -344,8 +340,6 @@
     });
     if ((className == constant_system.JavaScriptMapConstant.DART_STRING_CLASS &&
             emittedArgumentCount != 3) ||
-        (className == constant_system.JavaScriptMapConstant.DART_PROTO_CLASS &&
-            emittedArgumentCount != 4) ||
         (className ==
                 constant_system.JavaScriptMapConstant.DART_GENERAL_CLASS &&
             emittedArgumentCount != 1)) {
diff --git a/pkg/compiler/lib/src/js_model/js_world_builder.dart b/pkg/compiler/lib/src/js_model/js_world_builder.dart
index 8a4928d..f125f55 100644
--- a/pkg/compiler/lib/src/js_model/js_world_builder.dart
+++ b/pkg/compiler/lib/src/js_model/js_world_builder.dart
@@ -944,15 +944,13 @@
     DartType type = typeConverter.visit(constant.type, toBackendEntity);
     ListConstantValue keys = constant.keyList.accept(this, null);
     List<ConstantValue> values = _handleValues(constant.values);
-    ConstantValue protoValue = constant.protoValue?.accept(this, null);
     if (identical(keys, constant.keys) &&
         identical(values, constant.values) &&
-        type == constant.type &&
-        protoValue == constant.protoValue) {
+        type == constant.type) {
       return constant;
     }
     return new constant_system.JavaScriptMapConstant(
-        type, keys, values, protoValue, constant.onlyStringKeys);
+        type, keys, values, constant.onlyStringKeys);
   }
 
   @override
diff --git a/pkg/compiler/lib/src/serialization/abstract_sink.dart b/pkg/compiler/lib/src/serialization/abstract_sink.dart
index 1bd824c..d4a4746 100644
--- a/pkg/compiler/lib/src/serialization/abstract_sink.dart
+++ b/pkg/compiler/lib/src/serialization/abstract_sink.dart
@@ -506,7 +506,6 @@
         writeDartType(constant.type);
         writeConstant(constant.keyList);
         writeConstants(constant.values);
-        writeConstantOrNull(constant.protoValue);
         writeBool(constant.onlyStringKeys);
         break;
       case ConstantValueKind.CONSTRUCTED:
diff --git a/pkg/compiler/lib/src/serialization/abstract_source.dart b/pkg/compiler/lib/src/serialization/abstract_source.dart
index a06007f..ed14321 100644
--- a/pkg/compiler/lib/src/serialization/abstract_source.dart
+++ b/pkg/compiler/lib/src/serialization/abstract_source.dart
@@ -519,10 +519,9 @@
         DartType type = readDartType();
         ListConstantValue keyList = readConstant();
         List<ConstantValue> values = readConstants();
-        ConstantValue protoValue = readConstantOrNull();
         bool onlyStringKeys = readBool();
         return new constant_system.JavaScriptMapConstant(
-            type, keyList, values, protoValue, onlyStringKeys);
+            type, keyList, values, onlyStringKeys);
       case ConstantValueKind.CONSTRUCTED:
         InterfaceType type = readDartType();
         Map<FieldEntity, ConstantValue> fields =
diff --git a/pkg/compiler/test/impact/data/constants/main.dart b/pkg/compiler/test/impact/data/constants/main.dart
index a29bd63..be9a623 100644
--- a/pkg/compiler/test/impact/data/constants/main.dart
+++ b/pkg/compiler/test/impact/data/constants/main.dart
@@ -108,13 +108,27 @@
   inst:List<bool*>]*/
 listLiteral() => const [true, false];
 
-/*member: mapLiteral:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool]*/
+/*member: mapLiteral:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool]*/
 mapLiteral() => const {true: false};
 
-/*member: stringMapLiteral:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool,inst:JSString]*/
+/*member: stringMapLiteral:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool,
+  inst:JSString]*/
 stringMapLiteral() => const {'foo': false};
 
-/*member: setLiteral:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool,inst:_UnmodifiableSet<dynamic>]*/
+/*member: setLiteral:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool,
+  inst:_UnmodifiableSet<dynamic>]*/
 setLiteral() => const {true, false};
 
 /*member: instanceConstant:
@@ -190,13 +204,27 @@
   inst:List<bool*>]*/
 listLiteralRef() => listLiteralField;
 
-/*member: mapLiteralRef:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool]*/
+/*member: mapLiteralRef:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool]*/
 mapLiteralRef() => mapLiteralField;
 
-/*member: stringMapLiteralRef:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool,inst:JSString]*/
+/*member: stringMapLiteralRef:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool,
+  inst:JSString]*/
 stringMapLiteralRef() => stringMapLiteralField;
 
-/*member: setLiteralRef:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool,inst:_UnmodifiableSet<dynamic>]*/
+/*member: setLiteralRef:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool,
+  inst:_UnmodifiableSet<dynamic>]*/
 setLiteralRef() => setLiteralField;
 
 /*member: instanceConstantRef:
@@ -263,15 +291,29 @@
 listLiteralDeferred() => defer.listLiteralField;
 
 // TODO(johnniwinther): Should we record that this is deferred?
-/*member: mapLiteralDeferred:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool]*/
+/*member: mapLiteralDeferred:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool]*/
 mapLiteralDeferred() => defer.mapLiteralField;
 
 // TODO(johnniwinther): Should we record that this is deferred?
-/*member: stringMapLiteralDeferred:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool,inst:JSString]*/
+/*member: stringMapLiteralDeferred:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool,
+  inst:JSString]*/
 stringMapLiteralDeferred() => defer.stringMapLiteralField;
 
 // TODO(johnniwinther): Should we record that this is deferred?
-/*member: setLiteralDeferred:type=[inst:ConstantMap<dynamic,dynamic>,inst:ConstantProtoMap<dynamic,dynamic>,inst:ConstantStringMap<dynamic,dynamic>,inst:GeneralConstantMap<dynamic,dynamic>,inst:JSBool,inst:_UnmodifiableSet<dynamic>]*/
+/*member: setLiteralDeferred:type=[
+  inst:ConstantMap<dynamic,dynamic>,
+  inst:ConstantStringMap<dynamic,dynamic>,
+  inst:GeneralConstantMap<dynamic,dynamic>,
+  inst:JSBool,
+  inst:_UnmodifiableSet<dynamic>]*/
 setLiteralDeferred() => defer.setLiteralField;
 
 /*member: instanceConstantDeferred:
diff --git a/pkg/compiler/test/impact/data/literals.dart b/pkg/compiler/test/impact/data/literals.dart
index 05b7c0c..e0e3ba5 100644
--- a/pkg/compiler/test/impact/data/literals.dart
+++ b/pkg/compiler/test/impact/data/literals.dart
@@ -179,7 +179,6 @@
 /*member: testEmptyMapLiteralConstant:
 type=[
  inst:ConstantMap<dynamic,dynamic>,
- inst:ConstantProtoMap<dynamic,dynamic>,
  inst:ConstantStringMap<dynamic,dynamic>,
  inst:GeneralConstantMap<dynamic,dynamic>]
 */
diff --git a/pkg/pkg.status b/pkg/pkg.status
index 79e9538..8386f5a 100644
--- a/pkg/pkg.status
+++ b/pkg/pkg.status
@@ -66,6 +66,7 @@
 kernel/testcases/*: Skip # These are not tests but input for tests.
 vm/test/transformations/type_flow/transformer_test: Slow, Pass
 vm/testcases/*: SkipByDesign # These are not tests but input for tests.
+vm_service/test/cpu_samples_stream_test: Slow, Pass # Requires CPU sample buffer to fill.
 wasm/*: SkipByDesign # These can't be run without running wasm:setup first.
 
 [ $compiler == dart2analyzer ]
diff --git a/pkg/vm_service/example/vm_service_assert.dart b/pkg/vm_service/example/vm_service_assert.dart
index b458ad6..8914a67 100644
--- a/pkg/vm_service/example/vm_service_assert.dart
+++ b/pkg/vm_service/example/vm_service_assert.dart
@@ -137,6 +137,7 @@
   if (obj == "BreakpointRemoved") return obj;
   if (obj == "BreakpointResolved") return obj;
   if (obj == "BreakpointUpdated") return obj;
+  if (obj == "CpuSamples") return obj;
   if (obj == "Extension") return obj;
   if (obj == "GC") return obj;
   if (obj == "Inspect") return obj;
diff --git a/pkg/vm_service/java/version.properties b/pkg/vm_service/java/version.properties
index 62cbe79..5e787ed 100644
--- a/pkg/vm_service/java/version.properties
+++ b/pkg/vm_service/java/version.properties
@@ -1 +1 @@
-version=3.48
+version=3.49
diff --git a/pkg/vm_service/lib/src/vm_service.dart b/pkg/vm_service/lib/src/vm_service.dart
index 70c6db8..c80ebed 100644
--- a/pkg/vm_service/lib/src/vm_service.dart
+++ b/pkg/vm_service/lib/src/vm_service.dart
@@ -26,7 +26,7 @@
         HeapSnapshotObjectNoData,
         HeapSnapshotObjectNullData;
 
-const String vmServiceVersion = '3.48.0';
+const String vmServiceVersion = '3.49.0';
 
 /// @optional
 const String optional = 'optional';
@@ -1149,7 +1149,7 @@
   /// Debug | PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted,
   /// PauseException, PausePostRequest, Resume, BreakpointAdded,
   /// BreakpointResolved, BreakpointRemoved, BreakpointUpdated, Inspect, None
-  /// Profiler | UserTagChanged
+  /// Profiler | CpuSamples, UserTagChanged
   /// GC | GC
   /// Extension | Extension
   /// Timeline | TimelineEvents, TimelineStreamsSubscriptionUpdate
@@ -1690,7 +1690,7 @@
   // PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted, PauseException, PausePostRequest, Resume, BreakpointAdded, BreakpointResolved, BreakpointRemoved, BreakpointUpdated, Inspect, None
   Stream<Event> get onDebugEvent => _getEventController('Debug').stream;
 
-  // UserTagChanged
+  // CpuSamples, UserTagChanged
   Stream<Event> get onProfilerEvent => _getEventController('Profiler').stream;
 
   // GC
@@ -2502,6 +2502,9 @@
 
   /// Notification that the UserTag for an isolate has been changed.
   static const String kUserTagChanged = 'UserTagChanged';
+
+  /// A block of recently collected CPU samples.
+  static const String kCpuSamples = 'CpuSamples';
 }
 
 /// Adding new values to `InstanceKind` is considered a backwards compatible
@@ -3899,6 +3902,10 @@
   @optional
   String? previousTag;
 
+  /// A CPU profile containing recent samples.
+  @optional
+  CpuSamples? cpuSamples;
+
   /// Binary data associated with the event.
   ///
   /// This is provided for the event kinds:
@@ -3933,6 +3940,7 @@
     this.last,
     this.updatedTag,
     this.previousTag,
+    this.cpuSamples,
     this.data,
   });
 
@@ -3977,6 +3985,8 @@
     last = json['last'];
     updatedTag = json['updatedTag'];
     previousTag = json['previousTag'];
+    cpuSamples = createServiceObject(json['cpuSamples'], const ['CpuSamples'])
+        as CpuSamples?;
     data = json['data'];
   }
 
@@ -4018,6 +4028,7 @@
     _setIfNotNull(json, 'last', last);
     _setIfNotNull(json, 'updatedTag', updatedTag);
     _setIfNotNull(json, 'previousTag', previousTag);
+    _setIfNotNull(json, 'cpuSamples', cpuSamples?.toJson());
     _setIfNotNull(json, 'data', data);
     return json;
   }
diff --git a/pkg/vm_service/test/cpu_samples_stream_test.dart b/pkg/vm_service/test/cpu_samples_stream_test.dart
new file mode 100644
index 0000000..d6846ef
--- /dev/null
+++ b/pkg/vm_service/test/cpu_samples_stream_test.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+
+import 'common/service_test_common.dart';
+import 'common/test_helper.dart';
+
+fib(int n) {
+  if (n <= 1) {
+    return n;
+  }
+  return fib(n - 1) + fib(n - 2);
+}
+
+void testMain() async {
+  int i = 10;
+  while (true) {
+    ++i;
+    // Create progressively deeper stacks to more quickly fill the sample
+    // buffer.
+    fib(i);
+  }
+}
+
+late StreamSubscription sub;
+
+var tests = <IsolateTest>[
+  (VmService service, IsolateRef isolate) async {
+    final completer = Completer<void>();
+    int count = 0;
+    int previousOrigin = 0;
+    sub = service.onProfilerEvent.listen((event) async {
+      count++;
+      expect(event.kind, EventKind.kCpuSamples);
+      expect(event.cpuSamples, isNotNull);
+      expect(event.cpuSamples!.samples!.isNotEmpty, true);
+      if (previousOrigin != 0) {
+        expect(
+          event.cpuSamples!.timeOriginMicros! >= previousOrigin,
+          true,
+        );
+      }
+      previousOrigin = event.cpuSamples!.timeOriginMicros!;
+
+      if (count == 2) {
+        await sub.cancel();
+        completer.complete();
+      }
+    });
+    await service.streamListen(EventStreams.kProfiler);
+
+    await completer.future;
+    await service.streamCancel(EventStreams.kProfiler);
+  },
+];
+
+main([args = const <String>[]]) async => await runIsolateTests(
+      args,
+      tests,
+      'cpu_samples_stream_test.dart',
+      testeeConcurrent: testMain,
+      extraArgs: [
+        '--sample-buffer-duration=1',
+        '--profile-vm',
+      ],
+    );
diff --git a/runtime/observatory/tests/service/get_version_rpc_test.dart b/runtime/observatory/tests/service/get_version_rpc_test.dart
index 75d1743..f153eac 100644
--- a/runtime/observatory/tests/service/get_version_rpc_test.dart
+++ b/runtime/observatory/tests/service/get_version_rpc_test.dart
@@ -12,7 +12,7 @@
     final result = await vm.invokeRpcNoUpgrade('getVersion', {});
     expect(result['type'], 'Version');
     expect(result['major'], 3);
-    expect(result['minor'], 48);
+    expect(result['minor'], 49);
     expect(result['_privateMajor'], 0);
     expect(result['_privateMinor'], 0);
   },
diff --git a/runtime/observatory_2/tests/service_2/get_version_rpc_test.dart b/runtime/observatory_2/tests/service_2/get_version_rpc_test.dart
index 443f8bb..a36deeb 100644
--- a/runtime/observatory_2/tests/service_2/get_version_rpc_test.dart
+++ b/runtime/observatory_2/tests/service_2/get_version_rpc_test.dart
@@ -12,7 +12,7 @@
     final result = await vm.invokeRpcNoUpgrade('getVersion', {});
     expect(result['type'], equals('Version'));
     expect(result['major'], equals(3));
-    expect(result['minor'], equals(48));
+    expect(result['minor'], equals(49));
     expect(result['_privateMajor'], equals(0));
     expect(result['_privateMinor'], equals(0));
   },
diff --git a/runtime/platform/atomic.h b/runtime/platform/atomic.h
index ffb5bdc..390aa62 100644
--- a/runtime/platform/atomic.h
+++ b/runtime/platform/atomic.h
@@ -45,6 +45,10 @@
     return value_.fetch_and(arg, order);
   }
 
+  T exchange(T arg, std::memory_order order = std::memory_order_relaxed) {
+    return value_.exchange(arg, order);
+  }
+
   bool compare_exchange_weak(
       T& expected,  // NOLINT
       T desired,
@@ -76,6 +80,10 @@
   }
   T operator+=(T arg) { return fetch_add(arg) + arg; }
   T operator-=(T arg) { return fetch_sub(arg) - arg; }
+  T& operator++() { return fetch_add(1) + 1; }
+  T& operator--() { return fetch_sub(1) - 1; }
+  T operator++(int) { return fetch_add(1); }
+  T operator--(int) { return fetch_sub(1); }
 
  private:
   std::atomic<T> value_;
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index dd85b96..dffed84 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -2383,6 +2383,25 @@
                          reinterpret_cast<uword>(this));
 }
 
+#if !defined(PRODUCT)
+void Isolate::set_current_sample_block(SampleBlock* current) {
+  ASSERT(current_sample_block_lock_.IsOwnedByCurrentThread());
+  if (current != nullptr) {
+    current->set_is_allocation_block(false);
+    current->set_owner(this);
+  }
+  current_sample_block_ = current;
+}
+
+void Isolate::set_current_allocation_sample_block(SampleBlock* current) {
+  if (current != nullptr) {
+    current->set_is_allocation_block(true);
+    current->set_owner(this);
+  }
+  current_allocation_sample_block_ = current;
+}
+#endif  // !defined(PRODUCT)
+
 // static
 void Isolate::NotifyLowMemory() {
   Isolate::KillAllIsolates(Isolate::kLowMemoryMsg);
@@ -2516,11 +2535,12 @@
 }
 
 void Isolate::LowLevelCleanup(Isolate* isolate) {
-#if !defined(DART_PECOMPILED_RUNTIME)
+#if !defined(DART_PRECOMPILED_RUNTIME)
   if (KernelIsolate::IsKernelIsolate(isolate)) {
     KernelIsolate::SetKernelIsolate(nullptr);
+  }
 #endif
-  } else if (ServiceIsolate::IsServiceIsolate(isolate)) {
+  if (ServiceIsolate::IsServiceIsolate(isolate)) {
     ServiceIsolate::SetServiceIsolate(nullptr);
   }
 
@@ -2538,6 +2558,26 @@
   // requests anymore.
   Thread::ExitIsolate();
 
+#if !defined(PRODUCT)
+  // Cleanup profiler state.
+  SampleBlock* cpu_block = isolate->current_sample_block();
+  if (cpu_block != nullptr) {
+    cpu_block->release_block();
+  }
+  SampleBlock* allocation_block = isolate->current_allocation_sample_block();
+  if (allocation_block != nullptr) {
+    allocation_block->release_block();
+  }
+
+  // Process the previously assigned sample blocks if we're using the
+  // profiler's sample buffer. Some tests create their own SampleBlockBuffer
+  // and handle block processing themselves.
+  if ((cpu_block != nullptr || allocation_block != nullptr) &&
+      Profiler::sample_block_buffer() != nullptr) {
+    Profiler::sample_block_buffer()->ProcessCompletedBlocks();
+  }
+#endif  // !defined(PRODUCT)
+
   // Now it's safe to delete the isolate.
   delete isolate;
 
diff --git a/runtime/vm/isolate.h b/runtime/vm/isolate.h
index 09dbbcd..261b14f 100644
--- a/runtime/vm/isolate.h
+++ b/runtime/vm/isolate.h
@@ -74,6 +74,8 @@
 class SafepointRwLock;
 class SafepointHandler;
 class SampleBuffer;
+class SampleBlock;
+class SampleBlockBuffer;
 class SendPort;
 class SerializedObjectBuffer;
 class ServiceIdZone;
@@ -1084,6 +1086,27 @@
 #if !defined(PRODUCT)
   Debugger* debugger() const { return debugger_; }
 
+  // NOTE: this lock should only be acquired within the profiler signal handler.
+  Mutex* current_sample_block_lock() const {
+    return const_cast<Mutex*>(&current_sample_block_lock_);
+  }
+
+  // Returns the current SampleBlock used to track CPU profiling samples.
+  //
+  // NOTE: current_sample_block_lock() should be held when accessing this
+  // block.
+  SampleBlock* current_sample_block() const { return current_sample_block_; }
+  void set_current_sample_block(SampleBlock* current);
+
+  // Returns the current SampleBlock used to track Dart allocation samples.
+  //
+  // Allocations should only occur on the mutator thread for an isolate, so we
+  // don't need to worry about grabbing a lock while accessing this block.
+  SampleBlock* current_allocation_sample_block() const {
+    return current_allocation_sample_block_;
+  }
+  void set_current_allocation_sample_block(SampleBlock* current);
+
   void set_single_step(bool value) { single_step_ = value; }
   bool single_step() const { return single_step_; }
   static intptr_t single_step_offset() {
@@ -1528,6 +1551,21 @@
 // the top.
 #if !defined(PRODUCT)
   Debugger* debugger_ = nullptr;
+
+  // SampleBlock containing CPU profiling samples.
+  //
+  // Can be accessed by multiple threads, so current_sample_block_lock_ should
+  // be acquired before accessing.
+  SampleBlock* current_sample_block_ = nullptr;
+  Mutex current_sample_block_lock_;
+
+  // SampleBlock containing Dart allocation profiling samples.
+  //
+  // Allocations should only occur on the mutator thread for an isolate, so we
+  // shouldn't need to worry about grabbing a lock for the allocation sample
+  // block.
+  SampleBlock* current_allocation_sample_block_ = nullptr;
+
   int64_t last_resume_timestamp_;
 
   VMTagCounters vm_tag_counters_;
diff --git a/runtime/vm/malloc_hooks_test.cc b/runtime/vm/malloc_hooks_test.cc
index 8edd992..d04ab64 100644
--- a/runtime/vm/malloc_hooks_test.cc
+++ b/runtime/vm/malloc_hooks_test.cc
@@ -176,7 +176,7 @@
   EnableMallocHooksAndStacksScope scope;
 
   ClearProfileVisitor cpv(Isolate::Current());
-  Profiler::sample_buffer()->VisitSamples(&cpv);
+  Profiler::sample_block_buffer()->VisitSamples(&cpv);
 
   char* var = static_cast<char*>(malloc(16 * sizeof(char)));
   JSONStream js;
diff --git a/runtime/vm/profiler.cc b/runtime/vm/profiler.cc
index 532eb63..4590a76 100644
--- a/runtime/vm/profiler.cc
+++ b/runtime/vm/profiler.cc
@@ -19,6 +19,7 @@
 #include "vm/object.h"
 #include "vm/os.h"
 #include "vm/profiler.h"
+#include "vm/profiler_service.h"
 #include "vm/reusable_handles.h"
 #include "vm/signal_handler.h"
 #include "vm/simulator.h"
@@ -61,8 +62,8 @@
 #ifndef PRODUCT
 
 RelaxedAtomic<bool> Profiler::initialized_ = false;
-SampleBuffer* Profiler::sample_buffer_ = NULL;
-AllocationSampleBuffer* Profiler::allocation_sample_buffer_ = NULL;
+SampleBlockBuffer* Profiler::sample_block_buffer_ = nullptr;
+AllocationSampleBuffer* Profiler::allocation_sample_buffer_ = nullptr;
 ProfilerCounters Profiler::counters_ = {};
 
 void Profiler::Init() {
@@ -75,9 +76,9 @@
   SetSamplePeriod(FLAG_profile_period);
   // The profiler may have been shutdown previously, in which case the sample
   // buffer will have already been initialized.
-  if (sample_buffer_ == NULL) {
-    intptr_t capacity = CalculateSampleBufferCapacity();
-    sample_buffer_ = new SampleBuffer(capacity);
+  if (sample_block_buffer_ == nullptr) {
+    intptr_t num_blocks = CalculateSampleBufferCapacity();
+    sample_block_buffer_ = new SampleBlockBuffer(num_blocks);
     Profiler::InitAllocationSampleBuffer();
   }
   ThreadInterrupter::Init();
@@ -92,14 +93,30 @@
   }
 }
 
+class SampleBlockCleanupVisitor : public IsolateVisitor {
+ public:
+  SampleBlockCleanupVisitor() = default;
+  virtual ~SampleBlockCleanupVisitor() = default;
+
+  void VisitIsolate(Isolate* isolate) {
+    isolate->set_current_allocation_sample_block(nullptr);
+    {
+      MutexLocker ml(isolate->current_sample_block_lock());
+      isolate->set_current_sample_block(nullptr);
+    }
+  }
+};
+
 void Profiler::Cleanup() {
   if (!FLAG_profiler) {
     return;
   }
   ASSERT(initialized_);
   ThreadInterrupter::Cleanup();
-  delete sample_buffer_;
-  sample_buffer_ = NULL;
+  SampleBlockCleanupVisitor visitor;
+  Isolate::VisitIsolates(&visitor);
+  delete sample_block_buffer_;
+  sample_block_buffer_ = nullptr;
   initialized_ = false;
 }
 
@@ -130,16 +147,16 @@
 
 intptr_t Profiler::CalculateSampleBufferCapacity() {
   if (FLAG_sample_buffer_duration <= 0) {
-    return SampleBuffer::kDefaultBufferCapacity;
+    return SampleBlockBuffer::kDefaultBlockCount;
   }
   // Deeper stacks require more than a single Sample object to be represented
   // correctly. These samples are chained, so we need to determine the worst
   // case sample chain length for a single stack.
   const intptr_t max_sample_chain_length =
       FLAG_max_profile_depth / kMaxSamplesPerTick;
-  const intptr_t buffer_size = FLAG_sample_buffer_duration *
-                               SamplesPerSecond() * max_sample_chain_length;
-  return buffer_size;
+  const intptr_t sample_count = FLAG_sample_buffer_duration *
+                                SamplesPerSecond() * max_sample_chain_length;
+  return (sample_count / SampleBlock::kSamplesPerBlock) + 1;
 }
 
 void Profiler::SetSamplePeriod(intptr_t period) {
@@ -156,7 +173,166 @@
   SetSamplePeriod(FLAG_profile_period);
 }
 
-SampleBuffer::SampleBuffer(intptr_t capacity) {
+SampleBlockBuffer::SampleBlockBuffer(intptr_t blocks,
+                                     intptr_t samples_per_block) {
+  const intptr_t size = Utils::RoundUp(
+      blocks * samples_per_block * sizeof(Sample), VirtualMemory::PageSize());
+  const bool kNotExecutable = false;
+  memory_ = VirtualMemory::Allocate(size, kNotExecutable, "dart-profiler");
+  if (memory_ == NULL) {
+    OUT_OF_MEMORY();
+  }
+  sample_buffer_ = reinterpret_cast<Sample*>(memory_->address());
+  blocks_ = new SampleBlock[blocks];
+  for (intptr_t i = 0; i < blocks; ++i) {
+    blocks_[i].Init(&sample_buffer_[i * samples_per_block], samples_per_block);
+  }
+  capacity_ = blocks;
+  cursor_ = 0;
+  free_list_head_ = nullptr;
+  free_list_tail_ = nullptr;
+}
+
+SampleBlockBuffer::~SampleBlockBuffer() {
+  delete[] blocks_;
+  blocks_ = nullptr;
+  capacity_ = 0;
+  cursor_ = 0;
+}
+
+SampleBlock* SampleBlockBuffer::ReserveSampleBlock() {
+  // Don't increment right away to avoid unlikely wrap-around errors.
+  if (cursor_.load() < capacity_) {
+    intptr_t index = cursor_.fetch_add(1u);
+    // Check the index again to make sure the last block hasn't been snatched
+    // from underneath us.
+    if (index < capacity_) {
+      return &blocks_[index];
+    }
+  }
+  // Try to re-use a previously freed SampleBlock once we've handed out each
+  // block at least once. Freed blocks aren't cleared immediately and are still
+  // valid until they're re-allocated, similar to how a ring buffer would clear
+  // the oldest samples.
+  SampleBlock* block = GetFreeBlock();
+  if (block != nullptr) {
+    block->Clear();
+  }
+  return block;
+}
+
+void SampleBlockBuffer::ProcessCompletedBlocks() {
+  Thread* thread = Thread::Current();
+  int64_t start = Dart_TimelineGetMicros();
+  for (intptr_t i = 0; i < capacity_; ++i) {
+    SampleBlock* block = &blocks_[i];
+    if (block->is_full() && !block->evictable()) {
+      if (Service::profiler_stream.enabled()) {
+        Profile profile(block->owner());
+        profile.Build(thread, nullptr, block);
+        ServiceEvent event(block->owner(), ServiceEvent::kCpuSamples);
+        event.set_cpu_profile(&profile);
+        Service::HandleEvent(&event);
+      }
+      block->evictable_ = true;
+      FreeBlock(block);
+    }
+  }
+  int64_t end = Dart_TimelineGetMicros();
+  Dart_TimelineEvent("SampleBlockBuffer::ProcessCompletedBlocks", start, end,
+                     Dart_Timeline_Event_Duration, 0, nullptr, nullptr);
+}
+
+ProcessedSampleBuffer* SampleBlockBuffer::BuildProcessedSampleBuffer(
+    SampleFilter* filter,
+    ProcessedSampleBuffer* buffer) {
+  ASSERT(filter != NULL);
+  Thread* thread = Thread::Current();
+  Zone* zone = thread->zone();
+
+  if (buffer == nullptr) {
+    buffer = new (zone) ProcessedSampleBuffer();
+  }
+
+  for (intptr_t i = 0; i < capacity_; ++i) {
+    (&blocks_[i])->BuildProcessedSampleBuffer(filter, buffer);
+  }
+
+  return buffer;
+}
+
+Sample* SampleBlock::ReserveSample() {
+  if (full_.load()) {
+    return nullptr;
+  }
+  intptr_t slot = cursor_.fetch_add(1u);
+  if (slot + 1 == capacity_) {
+    full_ = true;
+  }
+  return (slot < capacity_) ? At(slot) : nullptr;
+}
+
+Sample* SampleBlock::ReserveSampleAndLink(Sample* previous) {
+  ASSERT(previous != nullptr);
+  SampleBlockBuffer* buffer = Profiler::sample_block_buffer();
+  Isolate* isolate = owner_;
+  ASSERT(isolate != nullptr);
+  Sample* next = previous->is_allocation_sample()
+                     ? buffer->ReserveAllocationSample(isolate)
+                     : buffer->ReserveCPUSample(isolate);
+  next->Init(previous->port(), previous->timestamp(), previous->tid());
+  next->set_head_sample(false);
+  // Mark that previous continues at next.
+  previous->SetContinuation(next);
+  return next;
+}
+
+Sample* SampleBlockBuffer::ReserveCPUSample(Isolate* isolate) {
+  return ReserveSampleImpl(isolate, false);
+}
+
+Sample* SampleBlockBuffer::ReserveAllocationSample(Isolate* isolate) {
+  return ReserveSampleImpl(isolate, true);
+}
+
+Sample* SampleBlockBuffer::ReserveSampleImpl(Isolate* isolate,
+                                             bool allocation_sample) {
+  SampleBlock* block = allocation_sample
+                           ? isolate->current_allocation_sample_block()
+                           : isolate->current_sample_block();
+  Sample* sample = nullptr;
+  if (block != nullptr) {
+    sample = block->ReserveSample();
+  }
+  if (sample != nullptr) {
+    return sample;
+  }
+  SampleBlock* next = nullptr;
+  if (allocation_sample) {
+    // We only need to be locked while accessing the CPU sample block since
+    // Dart allocations can only occur on the mutator thread.
+    next = ReserveSampleBlock();
+    if (next == nullptr) {
+      // We're out of blocks to reserve. Drop the sample.
+      return nullptr;
+    }
+    isolate->set_current_allocation_sample_block(next);
+  } else {
+    MutexLocker locker(isolate->current_sample_block_lock());
+    next = ReserveSampleBlock();
+    if (next == nullptr) {
+      // We're out of blocks to reserve. Drop the sample.
+      return nullptr;
+    }
+    isolate->set_current_sample_block(next);
+  }
+  next->set_is_allocation_block(allocation_sample);
+  can_process_block_.store(true);
+  isolate->mutator_thread()->ScheduleInterrupts(Thread::kVMInterrupt);
+  return ReserveSampleImpl(isolate, allocation_sample);
+}
+
+AllocationSampleBuffer::AllocationSampleBuffer(intptr_t capacity) {
   const intptr_t size =
       Utils::RoundUp(capacity * sizeof(Sample), VirtualMemory::PageSize());
   const bool kNotExecutable = false;
@@ -164,85 +340,19 @@
   if (memory_ == NULL) {
     OUT_OF_MEMORY();
   }
-
-  samples_ = reinterpret_cast<Sample*>(memory_->address());
-  capacity_ = capacity;
+  Init(reinterpret_cast<Sample*>(memory_->address()), capacity);
+  free_sample_list_ = nullptr;
   cursor_ = 0;
-
-  if (FLAG_trace_profiler) {
-    OS::PrintErr("Profiler holds %" Pd " samples\n", capacity);
-    OS::PrintErr("Profiler sample is %" Pd " bytes\n", sizeof(Sample));
-    OS::PrintErr("Profiler memory usage = %" Pd " bytes\n", size);
-  }
-  if (FLAG_sample_buffer_duration != 0) {
-    OS::PrintErr(
-        "** WARNING ** Custom sample buffer size provided via "
-        "--sample-buffer-duration\n");
-    OS::PrintErr(
-        "The sample buffer can hold at least %ds worth of "
-        "samples with stacks depths of up to %d, collected at "
-        "a sample rate of %" Pd "Hz.\n",
-        FLAG_sample_buffer_duration, FLAG_max_profile_depth,
-        SamplesPerSecond());
-    OS::PrintErr("The resulting sample buffer size is %" Pd " bytes.\n", size);
-  }
-}
-
-AllocationSampleBuffer::AllocationSampleBuffer(intptr_t capacity)
-    : SampleBuffer(capacity), mutex_(), free_sample_list_(NULL) {}
-
-SampleBuffer::~SampleBuffer() {
-  delete memory_;
-}
-
-AllocationSampleBuffer::~AllocationSampleBuffer() {
-}
-
-Sample* SampleBuffer::At(intptr_t idx) const {
-  ASSERT(idx >= 0);
-  ASSERT(idx < capacity_);
-  return &samples_[idx];
-}
-
-intptr_t SampleBuffer::ReserveSampleSlot() {
-  ASSERT(samples_ != NULL);
-  uintptr_t cursor = cursor_.fetch_add(1u);
-  // Map back into sample buffer range.
-  cursor = cursor % capacity_;
-  return cursor;
-}
-
-Sample* SampleBuffer::ReserveSample() {
-  return At(ReserveSampleSlot());
-}
-
-Sample* SampleBuffer::ReserveSampleAndLink(Sample* previous) {
-  ASSERT(previous != NULL);
-  intptr_t next_index = ReserveSampleSlot();
-  Sample* next = At(next_index);
-  next->Init(previous->port(), previous->timestamp(), previous->tid());
-  next->set_head_sample(false);
-  // Mark that previous continues at next.
-  previous->SetContinuationIndex(next_index);
-  return next;
 }
 
 void AllocationSampleBuffer::FreeAllocationSample(Sample* sample) {
   MutexLocker ml(&mutex_);
-  while (sample != NULL) {
-    intptr_t continuation_index = -1;
-    if (sample->is_continuation_sample()) {
-      continuation_index = sample->continuation_index();
-    }
+  while (sample != nullptr) {
+    Sample* next = sample->continuation_sample();
     sample->Clear();
     sample->set_next_free(free_sample_list_);
     free_sample_list_ = sample;
-
-    if (continuation_index != -1) {
-      sample = At(continuation_index);
-    } else {
-      sample = NULL;
-    }
+    sample = next;
   }
 }
 
@@ -255,7 +365,7 @@
     uint8_t* free_sample_ptr = reinterpret_cast<uint8_t*>(free_sample);
     return static_cast<intptr_t>((free_sample_ptr - samples_array_ptr) /
                                  sizeof(Sample));
-  } else if (cursor_ < static_cast<uintptr_t>(capacity_ - 1)) {
+  } else if (cursor_ < static_cast<intptr_t>(capacity_ - 1)) {
     return cursor_ += 1;
   } else {
     return -1;
@@ -277,7 +387,7 @@
       previous->native_allocation_size_bytes());
   next->set_head_sample(false);
   // Mark that previous continues at next.
-  previous->SetContinuationIndex(next_index);
+  previous->SetContinuation(next);
   return next;
 }
 
@@ -957,12 +1067,16 @@
 }
 
 static Sample* SetupSample(Thread* thread,
-                           SampleBuffer* sample_buffer,
+                           bool allocation_sample,
                            ThreadId tid) {
   ASSERT(thread != NULL);
   Isolate* isolate = thread->isolate();
-  ASSERT(sample_buffer != NULL);
-  Sample* sample = sample_buffer->ReserveSample();
+  SampleBlockBuffer* buffer = Profiler::sample_block_buffer();
+  Sample* sample = allocation_sample ? buffer->ReserveAllocationSample(isolate)
+                                     : buffer->ReserveCPUSample(isolate);
+  if (sample == nullptr) {
+    return nullptr;
+  }
   sample->Init(isolate->main_port(), OS::GetCurrentMonotonicMicros(), tid);
   uword vm_tag = thread->vm_tag();
 #if defined(USING_SIMULATOR)
@@ -980,7 +1094,8 @@
   return sample;
 }
 
-static Sample* SetupSampleNative(SampleBuffer* sample_buffer, ThreadId tid) {
+static Sample* SetupSampleNative(AllocationSampleBuffer* sample_buffer,
+                                 ThreadId tid) {
   Sample* sample = sample_buffer->ReserveSample();
   if (sample == NULL) {
     return NULL;
@@ -1129,11 +1244,10 @@
   if (!CheckIsolate(isolate)) {
     return;
   }
-
   const bool exited_dart_code = thread->HasExitedDartCode();
 
-  SampleBuffer* sample_buffer = Profiler::sample_buffer();
-  if (sample_buffer == NULL) {
+  SampleBlockBuffer* buffer = Profiler::sample_block_buffer();
+  if (buffer == nullptr) {
     // Profiler not initialized.
     return;
   }
@@ -1157,24 +1271,30 @@
     return;
   }
 
-  Sample* sample = SetupSample(thread, sample_buffer, os_thread->trace_id());
+  Sample* sample =
+      SetupSample(thread, /*allocation_block*/ true, os_thread->trace_id());
+  if (sample == nullptr) {
+    // We were unable to assign a sample for this allocation.
+    counters_.sample_allocation_failure++;
+    return;
+  }
   sample->SetAllocationCid(cid);
   sample->set_allocation_identity_hash(identity_hash);
 
   if (FLAG_profile_vm_allocation) {
     ProfilerNativeStackWalker native_stack_walker(
         &counters_, (isolate != NULL) ? isolate->main_port() : ILLEGAL_PORT,
-        sample, sample_buffer, stack_lower, stack_upper, pc, fp, sp);
+        sample, isolate->current_allocation_sample_block(), stack_lower,
+        stack_upper, pc, fp, sp);
     native_stack_walker.walk();
   } else if (exited_dart_code) {
     ProfilerDartStackWalker dart_exit_stack_walker(
-        thread, sample, sample_buffer, pc, fp, /* allocation_sample*/ true);
+        thread, sample, isolate->current_allocation_sample_block(), pc, fp,
+        /* allocation_sample*/ true);
     dart_exit_stack_walker.walk();
   } else {
     // Fall back.
     uintptr_t pc = OS::GetProgramCounter();
-    Sample* sample = SetupSample(thread, sample_buffer, os_thread->trace_id());
-    sample->SetAllocationCid(cid);
     sample->SetAt(0, pc);
   }
 }
@@ -1231,20 +1351,16 @@
   return sample;
 }
 
-void Profiler::SampleThreadSingleFrame(Thread* thread, uintptr_t pc) {
+void Profiler::SampleThreadSingleFrame(Thread* thread,
+                                       Sample* sample,
+                                       uintptr_t pc) {
   ASSERT(thread != NULL);
   OSThread* os_thread = thread->os_thread();
   ASSERT(os_thread != NULL);
   Isolate* isolate = thread->isolate();
 
-  SampleBuffer* sample_buffer = Profiler::sample_buffer();
-  if (sample_buffer == NULL) {
-    // Profiler not initialized.
-    return;
-  }
+  ASSERT(Profiler::sample_block_buffer() != nullptr);
 
-  // Setup sample.
-  Sample* sample = SetupSample(thread, sample_buffer, os_thread->trace_id());
   // Increment counter for vm tag.
   VMTagCounters* counters = isolate->vm_tag_counters();
   ASSERT(counters != NULL);
@@ -1306,22 +1422,37 @@
     return;
   }
 
+  SampleBlockBuffer* sample_block_buffer = Profiler::sample_block_buffer();
+  if (sample_block_buffer == nullptr) {
+    // Profiler not initialized.
+    return;
+  }
+
+  // Setup sample.
+  Sample* sample =
+      SetupSample(thread, /*allocation_block*/ false, os_thread->trace_id());
+  if (sample == nullptr) {
+    // We were unable to assign a sample for this profiler tick.
+    counters_.sample_allocation_failure++;
+    return;
+  }
+
   if (thread->IsMutatorThread()) {
     if (isolate->IsDeoptimizing()) {
       counters_.single_frame_sample_deoptimizing.fetch_add(1);
-      SampleThreadSingleFrame(thread, pc);
+      SampleThreadSingleFrame(thread, sample, pc);
       return;
     }
     if (isolate->group()->compaction_in_progress()) {
       // The Dart stack isn't fully walkable.
-      SampleThreadSingleFrame(thread, pc);
+      SampleThreadSingleFrame(thread, sample, pc);
       return;
     }
   }
 
   if (!InitialRegisterCheck(pc, fp, sp)) {
     counters_.single_frame_sample_register_check.fetch_add(1);
-    SampleThreadSingleFrame(thread, pc);
+    SampleThreadSingleFrame(thread, sample, pc);
     return;
   }
 
@@ -1331,20 +1462,13 @@
                                        &stack_upper)) {
     counters_.single_frame_sample_get_and_validate_stack_bounds.fetch_add(1);
     // Could not get stack boundary.
-    SampleThreadSingleFrame(thread, pc);
+    SampleThreadSingleFrame(thread, sample, pc);
     return;
   }
 
   // At this point we have a valid stack boundary for this isolate and
   // know that our initial stack and frame pointers are within the boundary.
-  SampleBuffer* sample_buffer = Profiler::sample_buffer();
-  if (sample_buffer == NULL) {
-    // Profiler not initialized.
-    return;
-  }
 
-  // Setup sample.
-  Sample* sample = SetupSample(thread, sample_buffer, os_thread->trace_id());
   // Increment counter for vm tag.
   VMTagCounters* counters = isolate->vm_tag_counters();
   ASSERT(counters != NULL);
@@ -1354,9 +1478,11 @@
 
   ProfilerNativeStackWalker native_stack_walker(
       &counters_, (isolate != NULL) ? isolate->main_port() : ILLEGAL_PORT,
-      sample, sample_buffer, stack_lower, stack_upper, pc, fp, sp);
+      sample, isolate->current_sample_block(), stack_lower, stack_upper, pc, fp,
+      sp);
   const bool exited_dart_code = thread->HasExitedDartCode();
-  ProfilerDartStackWalker dart_stack_walker(thread, sample, sample_buffer, pc,
+  ProfilerDartStackWalker dart_stack_walker(thread, sample,
+                                            isolate->current_sample_block(), pc,
                                             fp, /* allocation_sample*/ false);
 
   // All memory access is done inside CollectSample.
@@ -1480,12 +1606,14 @@
 }
 
 ProcessedSampleBuffer* SampleBuffer::BuildProcessedSampleBuffer(
-    SampleFilter* filter) {
-  ASSERT(filter != NULL);
+    SampleFilter* filter,
+    ProcessedSampleBuffer* buffer) {
   Thread* thread = Thread::Current();
   Zone* zone = thread->zone();
 
-  ProcessedSampleBuffer* buffer = new (zone) ProcessedSampleBuffer();
+  if (buffer == nullptr) {
+    buffer = new (zone) ProcessedSampleBuffer();
+  }
 
   const intptr_t length = capacity();
   for (intptr_t i = 0; i < length; i++) {
@@ -1498,12 +1626,6 @@
       // An inner sample in a chain of samples.
       continue;
     }
-    // If we're requesting all the native allocation samples, we don't care
-    // whether or not we're in the same isolate as the sample.
-    if (sample->port() != filter->port()) {
-      // Another isolate.
-      continue;
-    }
     if (sample->timestamp() == 0) {
       // Empty.
       continue;
@@ -1512,17 +1634,25 @@
       // No frames.
       continue;
     }
-    if (!filter->TimeFilterSample(sample)) {
-      // Did not pass time filter.
-      continue;
-    }
-    if (!filter->TaskFilterSample(sample)) {
-      // Did not pass task filter.
-      continue;
-    }
-    if (!filter->FilterSample(sample)) {
-      // Did not pass filter.
-      continue;
+    if (filter != nullptr) {
+      // If we're requesting all the native allocation samples, we don't care
+      // whether or not we're in the same isolate as the sample.
+      if (sample->port() != filter->port()) {
+        // Another isolate.
+        continue;
+      }
+      if (!filter->TimeFilterSample(sample)) {
+        // Did not pass time filter.
+        continue;
+      }
+      if (!filter->TaskFilterSample(sample)) {
+        // Did not pass task filter.
+        continue;
+      }
+      if (!filter->FilterSample(sample)) {
+        // Did not pass filter.
+        continue;
+      }
     }
     buffer->Add(BuildProcessedSample(sample, buffer->code_lookup_table()));
   }
@@ -1577,7 +1707,7 @@
 
 Sample* SampleBuffer::Next(Sample* sample) {
   if (!sample->is_continuation_sample()) return NULL;
-  Sample* next_sample = At(sample->continuation_index());
+  Sample* next_sample = sample->continuation_sample();
   // Sanity check.
   ASSERT(sample != next_sample);
   // Detect invalid chaining.
diff --git a/runtime/vm/profiler.h b/runtime/vm/profiler.h
index 2f9ea17..a4adbff4 100644
--- a/runtime/vm/profiler.h
+++ b/runtime/vm/profiler.h
@@ -29,7 +29,7 @@
 
 class Sample;
 class AllocationSampleBuffer;
-class SampleBuffer;
+class SampleBlock;
 class ProfileTrieNode;
 
 #define PROFILER_COUNTERS(V)                                                   \
@@ -46,7 +46,8 @@
   V(incomplete_sample_fp_bounds)                                               \
   V(incomplete_sample_fp_step)                                                 \
   V(incomplete_sample_bad_pc)                                                  \
-  V(failure_native_allocation_sample)
+  V(failure_native_allocation_sample)                                          \
+  V(sample_allocation_failure)
 
 struct ProfilerCounters {
 #define DECLARE_PROFILER_COUNTER(name) RelaxedAtomic<int64_t> name;
@@ -69,7 +70,9 @@
   // service protocol.
   static void UpdateRunningState();
 
-  static SampleBuffer* sample_buffer() { return sample_buffer_; }
+  static SampleBlockBuffer* sample_block_buffer() {
+    return sample_block_buffer_;
+  }
   static AllocationSampleBuffer* allocation_sample_buffer() {
     return allocation_sample_buffer_;
   }
@@ -111,10 +114,12 @@
   static intptr_t CalculateSampleBufferCapacity();
 
   // Does not walk the thread's stack.
-  static void SampleThreadSingleFrame(Thread* thread, uintptr_t pc);
+  static void SampleThreadSingleFrame(Thread* thread,
+                                      Sample* sample,
+                                      uintptr_t pc);
   static RelaxedAtomic<bool> initialized_;
 
-  static SampleBuffer* sample_buffer_;
+  static SampleBlockBuffer* sample_block_buffer_;
   static AllocationSampleBuffer* allocation_sample_buffer_;
 
   static ProfilerCounters counters_;
@@ -129,6 +134,8 @@
 
   virtual void VisitSample(Sample* sample) = 0;
 
+  virtual void Reset() { visited_ = 0; }
+
   intptr_t visited() const { return visited_; }
 
   void IncrementVisited() { visited_++; }
@@ -189,11 +196,14 @@
 // Each Sample holds a stack trace from an isolate.
 class Sample {
  public:
+  Sample() = default;
+
   void Init(Dart_Port port, int64_t timestamp, ThreadId tid) {
     Clear();
     timestamp_ = timestamp;
     tid_ = tid;
     port_ = port;
+    next_ = nullptr;
   }
 
   Dart_Port port() const { return port_; }
@@ -214,7 +224,7 @@
     vm_tag_ = VMTag::kInvalidTagId;
     user_tag_ = UserTags::kDefaultUserTag;
     state_ = 0;
-    continuation_index_ = -1;
+    next_ = nullptr;
     allocation_identity_hash_ = 0;
 #if defined(DART_USE_TCMALLOC) && defined(DEBUG)
     native_allocation_address_ = 0;
@@ -352,18 +362,14 @@
     return ContinuationSampleBit::decode(state_);
   }
 
-  void SetContinuationIndex(intptr_t index) {
+  void SetContinuation(Sample* next) {
     ASSERT(!is_continuation_sample());
-    ASSERT(continuation_index_ == -1);
+    ASSERT(next_ == nullptr);
     state_ = ContinuationSampleBit::update(true, state_);
-    continuation_index_ = index;
-    ASSERT(is_continuation_sample());
+    next_ = next;
   }
 
-  intptr_t continuation_index() const {
-    ASSERT(is_continuation_sample());
-    return continuation_index_;
-  }
+  Sample* continuation_sample() const { return next_; }
 
   intptr_t allocation_cid() const {
     ASSERT(is_allocation_sample());
@@ -431,7 +437,7 @@
   uword vm_tag_;
   uword user_tag_;
   uint32_t state_;
-  int32_t continuation_index_;
+  Sample* next_;
   uint32_t allocation_identity_hash_;
 
 #if defined(DART_USE_TCMALLOC) && defined(DEBUG)
@@ -623,21 +629,26 @@
   DISALLOW_COPY_AND_ASSIGN(CodeLookupTable);
 };
 
-// Ring buffer of Samples that is (usually) shared by many isolates.
-class SampleBuffer {
+// Interface for a class that can create a ProcessedSampleBuffer.
+class ProcessedSampleBufferBuilder {
  public:
-  // Up to 1 minute @ 1000Hz, less if samples are deep.
-  static const intptr_t kDefaultBufferCapacity = 60000;
+  virtual ~ProcessedSampleBufferBuilder() = default;
+  virtual ProcessedSampleBuffer* BuildProcessedSampleBuffer(
+      SampleFilter* filter,
+      ProcessedSampleBuffer* buffer = nullptr) = 0;
+};
 
-  explicit SampleBuffer(intptr_t capacity = kDefaultBufferCapacity);
-  virtual ~SampleBuffer();
+class SampleBuffer : public ProcessedSampleBufferBuilder {
+ public:
+  SampleBuffer() = default;
+  virtual ~SampleBuffer() = default;
 
-  intptr_t capacity() const { return capacity_; }
-
-  Sample* At(intptr_t idx) const;
-  intptr_t ReserveSampleSlot();
-  virtual Sample* ReserveSample();
-  virtual Sample* ReserveSampleAndLink(Sample* previous);
+  virtual void Init(Sample* samples, intptr_t capacity) {
+    ASSERT(samples != nullptr);
+    ASSERT(capacity > 0);
+    samples_ = samples;
+    capacity_ = capacity;
+  }
 
   void VisitSamples(SampleVisitor* visitor) {
     ASSERT(visitor != NULL);
@@ -669,45 +680,218 @@
     }
   }
 
-  ProcessedSampleBuffer* BuildProcessedSampleBuffer(SampleFilter* filter);
+  virtual Sample* ReserveSample() = 0;
+  virtual Sample* ReserveSampleAndLink(Sample* previous) = 0;
 
-  intptr_t Size() { return memory_->size(); }
+  Sample* At(intptr_t idx) const {
+    ASSERT(idx >= 0);
+    ASSERT(idx < capacity_);
+    return &samples_[idx];
+  }
+
+  intptr_t capacity() const { return capacity_; }
+
+  virtual ProcessedSampleBuffer* BuildProcessedSampleBuffer(
+      SampleFilter* filter,
+      ProcessedSampleBuffer* buffer = nullptr);
 
  protected:
-  ProcessedSample* BuildProcessedSample(Sample* sample,
-                                        const CodeLookupTable& clt);
   Sample* Next(Sample* sample);
 
-  VirtualMemory* memory_;
+  ProcessedSample* BuildProcessedSample(Sample* sample,
+                                        const CodeLookupTable& clt);
+
   Sample* samples_;
   intptr_t capacity_;
-  RelaxedAtomic<uintptr_t> cursor_;
+
+  DISALLOW_COPY_AND_ASSIGN(SampleBuffer);
+};
+
+class SampleBlock : public SampleBuffer {
+ public:
+  // The default number of samples per block. Overridden by some tests.
+  static const intptr_t kSamplesPerBlock = 1000;
+
+  SampleBlock() = default;
+  virtual ~SampleBlock() = default;
+
+  void Clear() {
+    allocation_block_ = false;
+    cursor_ = 0;
+    full_ = false;
+    evictable_ = false;
+    next_free_ = nullptr;
+  }
+
+  // Returns the number of samples contained within this block.
+  intptr_t capacity() const { return capacity_; }
+
+  // Specify whether or not this block is used for assigning allocation
+  // samples.
+  void set_is_allocation_block(bool is_allocation_block) {
+    allocation_block_ = is_allocation_block;
+  }
+
+  Isolate* owner() const { return owner_; }
+  void set_owner(Isolate* isolate) { owner_ = isolate; }
+
+  // Manually marks the block as full so it can be processed and added back to
+  // the pool of available blocks.
+  void release_block() { full_.store(true); }
+
+  // When true, this sample block is considered complete and will no longer be
+  // used to assign new Samples. This block is **not** available for
+  // re-allocation simply because it's full. It must be processed by
+  // SampleBlockBuffer::ProcessCompletedBlocks before it can be considered
+  // evictable and available for re-allocation.
+  bool is_full() const { return full_.load(); }
+
+  // When true, this sample block is available for re-allocation.
+  bool evictable() const { return evictable_.load(); }
+
+  virtual Sample* ReserveSample();
+  virtual Sample* ReserveSampleAndLink(Sample* previous);
+
+ protected:
+  Isolate* owner_ = nullptr;
+  bool allocation_block_ = false;
+
+  intptr_t index_;
+  RelaxedAtomic<int> cursor_ = 0;
+  RelaxedAtomic<bool> full_ = false;
+  RelaxedAtomic<bool> evictable_ = false;
+
+  SampleBlock* next_free_ = nullptr;
 
  private:
-  DISALLOW_COPY_AND_ASSIGN(SampleBuffer);
+  friend class SampleBlockBuffer;
+
+  DISALLOW_COPY_AND_ASSIGN(SampleBlock);
+};
+
+class SampleBlockBuffer : public ProcessedSampleBufferBuilder {
+ public:
+  static const intptr_t kDefaultBlockCount = 60;
+
+  // Creates a SampleBlockBuffer with a predetermined number of blocks.
+  //
+  // Defaults to kDefaultBlockCount blocks. Block size is fixed to
+  // SampleBlock::kSamplesPerBlock samples per block, except for in tests.
+  explicit SampleBlockBuffer(
+      intptr_t blocks = kDefaultBlockCount,
+      intptr_t samples_per_block = SampleBlock::kSamplesPerBlock);
+
+  virtual ~SampleBlockBuffer();
+
+  void VisitSamples(SampleVisitor* visitor) {
+    ASSERT(visitor != NULL);
+    for (intptr_t i = 0; i < cursor_.load(); ++i) {
+      (&blocks_[i])->VisitSamples(visitor);
+    }
+  }
+
+  // Returns true when there is at least a single block that needs to be
+  // processed.
+  //
+  // NOTE: this should only be called from the interrupt handler as
+  // invocation will have the side effect of clearing the underlying flag.
+  bool process_blocks() { return can_process_block_.exchange(false); }
+
+  // Iterates over the blocks in the buffer and processes blocks marked as
+  // full. Processing consists of sending a service event with the samples from
+  // completed, unprocessed blocks and marking these blocks are evictable
+  // (i.e., safe to be re-allocated and re-used).
+  void ProcessCompletedBlocks();
+
+  // Reserves a sample for a CPU profile.
+  //
+  // Returns nullptr when a sample can't be reserved.
+  Sample* ReserveCPUSample(Isolate* isolate);
+
+  // Reserves a sample for a Dart object allocation profile.
+  //
+  // Returns nullptr when a sample can't be reserved.
+  Sample* ReserveAllocationSample(Isolate* isolate);
+
+  intptr_t Size() const { return memory_->size(); }
+
+  virtual ProcessedSampleBuffer* BuildProcessedSampleBuffer(
+      SampleFilter* filter,
+      ProcessedSampleBuffer* buffer = nullptr);
+
+ private:
+  Sample* ReserveSampleImpl(Isolate* isolate, bool allocation_sample);
+
+  // Returns nullptr if there are no available blocks.
+  SampleBlock* ReserveSampleBlock();
+
+  void FreeBlock(SampleBlock* block) {
+    ASSERT(block->next_free_ == nullptr);
+    MutexLocker ml(&free_block_lock_);
+    if (free_list_head_ == nullptr) {
+      free_list_head_ = block;
+      free_list_tail_ = block;
+      return;
+    }
+    free_list_tail_->next_free_ = block;
+    free_list_tail_ = block;
+  }
+
+  SampleBlock* GetFreeBlock() {
+    MutexLocker ml(&free_block_lock_);
+    if (free_list_head_ == nullptr) {
+      return nullptr;
+    }
+    SampleBlock* block = free_list_head_;
+    free_list_head_ = block->next_free_;
+    if (free_list_head_ == nullptr) {
+      free_list_tail_ = nullptr;
+    }
+    block->next_free_ = nullptr;
+    return block;
+  }
+
+  Mutex free_block_lock_;
+  RelaxedAtomic<bool> can_process_block_ = false;
+
+  // Sample block management.
+  RelaxedAtomic<int> cursor_;
+  SampleBlock* blocks_;
+  intptr_t capacity_;
+  SampleBlock* free_list_head_;
+  SampleBlock* free_list_tail_;
+
+  // Sample buffer management.
+  VirtualMemory* memory_;
+  Sample* sample_buffer_;
+  DISALLOW_COPY_AND_ASSIGN(SampleBlockBuffer);
 };
 
 class AllocationSampleBuffer : public SampleBuffer {
  public:
-  explicit AllocationSampleBuffer(intptr_t capacity = kDefaultBufferCapacity);
-  virtual ~AllocationSampleBuffer();
+  explicit AllocationSampleBuffer(intptr_t capacity = 60000);
+  virtual ~AllocationSampleBuffer() = default;
 
-  intptr_t ReserveSampleSlotLocked();
   virtual Sample* ReserveSample();
   virtual Sample* ReserveSampleAndLink(Sample* previous);
   void FreeAllocationSample(Sample* sample);
 
+  intptr_t Size() { return memory_->size(); }
+
  private:
+  intptr_t ReserveSampleSlotLocked();
+
   Mutex mutex_;
   Sample* free_sample_list_;
-
+  VirtualMemory* memory_;
+  RelaxedAtomic<int> cursor_ = 0;
   DISALLOW_COPY_AND_ASSIGN(AllocationSampleBuffer);
 };
 
 intptr_t Profiler::Size() {
   intptr_t size = 0;
-  if (sample_buffer_ != nullptr) {
-    size += sample_buffer_->Size();
+  if (sample_block_buffer_ != nullptr) {
+    size += sample_block_buffer_->Size();
   }
   if (allocation_sample_buffer_ != nullptr) {
     size += allocation_sample_buffer_->Size();
diff --git a/runtime/vm/profiler_service.cc b/runtime/vm/profiler_service.cc
index a61fb94..e9f333e 100644
--- a/runtime/vm/profiler_service.cc
+++ b/runtime/vm/profiler_service.cc
@@ -7,6 +7,7 @@
 #include "platform/text_buffer.h"
 #include "vm/growable_array.h"
 #include "vm/hash_map.h"
+#include "vm/heap/safepoint.h"
 #include "vm/log.h"
 #include "vm/malloc_hooks.h"
 #include "vm/native_symbol.h"
@@ -15,6 +16,8 @@
 #include "vm/profiler.h"
 #include "vm/reusable_handles.h"
 #include "vm/scope_timer.h"
+#include "vm/service.h"
+#include "vm/service_event.h"
 #include "vm/timeline.h"
 
 namespace dart {
@@ -919,7 +922,7 @@
 
   ProfileBuilder(Thread* thread,
                  SampleFilter* filter,
-                 SampleBuffer* sample_buffer,
+                 ProcessedSampleBufferBuilder* sample_buffer,
                  Profile* profile)
       : thread_(thread),
         vm_isolate_(Dart::vm_isolate()),
@@ -932,8 +935,6 @@
         inlined_functions_cache_(new ProfileCodeInlinedFunctionsCache()),
         samples_(NULL),
         info_kind_(kNone) {
-    ASSERT((sample_buffer_ == Profiler::sample_buffer()) ||
-           (sample_buffer_ == Profiler::allocation_sample_buffer()));
     ASSERT(profile_ != NULL);
   }
 
@@ -975,7 +976,7 @@
 
   bool FilterSamples() {
     ScopeTimer sw("ProfileBuilder::FilterSamples", FLAG_trace_profiler);
-    ASSERT(sample_buffer_ != NULL);
+    ASSERT(sample_buffer_ != nullptr);
     samples_ = sample_buffer_->BuildProcessedSampleBuffer(filter_);
     profile_->samples_ = samples_;
     profile_->sample_count_ = samples_->length();
@@ -1439,7 +1440,7 @@
   Thread* thread_;
   Isolate* vm_isolate_;
   SampleFilter* filter_;
-  SampleBuffer* sample_buffer_;
+  ProcessedSampleBufferBuilder* sample_buffer_;
   Profile* profile_;
   const AbstractCode null_code_;
   const Function& null_function_;
@@ -1466,11 +1467,10 @@
 
 void Profile::Build(Thread* thread,
                     SampleFilter* filter,
-                    SampleBuffer* sample_buffer) {
+                    ProcessedSampleBufferBuilder* sample_buffer) {
   // Disable thread interrupts while processing the buffer.
   DisableThreadInterruptsScope dtis(thread);
   ThreadInterrupter::SampleBufferReaderScope scope;
-
   ProfileBuilder builder(thread, filter, sample_buffer, this);
   builder.Build();
 }
@@ -1722,12 +1722,16 @@
 }
 
 void Profile::PrintProfileJSON(JSONStream* stream, bool include_code_samples) {
-  ScopeTimer sw("Profile::PrintProfileJSON", FLAG_trace_profiler);
   JSONObject obj(stream);
-  obj.AddProperty("type", "CpuSamples");
-  PrintHeaderJSON(&obj);
+  PrintProfileJSON(&obj, include_code_samples);
+}
+
+void Profile::PrintProfileJSON(JSONObject* obj, bool include_code_samples) {
+  ScopeTimer sw("Profile::PrintProfileJSON", FLAG_trace_profiler);
+  obj->AddProperty("type", "CpuSamples");
+  PrintHeaderJSON(obj);
   if (include_code_samples) {
-    JSONArray codes(&obj, "_codes");
+    JSONArray codes(obj, "_codes");
     for (intptr_t i = 0; i < live_code_->length(); i++) {
       ProfileCode* code = live_code_->At(i);
       ASSERT(code != NULL);
@@ -1746,30 +1750,30 @@
   }
 
   {
-    JSONArray functions(&obj, "functions");
+    JSONArray functions(obj, "functions");
     for (intptr_t i = 0; i < functions_->length(); i++) {
       ProfileFunction* function = functions_->At(i);
       ASSERT(function != NULL);
       function->PrintToJSONArray(&functions);
     }
   }
-  PrintSamplesJSON(&obj, include_code_samples);
+  PrintSamplesJSON(obj, include_code_samples);
 }
 
 void ProfilerService::PrintJSONImpl(Thread* thread,
                                     JSONStream* stream,
                                     SampleFilter* filter,
-                                    SampleBuffer* sample_buffer,
+                                    ProcessedSampleBufferBuilder* buffer,
                                     bool include_code_samples) {
   Isolate* isolate = thread->isolate();
 
   // We should bail out in service.cc if the profiler is disabled.
-  ASSERT(sample_buffer != NULL);
+  ASSERT(buffer != nullptr);
 
   StackZone zone(thread);
   HANDLESCOPE(thread);
   Profile profile(isolate);
-  profile.Build(thread, filter, sample_buffer);
+  profile.Build(thread, filter, buffer);
   profile.PrintProfileJSON(stream, include_code_samples);
 }
 
@@ -1795,7 +1799,7 @@
   Isolate* isolate = thread->isolate();
   NoAllocationSampleFilter filter(isolate->main_port(), Thread::kMutatorTask,
                                   time_origin_micros, time_extent_micros);
-  PrintJSONImpl(thread, stream, &filter, Profiler::sample_buffer(),
+  PrintJSONImpl(thread, stream, &filter, Profiler::sample_block_buffer(),
                 include_code_samples);
 }
 
@@ -1820,7 +1824,7 @@
   Isolate* isolate = thread->isolate();
   AllocationSampleFilter filter(isolate->main_port(), Thread::kMutatorTask,
                                 time_origin_micros, time_extent_micros);
-  PrintJSONImpl(thread, stream, &filter, Profiler::sample_buffer(), true);
+  PrintJSONImpl(thread, stream, &filter, Profiler::sample_block_buffer(), true);
 }
 
 class ClassAllocationSampleFilter : public SampleFilter {
@@ -1856,7 +1860,7 @@
   ClassAllocationSampleFilter filter(isolate->main_port(), cls,
                                      Thread::kMutatorTask, time_origin_micros,
                                      time_extent_micros);
-  PrintJSONImpl(thread, stream, &filter, Profiler::sample_buffer(), true);
+  PrintJSONImpl(thread, stream, &filter, Profiler::sample_block_buffer(), true);
 }
 
 void ProfilerService::PrintNativeAllocationJSON(JSONStream* stream,
@@ -1870,8 +1874,8 @@
 }
 
 void ProfilerService::ClearSamples() {
-  SampleBuffer* sample_buffer = Profiler::sample_buffer();
-  if (sample_buffer == NULL) {
+  SampleBlockBuffer* sample_block_buffer = Profiler::sample_block_buffer();
+  if (sample_block_buffer == nullptr) {
     return;
   }
 
@@ -1883,7 +1887,7 @@
   ThreadInterrupter::SampleBufferReaderScope scope;
 
   ClearProfileVisitor clear_profile(isolate);
-  sample_buffer->VisitSamples(&clear_profile);
+  sample_block_buffer->VisitSamples(&clear_profile);
 }
 
 #endif  // !PRODUCT
diff --git a/runtime/vm/profiler_service.h b/runtime/vm/profiler_service.h
index 8009207..1c02db3 100644
--- a/runtime/vm/profiler_service.h
+++ b/runtime/vm/profiler_service.h
@@ -367,7 +367,9 @@
   explicit Profile(Isolate* isolate);
 
   // Build a filtered model using |filter|.
-  void Build(Thread* thread, SampleFilter* filter, SampleBuffer* sample_buffer);
+  void Build(Thread* thread,
+             SampleFilter* filter,
+             ProcessedSampleBufferBuilder* sample_block_buffer);
 
   // After building:
   int64_t min_time() const { return min_time_; }
@@ -383,6 +385,7 @@
   ProfileCode* GetCodeFromPC(uword pc, int64_t timestamp);
 
   void PrintProfileJSON(JSONStream* stream, bool include_code_samples);
+  void PrintProfileJSON(JSONObject* obj, bool include_code_samples);
 
   ProfileFunction* FindFunction(const Function& function);
 
@@ -445,7 +448,7 @@
   static void PrintJSONImpl(Thread* thread,
                             JSONStream* stream,
                             SampleFilter* filter,
-                            SampleBuffer* sample_buffer,
+                            ProcessedSampleBufferBuilder* buffer,
                             bool include_code_samples);
 };
 
diff --git a/runtime/vm/profiler_test.cc b/runtime/vm/profiler_test.cc
index 1829169..1171458 100644
--- a/runtime/vm/profiler_test.cc
+++ b/runtime/vm/profiler_test.cc
@@ -22,6 +22,12 @@
 DECLARE_FLAG(int, max_profile_depth);
 DECLARE_FLAG(int, optimization_counter_threshold);
 
+// SampleVisitor ignores samples with timestamp == 0.
+const int64_t kValidTimeStamp = 1;
+
+// SampleVisitor ignores samples with pc == 0.
+const uword kValidPc = 0xFF;
+
 // Some tests are written assuming native stack trace profiling is disabled.
 class DisableNativeProfileScope : public ValueObject {
  public:
@@ -71,87 +77,123 @@
   const intptr_t FLAG_max_profile_depth_;
 };
 
-class ProfileSampleBufferTestHelper {
+class ProfileSampleBufferTestHelper : public SampleVisitor {
  public:
-  static intptr_t IterateCount(const Dart_Port port,
-                               const SampleBuffer& sample_buffer) {
-    intptr_t c = 0;
-    for (intptr_t i = 0; i < sample_buffer.capacity(); i++) {
-      Sample* sample = sample_buffer.At(i);
-      if (sample->port() != port) {
-        continue;
-      }
-      c++;
-    }
-    return c;
+  explicit ProfileSampleBufferTestHelper(Dart_Port port)
+      : SampleVisitor(port) {}
+
+  void VisitSample(Sample* sample) { sum_ += sample->At(0); }
+
+  void Reset() {
+    sum_ = 0;
+    SampleVisitor::Reset();
   }
 
-  static intptr_t IterateSumPC(const Dart_Port port,
-                               const SampleBuffer& sample_buffer) {
-    intptr_t c = 0;
-    for (intptr_t i = 0; i < sample_buffer.capacity(); i++) {
-      Sample* sample = sample_buffer.At(i);
-      if (sample->port() != port) {
-        continue;
-      }
-      c += sample->At(0);
-    }
-    return c;
-  }
+  intptr_t sum() const { return sum_; }
+
+ private:
+  intptr_t sum_ = 0;
 };
 
+void VisitSamples(SampleBlockBuffer* buffer, SampleVisitor* visitor) {
+  // Mark the completed blocks as free so they can be re-used.
+  buffer->ProcessCompletedBlocks();
+  visitor->Reset();
+  buffer->VisitSamples(visitor);
+}
+
 TEST_CASE(Profiler_SampleBufferWrapTest) {
-  SampleBuffer* sample_buffer = new SampleBuffer(3);
+  Isolate* isolate = Isolate::Current();
+  SampleBlockBuffer* sample_buffer = new SampleBlockBuffer(3, 1);
   Dart_Port i = 123;
-  EXPECT_EQ(0, ProfileSampleBufferTestHelper::IterateSumPC(i, *sample_buffer));
+  ProfileSampleBufferTestHelper visitor(i);
+
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(0, visitor.sum());
   Sample* s;
-  s = sample_buffer->ReserveSample();
-  s->Init(i, 0, 0);
+
+  s = sample_buffer->ReserveCPUSample(isolate);
+  s->Init(i, kValidTimeStamp, 0);
   s->SetAt(0, 2);
-  EXPECT_EQ(2, ProfileSampleBufferTestHelper::IterateSumPC(i, *sample_buffer));
-  s = sample_buffer->ReserveSample();
-  s->Init(i, 0, 0);
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(2, visitor.sum());
+
+  s = sample_buffer->ReserveCPUSample(isolate);
+  s->Init(i, kValidTimeStamp, 0);
   s->SetAt(0, 4);
-  EXPECT_EQ(6, ProfileSampleBufferTestHelper::IterateSumPC(i, *sample_buffer));
-  s = sample_buffer->ReserveSample();
-  s->Init(i, 0, 0);
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(6, visitor.sum());
+
+  s = sample_buffer->ReserveCPUSample(isolate);
+  s->Init(i, kValidTimeStamp, 0);
   s->SetAt(0, 6);
-  EXPECT_EQ(12, ProfileSampleBufferTestHelper::IterateSumPC(i, *sample_buffer));
-  s = sample_buffer->ReserveSample();
-  s->Init(i, 0, 0);
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(12, visitor.sum());
+
+  // Mark the completed blocks as free so they can be re-used.
+  sample_buffer->ProcessCompletedBlocks();
+
+  s = sample_buffer->ReserveCPUSample(isolate);
+  s->Init(i, kValidTimeStamp, 0);
   s->SetAt(0, 8);
-  EXPECT_EQ(18, ProfileSampleBufferTestHelper::IterateSumPC(i, *sample_buffer));
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(18, visitor.sum());
+  {
+    MutexLocker ml(isolate->current_sample_block_lock());
+    isolate->set_current_sample_block(nullptr);
+  }
   delete sample_buffer;
 }
 
 TEST_CASE(Profiler_SampleBufferIterateTest) {
-  SampleBuffer* sample_buffer = new SampleBuffer(3);
+  Isolate* isolate = Isolate::Current();
+  SampleBlockBuffer* sample_buffer = new SampleBlockBuffer(3, 1);
   Dart_Port i = 123;
-  EXPECT_EQ(0, ProfileSampleBufferTestHelper::IterateCount(i, *sample_buffer));
+  ProfileSampleBufferTestHelper visitor(i);
+
+  sample_buffer->VisitSamples(&visitor);
+  EXPECT_EQ(0, visitor.visited());
   Sample* s;
-  s = sample_buffer->ReserveSample();
-  s->Init(i, 0, 0);
-  EXPECT_EQ(1, ProfileSampleBufferTestHelper::IterateCount(i, *sample_buffer));
-  s = sample_buffer->ReserveSample();
-  s->Init(i, 0, 0);
-  EXPECT_EQ(2, ProfileSampleBufferTestHelper::IterateCount(i, *sample_buffer));
-  s = sample_buffer->ReserveSample();
-  s->Init(i, 0, 0);
-  EXPECT_EQ(3, ProfileSampleBufferTestHelper::IterateCount(i, *sample_buffer));
-  s = sample_buffer->ReserveSample();
-  s->Init(i, 0, 0);
-  EXPECT_EQ(3, ProfileSampleBufferTestHelper::IterateCount(i, *sample_buffer));
+  s = sample_buffer->ReserveCPUSample(isolate);
+  s->Init(i, kValidTimeStamp, 0);
+  s->SetAt(0, kValidPc);
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(1, visitor.visited());
+
+  s = sample_buffer->ReserveCPUSample(isolate);
+  s->Init(i, kValidTimeStamp, 0);
+  s->SetAt(0, kValidPc);
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(2, visitor.visited());
+
+  s = sample_buffer->ReserveCPUSample(isolate);
+  s->Init(i, kValidTimeStamp, 0);
+  s->SetAt(0, kValidPc);
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(3, visitor.visited());
+
+  s = sample_buffer->ReserveCPUSample(isolate);
+  s->Init(i, kValidTimeStamp, 0);
+  s->SetAt(0, kValidPc);
+  VisitSamples(sample_buffer, &visitor);
+  EXPECT_EQ(3, visitor.visited());
+
+  {
+    MutexLocker ml(isolate->current_sample_block_lock());
+    isolate->set_current_sample_block(nullptr);
+  }
   delete sample_buffer;
 }
 
 TEST_CASE(Profiler_AllocationSampleTest) {
   Isolate* isolate = Isolate::Current();
-  SampleBuffer* sample_buffer = new SampleBuffer(3);
-  Sample* sample = sample_buffer->ReserveSample();
+  SampleBlockBuffer* sample_buffer = new SampleBlockBuffer(1, 1);
+  Sample* sample = sample_buffer->ReserveAllocationSample(isolate);
   sample->Init(isolate->main_port(), 0, 0);
   sample->set_metadata(99);
   sample->set_is_allocation_sample(true);
   EXPECT_EQ(99, sample->allocation_cid());
+  isolate->set_current_allocation_sample_block(nullptr);
   delete sample_buffer;
 }
 
@@ -443,7 +485,7 @@
     AllocationFilter filter(isolate->main_port(), class_a.id(),
                             before_allocations_micros,
                             allocation_extent_micros);
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have 1 allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -471,7 +513,7 @@
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id(),
                             Dart_TimelineGetMicros(), 16000);
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples because none occured within
     // the specified time range.
     EXPECT_EQ(0, profile.sample_count());
@@ -561,7 +603,7 @@
     // Filter for the class in the time range.
     NativeAllocationSampleFilter filter(before_allocations_micros,
                                         allocation_extent_micros);
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have 0 allocation samples since we freed the memory.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -574,7 +616,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     NativeAllocationSampleFilter filter(Dart_TimelineGetMicros(), 16000);
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples because none occured within
     // the specified time range.
     EXPECT_EQ(0, profile.sample_count());
@@ -620,7 +662,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -637,7 +679,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -667,7 +709,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
   }
@@ -705,7 +747,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -725,7 +767,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have three allocation samples.
     EXPECT_EQ(3, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -780,7 +822,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -800,7 +842,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have three allocation samples.
     EXPECT_EQ(3, profile.sample_count());
     ProfileStackWalker walker(&profile, true);
@@ -850,7 +892,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), double_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -863,7 +905,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), double_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -885,7 +927,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), double_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
   }
@@ -912,7 +954,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), array_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -925,7 +967,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), array_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -947,7 +989,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), array_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
   }
@@ -969,7 +1011,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), array_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples, since empty
     // growable lists use a shared backing.
     EXPECT_EQ(0, profile.sample_count());
@@ -999,7 +1041,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), context_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -1012,7 +1054,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), context_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -1032,7 +1074,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), context_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
   }
@@ -1073,7 +1115,7 @@
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), closure_class.id());
     filter.set_enable_vm_ticks(true);
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -1097,7 +1139,7 @@
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), closure_class.id());
     filter.set_enable_vm_ticks(true);
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
   }
@@ -1127,7 +1169,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), float32_list_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -1140,7 +1182,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), float32_list_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -1162,7 +1204,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), float32_list_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
   }
@@ -1175,7 +1217,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), float32_list_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should now have two allocation samples.
     EXPECT_EQ(2, profile.sample_count());
   }
@@ -1207,7 +1249,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), one_byte_string_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -1220,7 +1262,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), one_byte_string_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -1240,7 +1282,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), one_byte_string_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
   }
@@ -1253,7 +1295,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), one_byte_string_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should now have two allocation samples.
     EXPECT_EQ(2, profile.sample_count());
   }
@@ -1285,7 +1327,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), one_byte_string_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -1298,7 +1340,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), one_byte_string_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -1324,7 +1366,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), one_byte_string_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should still only have one allocation sample.
     EXPECT_EQ(1, profile.sample_count());
   }
@@ -1337,7 +1379,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), one_byte_string_class.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should now have two allocation samples.
     EXPECT_EQ(2, profile.sample_count());
   }
@@ -1393,7 +1435,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -1411,7 +1453,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have 50,000 allocation samples.
     EXPECT_EQ(50000, profile.sample_count());
     {
@@ -1540,7 +1582,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have no allocation samples.
     EXPECT_EQ(0, profile.sample_count());
   }
@@ -1557,7 +1599,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile, true);
 
@@ -1633,7 +1675,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have 1 allocation sample.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile);
@@ -1728,7 +1770,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation samples.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile, true);
@@ -1810,7 +1852,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation samples.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile, true);
@@ -1888,7 +1930,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation samples.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile, true);
@@ -1998,7 +2040,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation samples.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile, true);
@@ -2093,7 +2135,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation samples.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile, true);
@@ -2211,7 +2253,7 @@
     HANDLESCOPE(thread);
     Profile profile(isolate);
     AllocationFilter filter(isolate->main_port(), class_a.id());
-    profile.Build(thread, &filter, Profiler::sample_buffer());
+    profile.Build(thread, &filter, Profiler::sample_block_buffer());
     // We should have one allocation samples.
     EXPECT_EQ(1, profile.sample_count());
     ProfileStackWalker walker(&profile, true);
@@ -2256,10 +2298,10 @@
   }
 }
 
-static void InsertFakeSample(SampleBuffer* sample_buffer, uword* pc_offsets) {
-  ASSERT(sample_buffer != NULL);
+static void InsertFakeSample(uword* pc_offsets) {
   Isolate* isolate = Isolate::Current();
-  Sample* sample = sample_buffer->ReserveSample();
+  ASSERT(Profiler::sample_block_buffer() != nullptr);
+  Sample* sample = Profiler::sample_block_buffer()->ReserveCPUSample(isolate);
   ASSERT(sample != NULL);
   sample->Init(isolate->main_port(), OS::GetCurrentMonotonicMicros(),
                OSThread::Current()->trace_id());
@@ -2319,8 +2361,8 @@
 
   DisableBackgroundCompilationScope dbcs;
 
-  SampleBuffer* sample_buffer = Profiler::sample_buffer();
-  EXPECT(sample_buffer != NULL);
+  SampleBlockBuffer* sample_block_buffer = Profiler::sample_block_buffer();
+  ASSERT(sample_block_buffer != nullptr);
 
   const Library& root_library = Library::Handle(LoadTestScript(kScript));
 
@@ -2330,7 +2372,7 @@
   {
     // Clear the profile for this isolate.
     ClearProfileVisitor cpv(Isolate::Current());
-    sample_buffer->VisitSamples(&cpv);
+    sample_block_buffer->VisitSamples(&cpv);
   }
 
   // Query the code object for main and determine the PC at some token
@@ -2390,9 +2432,9 @@
                      callPositionPc,  // main.
                      0};
 
-  InsertFakeSample(sample_buffer, &sample1[0]);
-  InsertFakeSample(sample_buffer, &sample2[0]);
-  InsertFakeSample(sample_buffer, &sample3[0]);
+  InsertFakeSample(&sample1[0]);
+  InsertFakeSample(&sample2[0]);
+  InsertFakeSample(&sample3[0]);
 
   // Generate source report for main.
   JSONStream js;
diff --git a/runtime/vm/service.h b/runtime/vm/service.h
index e178c6a..7f5a223 100644
--- a/runtime/vm/service.h
+++ b/runtime/vm/service.h
@@ -15,7 +15,7 @@
 namespace dart {
 
 #define SERVICE_PROTOCOL_MAJOR_VERSION 3
-#define SERVICE_PROTOCOL_MINOR_VERSION 48
+#define SERVICE_PROTOCOL_MINOR_VERSION 49
 
 class Array;
 class EmbedderServiceHandler;
diff --git a/runtime/vm/service/service.md b/runtime/vm/service/service.md
index 708f581..fe8233a 100644
--- a/runtime/vm/service/service.md
+++ b/runtime/vm/service/service.md
@@ -1,8 +1,8 @@
-# Dart VM Service Protocol 3.48
+# Dart VM Service Protocol 3.49
 
 > Please post feedback to the [observatory-discuss group][discuss-list]
 
-This document describes of _version 3.48_ of the Dart VM Service Protocol. This
+This document describes of _version 3.49_ of the Dart VM Service Protocol. This
 protocol is used to communicate with a running Dart Virtual Machine.
 
 To use the Service Protocol, start the VM with the *--observe* flag.
@@ -1477,7 +1477,7 @@
 VM | VMUpdate, VMFlagUpdate
 Isolate | IsolateStart, IsolateRunnable, IsolateExit, IsolateUpdate, IsolateReload, ServiceExtensionAdded
 Debug | PauseStart, PauseExit, PauseBreakpoint, PauseInterrupted, PauseException, PausePostRequest, Resume, BreakpointAdded, BreakpointResolved, BreakpointRemoved, BreakpointUpdated, Inspect, None
-Profiler | UserTagChanged
+Profiler | CpuSamples, UserTagChanged
 GC | GC
 Extension | Extension
 Timeline | TimelineEvents, TimelineStreamsSubscriptionUpdate
@@ -2161,6 +2161,9 @@
 
   // The previous UserTag label.
   string previousTag [optional];
+
+  // A CPU profile containing recent samples.
+  CpuSamples cpuSamples [optional];
 }
 ```
 
@@ -2274,6 +2277,9 @@
 
   // Notification that the UserTag for an isolate has been changed.
   UserTagChanged,
+
+  // A block of recently collected CPU samples.
+  CpuSamples,
 }
 ```
 
@@ -4069,5 +4075,6 @@
 3.46 | Moved `sourceLocation` property into reference types for `Class`, `Field`, and `Function`.
 3.47 | Added `shows` and `hides` properties to `LibraryDependency`.
 3.48 | Added `Profiler` stream, `UserTagChanged` event kind, and `updatedTag` and `previousTag` properties to `Event`.
+3.49 | Added `CpuSamples` event kind, and `cpuSamples` property to `Event`.
 
 [discuss-list]: https://groups.google.com/a/dartlang.org/forum/#!forum/observatory-discuss
diff --git a/runtime/vm/service_event.cc b/runtime/vm/service_event.cc
index f8ab174..85740cf 100644
--- a/runtime/vm/service_event.cc
+++ b/runtime/vm/service_event.cc
@@ -64,7 +64,8 @@
            event_kind == ServiceEvent::kNone ||
            // VM service can print Observatory information to Stdout or Stderr
            // which are embedder streams.
-           event_kind == ServiceEvent::kEmbedder)));
+           event_kind == ServiceEvent::kEmbedder ||
+           event_kind == ServiceEvent::kCpuSamples)));
 
   if ((event_kind == ServiceEvent::kPauseStart) ||
       (event_kind == ServiceEvent::kPauseExit)) {
@@ -141,6 +142,8 @@
       return "TimelineStreamSubscriptionsUpdate";
     case kUserTagChanged:
       return "UserTagChanged";
+    case kCpuSamples:
+      return "CpuSamples";
     default:
       UNREACHABLE();
       return "Unknown";
@@ -193,6 +196,7 @@
     case kEmbedder:
       return nullptr;
 
+    case kCpuSamples:
     case kUserTagChanged:
       return &Service::profiler_stream;
 
@@ -300,6 +304,11 @@
     js->AppendSerializedObject("extensionData",
                                extension_event_.event_data->ToCString());
   }
+
+  if (kind() == kCpuSamples) {
+    JSONObject cpu_profile(&jsobj, "cpuSamples");
+    cpu_profile_->PrintProfileJSON(&cpu_profile, false);
+  }
 }
 
 void ServiceEvent::PrintJSONHeader(JSONObject* jsobj) const {
diff --git a/runtime/vm/service_event.h b/runtime/vm/service_event.h
index 3b8de0e..4c0efdd 100644
--- a/runtime/vm/service_event.h
+++ b/runtime/vm/service_event.h
@@ -7,6 +7,7 @@
 
 #include "vm/globals.h"
 #include "vm/heap/heap.h"
+#include "vm/profiler_service.h"
 
 namespace dart {
 
@@ -62,6 +63,8 @@
 
     kUserTagChanged,
 
+    kCpuSamples,
+
     kIllegal,
   };
 
@@ -213,6 +216,9 @@
     timeline_event_block_ = block;
   }
 
+  Profile* cpu_profile() const { return cpu_profile_; }
+  void set_cpu_profile(Profile* profile) { cpu_profile_ = profile; }
+
   void PrintJSON(JSONStream* js) const;
 
   void PrintJSONHeader(JSONObject* jsobj) const;
@@ -246,6 +252,7 @@
   intptr_t bytes_length_;
   LogRecord log_record_;
   ExtensionEvent extension_event_;
+  Profile* cpu_profile_;
   int64_t timestamp_;
 };
 
diff --git a/runtime/vm/source_report.cc b/runtime/vm/source_report.cc
index 03eaf12..f999c81 100644
--- a/runtime/vm/source_report.cc
+++ b/runtime/vm/source_report.cc
@@ -59,7 +59,7 @@
     // Build the profile.
     SampleFilter samplesForIsolate(thread_->isolate()->main_port(),
                                    Thread::kMutatorTask, -1, -1);
-    profile_.Build(thread, &samplesForIsolate, Profiler::sample_buffer());
+    profile_.Build(thread, &samplesForIsolate, Profiler::sample_block_buffer());
   }
 }
 
diff --git a/runtime/vm/thread.cc b/runtime/vm/thread.cc
index d03ff5a..09a3b25 100644
--- a/runtime/vm/thread.cc
+++ b/runtime/vm/thread.cc
@@ -448,6 +448,15 @@
       }
       heap()->CollectGarbage(Heap::kNew);
     }
+
+#if !defined(PRODUCT)
+    // Processes completed SampleBlocks and sends CPU sample events over the
+    // service protocol when applicable.
+    SampleBlockBuffer* sample_buffer = Profiler::sample_block_buffer();
+    if (sample_buffer != nullptr && sample_buffer->process_blocks()) {
+      sample_buffer->ProcessCompletedBlocks();
+    }
+#endif  // !defined(PRODUCT)
   }
   if ((interrupt_bits & kMessageInterrupt) != 0) {
     MessageHandler::MessageStatus status =
diff --git a/sdk/lib/_internal/js_dev_runtime/patch/js_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/js_patch.dart
index 033b303..feb5ef1 100644
--- a/sdk/lib/_internal/js_dev_runtime/patch/js_patch.dart
+++ b/sdk/lib/_internal/js_dev_runtime/patch/js_patch.dart
@@ -9,7 +9,6 @@
 
 import 'dart:_js_helper' show patch, NoReifyGeneric, Primitives;
 import 'dart:_foreign_helper' show JS;
-import 'dart:_interceptors' show JavaScriptObject;
 import 'dart:_runtime' as dart;
 
 @patch
@@ -368,7 +367,7 @@
     int ms = JS('!', '#.getTime()', o);
     return DateTime.fromMillisecondsSinceEpoch(ms);
   } else if (o is _DartObject &&
-      !identical(dart.getReifiedType(o), dart.typeRep<JavaScriptObject>())) {
+      !identical(dart.getReifiedType(o), dart.jsobject)) {
     return o._dartObj;
   } else {
     return _wrapToDart(o);
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
index 9e99b26..1f362a8 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/rtti.dart
@@ -92,13 +92,13 @@
         return JS('', '#.constructor', obj);
       }
       var result = JS('', '#[#]', obj, _extensionType);
-      if (result == null) return typeRep<JavaScriptObject>();
+      if (result == null) return JS('', '#', jsobject);
       return result;
     case "function":
       // All Dart functions and callable classes must set _runtimeType
       var result = JS('', '#[#]', obj, _runtimeType);
       if (result != null) return result;
-      return typeRep<JavaScriptObject>();
+      return JS('', '#', jsobject);
     case "undefined":
       return JS('', '#', Null);
     case "number":
@@ -109,7 +109,7 @@
       return JS('', '#', String);
     case "symbol":
     default:
-      return typeRep<JavaScriptObject>();
+      return JS('', '#', jsobject);
   }
 }
 
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/runtime.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/runtime.dart
index 2cd212a..e4afbf4 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/runtime.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/runtime.dart
@@ -10,8 +10,7 @@
 
 import 'dart:_debugger' show stackTraceMapper, trackCall;
 import 'dart:_foreign_helper' show JS, JSExportName, rest, spread;
-import 'dart:_interceptors'
-    show JSArray, jsNull, JSFunction, NativeError, JavaScriptObject;
+import 'dart:_interceptors' show JSArray, jsNull, JSFunction, NativeError;
 import 'dart:_internal' as internal show LateError, Symbol;
 import 'dart:_js_helper'
     show
diff --git a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/types.dart b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/types.dart
index e48e530..7874940 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/types.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/types.dart
@@ -154,8 +154,7 @@
 }
 
 @notNull
-bool _isJsObject(obj) =>
-    JS('!', '# === #', getReifiedType(obj), typeRep<JavaScriptObject>());
+bool _isJsObject(obj) => JS('!', '# === #', getReifiedType(obj), jsobject);
 
 /// Asserts that [f] is a native JS functions and returns it if so.
 ///
@@ -238,7 +237,7 @@
     return raw;
   }
 
-  Object rawJSTypeForCheck() => _getRawJSType() ?? typeRep<JavaScriptObject>();
+  Object rawJSTypeForCheck() => _getRawJSType() ?? jsobject;
 
   @notNull
   @JSExportName('is')
@@ -479,6 +478,12 @@
 
 final bottom = unwrapType(Null);
 
+class JSObjectType extends DartType {
+  toString() => 'NativeJavaScriptObject';
+}
+
+final jsobject = JSObjectType();
+
 /// Dev Compiler's implementation of Type, wrapping its internal [_type].
 class _Type extends Type {
   /// The internal type representation, either a [DartType] or class constructor
@@ -1513,43 +1518,8 @@
       return ${_equalType(t2, Function)};
     }
 
-    // Even though lazy and anonymous JS types are natural subtypes of
-    // JavaScriptObject, JS types should be treated as mutual subtypes of each
-    // other. This allows users to be able to interface with both extension
-    // types on JavaScriptObject and package:js using the same object.
-    //
-    // Therefore, the following relationships hold true:
-    //
-    // JavaScriptObject <: package:js types
-    // package:js types <: JavaScriptObject
-    //
-    // TODO: This doesn't allow package:js type <: another package:js type yet.
-
-    if (${_isInterfaceSubtype(t1, JavaScriptObject, strictMode)}
-        && (${_jsInstanceOf(t2, LazyJSType)}
-            || ${_jsInstanceOf(t2, AnonymousJSType)}
-            // TODO: Since package:js types are instances of LazyJSType and
-            // AnonymousJSType, we don't have a mechanism to determine if *some*
-            // package:js type implements t2. This will possibly require keeping
-            // a map of these relationships for this subtyping check. For now,
-            // don't execute the following checks.
-            // || _isInterfaceSubtype(LazyJSType, t2, strictMode)
-            // || _isInterfaceSubtype(AnonymousJSType, t2, strictMode)
-            )) {
-      return true;
-    }
-
-    if (${_isInterfaceSubtype(JavaScriptObject, t2, strictMode)}
-        && (${_jsInstanceOf(t1, LazyJSType)}
-            || ${_jsInstanceOf(t1, AnonymousJSType)}
-            // TODO: We don't have a check in `isInterfaceSubtype` to check that
-            // a class is a subtype of *some* package:js class. This will likely
-            // require modifying `_isInterfaceSubtype` to check if the
-            // interfaces of t1 include a package:js type. For now, don't
-            // execute the following checks.
-            // || _isInterfaceSubtype(t1, LazyJSType, strictMode)
-            // || _isInterfaceSubtype(t1, AnonymousJSType, strictMode)
-            )) {
+    // All JS types are subtypes of anonymous JS types.
+    if ($t1 === $jsobject && ${_jsInstanceOf(t2, AnonymousJSType)}) {
       return true;
     }
 
diff --git a/sdk/lib/_internal/js_dev_runtime/private/interceptors.dart b/sdk/lib/_internal/js_dev_runtime/private/interceptors.dart
index fdc771d..71c559c 100644
--- a/sdk/lib/_internal/js_dev_runtime/private/interceptors.dart
+++ b/sdk/lib/_internal/js_dev_runtime/private/interceptors.dart
@@ -74,40 +74,45 @@
   operator []=(int index, E value);
 }
 
-/// The interface implemented by JavaScript objects.
-///
-/// These are methods in addition to the regular Dart Object methods like
-/// [Object.hashCode]. This is the type that should be exported by a JavaScript
-/// interop library.
+/**
+ * The interface implemented by JavaScript objects.  These are methods in
+ * addition to the regular Dart Object methods like [Object.hashCode].
+ *
+ * This is the type that should be exported by a JavaScript interop library.
+ */
 abstract class JSObject {}
 
-/// Interceptor base class for JavaScript objects not recognized as some more
-/// specific native type.
-
-/// Unlike dart2js, ddc does not intercept JS objects, so this is only used as
-/// an on-type for JS interop extension types. All JS interop objects should be
-/// castable to this type.
+/**
+ * Interceptor base class for JavaScript objects not recognized as some more
+ * specific native type.
+ */
 abstract class JavaScriptObject extends Interceptor implements JSObject {
   const JavaScriptObject();
+
+  // It would be impolite to stash a property on the object.
+  int get hashCode => 0;
+
+  Type get runtimeType => JSObject;
 }
 
-/// Interceptor for plain JavaScript objects created as JavaScript object
-/// literals or `new Object()`.
-///
-/// Note that this isn't being used today in ddc. Instead of using interceptors,
-/// we have other type logic to distinguish JS types.
+/**
+ * Interceptor for plain JavaScript objects created as JavaScript object
+ * literals or `new Object()`.
+ */
 class PlainJavaScriptObject extends JavaScriptObject {
   const PlainJavaScriptObject();
 }
 
-/// Interceptor for unclassified JavaScript objects, typically objects with a
-/// non-trivial prototype chain.
-///
-/// This class also serves as a fallback for unknown JavaScript exceptions.
-/// Note that this isn't being used today in ddc. Instead of using interceptors,
-/// we have other type logic to distinguish JS types.
+/**
+ * Interceptor for unclassified JavaScript objects, typically objects with a
+ * non-trivial prototype chain.
+ *
+ * This class also serves as a fallback for unknown JavaScript exceptions.
+ */
 class UnknownJavaScriptObject extends JavaScriptObject {
   const UnknownJavaScriptObject();
+
+  String toString() => JS<String>('!', 'String(#)', this);
 }
 
 class NativeError extends Interceptor {
diff --git a/sdk/lib/_internal/js_runtime/lib/constant_map.dart b/sdk/lib/_internal/js_runtime/lib/constant_map.dart
index d5a5174..c7a2d84 100644
--- a/sdk/lib/_internal/js_runtime/lib/constant_map.dart
+++ b/sdk/lib/_internal/js_runtime/lib/constant_map.dart
@@ -12,37 +12,24 @@
 abstract class ConstantMap<K, V> implements Map<K, V> {
   // Used to create unmodifiable maps from other maps.
   factory ConstantMap.from(Map other) {
-    var keys = new List<K>.from(other.keys);
+    final keys = List<K>.from(other.keys);
     bool allStrings = true;
     for (var k in keys) {
-      if (k is! String) {
+      if (k is! String || '__proto__' == k) {
         allStrings = false;
         break;
       }
     }
     if (allStrings) {
-      bool containsProto = false;
-      var protoValue = null;
       var object = JS('=Object', '{}');
-      int length = 0;
-      for (var k in keys) {
+      for (final k in keys) {
         V v = other[k];
-        if (k != '__proto__') {
-          if (!jsHasOwnProperty(object, k as String)) length++;
-          JS('void', '#[#] = #', object, k, v);
-        } else {
-          containsProto = true;
-          protoValue = v;
-        }
+        JS('void', '#[#] = #', object, k, v);
       }
-      if (containsProto) {
-        length++;
-        return new ConstantProtoMap<K, V>._(length, object, keys, protoValue);
-      }
-      return new ConstantStringMap<K, V>._(length, object, keys);
+      return ConstantStringMap<K, V>._(keys.length, object, keys);
     }
     // TODO(lrn): Make a proper unmodifiable map implementation.
-    return new ConstantMapView<K, V>(new Map.from(other));
+    return ConstantMapView<K, V>(Map.from(other));
   }
 
   const ConstantMap._();
@@ -55,7 +42,7 @@
   String toString() => MapBase.mapToString(this);
 
   static Never _throwUnmodifiable() {
-    throw new UnsupportedError('Cannot modify unmodifiable Map');
+    throw UnsupportedError('Cannot modify unmodifiable Map');
   }
 
   void operator []=(K key, V val) {
@@ -70,14 +57,19 @@
     _throwUnmodifiable();
   }
 
-  void clear() => _throwUnmodifiable();
-  void addAll(Map<K, V> other) => _throwUnmodifiable();
+  void clear() {
+    _throwUnmodifiable();
+  }
+
+  void addAll(Map<K, V> other) {
+    _throwUnmodifiable();
+  }
 
   Iterable<MapEntry<K, V>> get entries sync* {
     // `this[key]` has static type `V?` but is always `V`. Rather than `as V`,
     // we use `as dynamic` so the upcast requires no checking and the implicit
     // downcast to `V` will be discarded in production.
-    for (var key in keys) yield new MapEntry<K, V>(key, this[key] as dynamic);
+    for (var key in keys) yield MapEntry<K, V>(key, this[key] as dynamic);
   }
 
   void addEntries(Iterable<MapEntry<K, V>> entries) {
@@ -151,32 +143,14 @@
   }
 
   Iterable<K> get keys {
-    return new _ConstantMapKeyIterable<K>(this);
+    return _ConstantMapKeyIterable<K>(this);
   }
 
   Iterable<V> get values {
-    return new MappedIterable<K, V>(_keysArray, (key) => _fetch(key));
+    return MappedIterable<K, V>(_keysArray, (key) => _fetch(key));
   }
 }
 
-class ConstantProtoMap<K, V> extends ConstantStringMap<K, V> {
-  // This constructor is not used.  The instantiation is shortcut by the
-  // compiler. It is here to make the uninitialized final fields legal.
-  ConstantProtoMap._(length, jsObject, keys, this._protoValue)
-      : super._(length, jsObject, keys);
-
-  final V _protoValue;
-
-  bool containsKey(Object? key) {
-    if (key is! String) return false;
-    if ('__proto__' == key) return true;
-    return jsHasOwnProperty(_jsObject, key);
-  }
-
-  _fetch(key) =>
-      '__proto__' == key ? _protoValue : jsPropertyAccess(_jsObject, key);
-}
-
 class _ConstantMapKeyIterable<K> extends Iterable<K> {
   ConstantStringMap<K, dynamic> _map;
   _ConstantMapKeyIterable(this._map);
@@ -199,7 +173,7 @@
   Map<K, V> _getMap() {
     LinkedHashMap<K, V>? backingMap = JS('LinkedHashMap|Null', r'#.$map', this);
     if (backingMap == null) {
-      backingMap = new JsLinkedHashMap<K, V>();
+      backingMap = JsLinkedHashMap<K, V>();
       fillLiteralMap(_jsData, backingMap);
       JS('', r'#.$map = #', this, backingMap);
     }
diff --git a/tools/VERSION b/tools/VERSION
index db6c054..4e0fc8a 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 14
 PATCH 0
-PRERELEASE 328
+PRERELEASE 329
 PRERELEASE_PATCH 0
\ No newline at end of file