Version 2.19.0-73.0.dev

Merge commit '4539bf6584b3e4a5699e1890990502d8ac9db939' into 'dev'
diff --git a/runtime/lib/object.cc b/runtime/lib/object.cc
index 3f50303..247f5d5 100644
--- a/runtime/lib/object.cc
+++ b/runtime/lib/object.cc
@@ -11,6 +11,7 @@
 #include "vm/heap/heap.h"
 #include "vm/native_entry.h"
 #include "vm/object.h"
+#include "vm/object_graph.h"
 #include "vm/object_store.h"
 #include "vm/resolver.h"
 #include "vm/stack_frame.h"
@@ -313,6 +314,22 @@
   return Object::null();
 }
 
+DEFINE_NATIVE_ENTRY(Internal_writeHeapSnapshotToFile, 0, 1) {
+#if !defined(PRODUCT)
+  const String& filename =
+      String::CheckedHandle(zone, arguments->NativeArgAt(0));
+  {
+    FileHeapSnapshotWriter file_writer(thread, filename.ToCString());
+    HeapSnapshotWriter writer(thread, &file_writer);
+    writer.Write();
+  }
+#else
+  Exceptions::ThrowUnsupportedError(
+      "Heap snapshots are only supported in non-product mode.");
+#endif  // !defined(PRODUCT)
+  return Object::null();
+}
+
 DEFINE_NATIVE_ENTRY(Internal_deoptimizeFunctionsOnStack, 0, 0) {
   DeoptimizeFunctionsOnStack();
   return Object::null();
diff --git a/runtime/tests/vm/dart/heap_snapshot_test.dart b/runtime/tests/vm/dart/heap_snapshot_test.dart
new file mode 100644
index 0000000..76eee909
--- /dev/null
+++ b/runtime/tests/vm/dart/heap_snapshot_test.dart
@@ -0,0 +1,108 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+import 'dart:_internal';
+
+import 'package:expect/expect.dart';
+import 'package:path/path.dart' as path;
+import 'package:vm_service/vm_service.dart';
+
+import 'use_flag_test_helper.dart';
+
+final bool alwaysTrue = int.parse('1') == 1;
+
+@pragma('vm:entry-point') // Prevent name mangling
+class Foo {}
+
+var global = null;
+
+main() async {
+  if (const bool.fromEnvironment('dart.vm.product')) {
+    var exception;
+    try {
+      await runTest();
+    } catch (e) {
+      exception = e;
+    }
+    Expect.contains(
+        'Heap snapshots are only supported in non-product mode.', '$exception');
+    return;
+  }
+
+  await runTest();
+}
+
+Future runTest() async {
+  await withTempDir('heap_snapshot_test', (String dir) async {
+    final state1 = path.join(dir, 'state1.heapsnapshot');
+    final state2 = path.join(dir, 'state2.heapsnapshot');
+    final state3 = path.join(dir, 'state3.heapsnapshot');
+
+    var local;
+
+    VMInternalsForTesting.writeHeapSnapshotToFile(state1);
+    if (alwaysTrue) {
+      global = Foo();
+      local = Foo();
+    }
+    VMInternalsForTesting.writeHeapSnapshotToFile(state2);
+    if (alwaysTrue) {
+      global = null;
+      local = null;
+    }
+    VMInternalsForTesting.writeHeapSnapshotToFile(state3);
+
+    final int count1 = countFooInstances(
+        findReachableObjects(loadHeapSnapshotFromFile(state1)));
+    final int count2 = countFooInstances(
+        findReachableObjects(loadHeapSnapshotFromFile(state2)));
+    final int count3 = countFooInstances(
+        findReachableObjects(loadHeapSnapshotFromFile(state3)));
+
+    Expect.equals(0, count1);
+    Expect.equals(2, count2);
+    Expect.equals(0, count3);
+
+    reachabilityFence(local);
+    reachabilityFence(global);
+  });
+}
+
+HeapSnapshotGraph loadHeapSnapshotFromFile(String filename) {
+  final bytes = File(filename).readAsBytesSync();
+  return HeapSnapshotGraph.fromChunks([bytes.buffer.asByteData()]);
+}
+
+Set<HeapSnapshotObject> findReachableObjects(HeapSnapshotGraph graph) {
+  const int rootObjectIdx = 1;
+
+  final reachableObjects = Set<HeapSnapshotObject>();
+  final worklist = <HeapSnapshotObject>[];
+
+  final rootObject = graph.objects[rootObjectIdx];
+
+  reachableObjects.add(rootObject);
+  worklist.add(rootObject);
+
+  while (worklist.isNotEmpty) {
+    final objectToExpand = worklist.removeLast();
+
+    for (final successor in objectToExpand.successors) {
+      if (!reachableObjects.contains(successor)) {
+        reachableObjects.add(successor);
+        worklist.add(successor);
+      }
+    }
+  }
+  return reachableObjects;
+}
+
+int countFooInstances(Set<HeapSnapshotObject> reachableObjects) {
+  int count = 0;
+  for (final object in reachableObjects) {
+    if (object.klass.name == 'Foo') count++;
+  }
+  return count;
+}
diff --git a/runtime/tests/vm/dart_2/heap_snapshot_test.dart b/runtime/tests/vm/dart_2/heap_snapshot_test.dart
new file mode 100644
index 0000000..a646c1a
--- /dev/null
+++ b/runtime/tests/vm/dart_2/heap_snapshot_test.dart
@@ -0,0 +1,110 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart=2.9
+
+import 'dart:io';
+import 'dart:_internal';
+
+import 'package:expect/expect.dart';
+import 'package:path/path.dart' as path;
+import 'package:vm_service/vm_service.dart';
+
+import 'use_flag_test_helper.dart';
+
+final bool alwaysTrue = int.parse('1') == 1;
+
+@pragma('vm:entry-point') // Prevent name mangling
+class Foo {}
+
+var global = null;
+
+main() async {
+  if (const bool.fromEnvironment('dart.vm.product')) {
+    var exception;
+    try {
+      await runTest();
+    } catch (e) {
+      exception = e;
+    }
+    Expect.contains(
+        'Heap snapshots are only supported in non-product mode.', '$exception');
+    return;
+  }
+
+  await runTest();
+}
+
+Future runTest() async {
+  await withTempDir('heap_snapshot_test', (String dir) async {
+    final state1 = path.join(dir, 'state1.heapsnapshot');
+    final state2 = path.join(dir, 'state2.heapsnapshot');
+    final state3 = path.join(dir, 'state3.heapsnapshot');
+
+    var local;
+
+    VMInternalsForTesting.writeHeapSnapshotToFile(state1);
+    if (alwaysTrue) {
+      global = Foo();
+      local = Foo();
+    }
+    VMInternalsForTesting.writeHeapSnapshotToFile(state2);
+    if (alwaysTrue) {
+      global = null;
+      local = null;
+    }
+    VMInternalsForTesting.writeHeapSnapshotToFile(state3);
+
+    final int count1 = countFooInstances(
+        findReachableObjects(loadHeapSnapshotFromFile(state1)));
+    final int count2 = countFooInstances(
+        findReachableObjects(loadHeapSnapshotFromFile(state2)));
+    final int count3 = countFooInstances(
+        findReachableObjects(loadHeapSnapshotFromFile(state3)));
+
+    Expect.equals(0, count1);
+    Expect.equals(2, count2);
+    Expect.equals(0, count3);
+
+    reachabilityFence(local);
+    reachabilityFence(global);
+  });
+}
+
+HeapSnapshotGraph loadHeapSnapshotFromFile(String filename) {
+  final bytes = File(filename).readAsBytesSync();
+  return HeapSnapshotGraph.fromChunks([bytes.buffer.asByteData()]);
+}
+
+Set<HeapSnapshotObject> findReachableObjects(HeapSnapshotGraph graph) {
+  const int rootObjectIdx = 1;
+
+  final reachableObjects = Set<HeapSnapshotObject>();
+  final worklist = <HeapSnapshotObject>[];
+
+  final rootObject = graph.objects[rootObjectIdx];
+
+  reachableObjects.add(rootObject);
+  worklist.add(rootObject);
+
+  while (worklist.isNotEmpty) {
+    final objectToExpand = worklist.removeLast();
+
+    for (final successor in objectToExpand.successors) {
+      if (!reachableObjects.contains(successor)) {
+        reachableObjects.add(successor);
+        worklist.add(successor);
+      }
+    }
+  }
+  return reachableObjects;
+}
+
+int countFooInstances(Set<HeapSnapshotObject> reachableObjects) {
+  int count = 0;
+  for (final object in reachableObjects) {
+    if (object.klass.name == 'Foo') count++;
+  }
+  return count;
+}
diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h
index f5d3e55..d98992e 100644
--- a/runtime/vm/bootstrap_natives.h
+++ b/runtime/vm/bootstrap_natives.h
@@ -319,6 +319,7 @@
   V(Internal_unsafeCast, 1)                                                    \
   V(Internal_nativeEffect, 1)                                                  \
   V(Internal_collectAllGarbage, 0)                                             \
+  V(Internal_writeHeapSnapshotToFile, 1)                                       \
   V(Internal_makeListFixedLength, 1)                                           \
   V(Internal_makeFixedListUnmodifiable, 1)                                     \
   V(Internal_extractTypeArguments, 2)                                          \
diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc
index 3b988bd..e087b67 100644
--- a/runtime/vm/isolate.cc
+++ b/runtime/vm/isolate.cc
@@ -2825,6 +2825,13 @@
   }
 }
 
+void Isolate::VisitStackPointers(ObjectPointerVisitor* visitor,
+                                 ValidationPolicy validate_frames) {
+  if (mutator_thread_ != nullptr) {
+    mutator_thread_->VisitObjectPointers(visitor, validate_frames);
+  }
+}
+
 void IsolateGroup::ReleaseStoreBuffers() {
   thread_registry()->ReleaseStoreBuffers();
 }
@@ -3016,9 +3023,7 @@
   for (Isolate* isolate : isolates_) {
     // Visit mutator thread, even if the isolate isn't entered/scheduled
     // (there might be live API handles to visit).
-    if (isolate->mutator_thread_ != nullptr) {
-      isolate->mutator_thread_->VisitObjectPointers(visitor, validate_frames);
-    }
+    isolate->VisitStackPointers(visitor, validate_frames);
   }
 
   visitor->clear_gc_root_type();
diff --git a/runtime/vm/object_graph.cc b/runtime/vm/object_graph.cc
index 8a1cd87..6265e3e 100644
--- a/runtime/vm/object_graph.cc
+++ b/runtime/vm/object_graph.cc
@@ -656,11 +656,12 @@
   ASSERT(buffer_ == nullptr);
 
   intptr_t chunk_size = kPreferredChunkSize;
-  if (chunk_size < needed + kMetadataReservation) {
-    chunk_size = needed + kMetadataReservation;
+  const intptr_t reserved_prefix = writer_->ReserveChunkPrefixSize();
+  if (chunk_size < (reserved_prefix + needed)) {
+    chunk_size = reserved_prefix + needed;
   }
   buffer_ = reinterpret_cast<uint8_t*>(malloc(chunk_size));
-  size_ = kMetadataReservation;
+  size_ = reserved_prefix;
   capacity_ = chunk_size;
 }
 
@@ -669,28 +670,8 @@
     return;
   }
 
-  JSONStream js;
-  {
-    JSONObject jsobj(&js);
-    jsobj.AddProperty("jsonrpc", "2.0");
-    jsobj.AddProperty("method", "streamNotify");
-    {
-      JSONObject params(&jsobj, "params");
-      params.AddProperty("streamId", Service::heapsnapshot_stream.id());
-      {
-        JSONObject event(&params, "event");
-        event.AddProperty("type", "Event");
-        event.AddProperty("kind", "HeapSnapshot");
-        event.AddProperty("isolate", thread()->isolate());
-        event.AddPropertyTimeMillis("timestamp", OS::GetCurrentTimeMillis());
-        event.AddProperty("last", last);
-      }
-    }
-  }
+  writer_->WriteChunk(buffer_, size_, last);
 
-  Service::SendEventWithData(Service::heapsnapshot_stream.id(), "HeapSnapshot",
-                             kMetadataReservation, js.buffer()->buffer(),
-                             js.buffer()->length(), buffer_, size_);
   buffer_ = nullptr;
   size_ = 0;
   capacity_ = 0;
@@ -1200,6 +1181,60 @@
   DISALLOW_COPY_AND_ASSIGN(CollectStaticFieldNames);
 };
 
+void VmServiceHeapSnapshotChunkedWriter::WriteChunk(uint8_t* buffer,
+                                                    intptr_t size,
+                                                    bool last) {
+  JSONStream js;
+  {
+    JSONObject jsobj(&js);
+    jsobj.AddProperty("jsonrpc", "2.0");
+    jsobj.AddProperty("method", "streamNotify");
+    {
+      JSONObject params(&jsobj, "params");
+      params.AddProperty("streamId", Service::heapsnapshot_stream.id());
+      {
+        JSONObject event(&params, "event");
+        event.AddProperty("type", "Event");
+        event.AddProperty("kind", "HeapSnapshot");
+        event.AddProperty("isolate", thread()->isolate());
+        event.AddPropertyTimeMillis("timestamp", OS::GetCurrentTimeMillis());
+        event.AddProperty("last", last);
+      }
+    }
+  }
+
+  Service::SendEventWithData(Service::heapsnapshot_stream.id(), "HeapSnapshot",
+                             kMetadataReservation, js.buffer()->buffer(),
+                             js.buffer()->length(), buffer, size);
+}
+
+FileHeapSnapshotWriter::FileHeapSnapshotWriter(Thread* thread,
+                                               const char* filename)
+    : ChunkedWriter(thread) {
+  auto open = Dart::file_open_callback();
+  if (open != nullptr) {
+    file_ = open(filename, /*write=*/true);
+  }
+}
+FileHeapSnapshotWriter::~FileHeapSnapshotWriter() {
+  auto close = Dart::file_close_callback();
+  if (close != nullptr) {
+    close(file_);
+  }
+}
+
+void FileHeapSnapshotWriter::WriteChunk(uint8_t* buffer,
+                                        intptr_t size,
+                                        bool last) {
+  if (file_ != nullptr) {
+    auto write = Dart::file_write_callback();
+    if (write != nullptr) {
+      write(buffer, size, file_);
+    }
+  }
+  free(buffer);
+}
+
 void HeapSnapshotWriter::Write() {
   HeapIterationScope iteration(thread());
 
@@ -1393,6 +1428,8 @@
             ++object_count_;
             isolate->VisitObjectPointers(&visitor,
                                          ValidationPolicy::kDontValidateFrames);
+            isolate->VisitStackPointers(&visitor,
+                                        ValidationPolicy::kDontValidateFrames);
             ++num_isolates;
           },
           /*at_safepoint=*/true);
@@ -1449,9 +1486,13 @@
           visitor.DoCount();
           isolate->VisitObjectPointers(&visitor,
                                        ValidationPolicy::kDontValidateFrames);
+          isolate->VisitStackPointers(&visitor,
+                                      ValidationPolicy::kDontValidateFrames);
           visitor.DoWrite();
           isolate->VisitObjectPointers(&visitor,
                                        ValidationPolicy::kDontValidateFrames);
+          isolate->VisitStackPointers(&visitor,
+                                      ValidationPolicy::kDontValidateFrames);
         },
         /*at_safepoint=*/true);
 
diff --git a/runtime/vm/object_graph.h b/runtime/vm/object_graph.h
index 55dc815..f9e3ef7 100644
--- a/runtime/vm/object_graph.h
+++ b/runtime/vm/object_graph.h
@@ -117,11 +117,45 @@
   DISALLOW_IMPLICIT_CONSTRUCTORS(ObjectGraph);
 };
 
+class ChunkedWriter : public ThreadStackResource {
+ public:
+  explicit ChunkedWriter(Thread* thread) : ThreadStackResource(thread) {}
+
+  virtual intptr_t ReserveChunkPrefixSize() { return 0; }
+
+  // Takes ownership of [buffer], must be freed with [malloc].
+  virtual void WriteChunk(uint8_t* buffer, intptr_t size, bool last) = 0;
+};
+
+class FileHeapSnapshotWriter : public ChunkedWriter {
+ public:
+  FileHeapSnapshotWriter(Thread* thread, const char* filename);
+  ~FileHeapSnapshotWriter();
+
+  virtual void WriteChunk(uint8_t* buffer, intptr_t size, bool last);
+
+ private:
+  void* file_ = nullptr;
+};
+
+class VmServiceHeapSnapshotChunkedWriter : public ChunkedWriter {
+ public:
+  explicit VmServiceHeapSnapshotChunkedWriter(Thread* thread)
+      : ChunkedWriter(thread) {}
+
+  virtual intptr_t ReserveChunkPrefixSize() { return kMetadataReservation; }
+  virtual void WriteChunk(uint8_t* buffer, intptr_t size, bool last);
+
+ private:
+  static const intptr_t kMetadataReservation = 512;
+};
+
 // Generates a dump of the heap, whose format is described in
 // runtime/vm/service/heap_snapshot.md.
 class HeapSnapshotWriter : public ThreadStackResource {
  public:
-  explicit HeapSnapshotWriter(Thread* thread) : ThreadStackResource(thread) {}
+  HeapSnapshotWriter(Thread* thread, ChunkedWriter* writer)
+      : ThreadStackResource(thread), writer_(writer) {}
 
   void WriteSigned(int64_t value) {
     EnsureAvailable((sizeof(value) * kBitsPerByte) / 7 + 1);
@@ -191,7 +225,6 @@
  private:
   static uint32_t GetHashHelper(Thread* thread, ObjectPtr obj);
 
-  static const intptr_t kMetadataReservation = 512;
   static const intptr_t kPreferredChunkSize = MB;
 
   void SetupCountingPages();
@@ -201,6 +234,8 @@
   void EnsureAvailable(intptr_t needed);
   void Flush(bool last = false);
 
+  ChunkedWriter* writer_ = nullptr;
+
   uint8_t* buffer_ = nullptr;
   intptr_t size_ = 0;
   intptr_t capacity_ = 0;
diff --git a/runtime/vm/service.cc b/runtime/vm/service.cc
index a455360..8bf7906 100644
--- a/runtime/vm/service.cc
+++ b/runtime/vm/service.cc
@@ -4545,7 +4545,8 @@
 
 static void RequestHeapSnapshot(Thread* thread, JSONStream* js) {
   if (Service::heapsnapshot_stream.enabled()) {
-    HeapSnapshotWriter writer(thread);
+    VmServiceHeapSnapshotChunkedWriter vmservice_writer(thread);
+    HeapSnapshotWriter writer(thread, &vmservice_writer);
     writer.Write();
   }
   // TODO(koda): Provide some id that ties this request to async response(s).
diff --git a/sdk/lib/_internal/vm/lib/internal_patch.dart b/sdk/lib/_internal/vm/lib/internal_patch.dart
index 30d0176..2bf8642 100644
--- a/sdk/lib/_internal/vm/lib/internal_patch.dart
+++ b/sdk/lib/_internal/vm/lib/internal_patch.dart
@@ -177,6 +177,9 @@
   @pragma("vm:external-name", "Internal_collectAllGarbage")
   external static void collectAllGarbage();
 
+  @pragma("vm:external-name", "Internal_writeHeapSnapshotToFile")
+  external static void writeHeapSnapshotToFile(String filename);
+
   @pragma("vm:external-name", "Internal_deoptimizeFunctionsOnStack")
   external static void deoptimizeFunctionsOnStack();
 
diff --git a/tools/VERSION b/tools/VERSION
index 844760f..16e4f46 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 19
 PATCH 0
-PRERELEASE 72
+PRERELEASE 73
 PRERELEASE_PATCH 0
\ No newline at end of file