[vm] Ensure transitive object copy allows low safepoint latencies

To make responsiveness to safepointing requests better when a thread
does a transitive object copy we add safepoint checkins.

In the fast object copy implementation - which runs inside
NoSafepointScope - we bail out to the slow path if there's a safepoint
requested.

The slow path is safe to GC at any point due to having all objects
inside handles / GC-visible objects.

TEST=Existing test suite.

Change-Id: Ie2c37e2f618506ab62a592aa9ff9ab266a02b64b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/245166
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
diff --git a/runtime/tests/vm/dart/isolates/fast_object_copy_test.dart b/runtime/tests/vm/dart/isolates/fast_object_copy_test.dart
index 7dc9690..22bf3ac 100644
--- a/runtime/tests/vm/dart/isolates/fast_object_copy_test.dart
+++ b/runtime/tests/vm/dart/isolates/fast_object_copy_test.dart
@@ -49,12 +49,20 @@
   })(),
 ];
 
+Uint8List initializeUint8List(Uint8List l) {
+  for (int i = 0; i < l.length; ++i) {
+    l[i] = i % 256;
+  }
+  return l;
+}
+
 final Uint8List largeExternalTypedData =
-    File(Platform.resolvedExecutable).readAsBytesSync()..[0] = 42;
-final Uint8List largeInternalTypedData = Uint8List(20 * 1024 * 1024)..[0] = 42;
+    initializeUint8List(File(Platform.resolvedExecutable).readAsBytesSync());
+final Uint8List largeInternalTypedData =
+    initializeUint8List(Uint8List(20 * 1024 * 1024));
 
 final Uint8List smallExternalTypedData =
-    File(Platform.script.toFilePath()).readAsBytesSync()..[0] = 21;
+    initializeUint8List(File(Platform.script.toFilePath()).readAsBytesSync());
 final Uint8List smallExternalTypedDataView =
     Uint8List.view(smallExternalTypedData.buffer, 1, 1);
 
@@ -319,7 +327,7 @@
     for (int i = 0; i < 10; ++i) {
       final result = await sendReceive(graph);
       final etd = result[1];
-      Expect.equals(42, etd[0]);
+      expectGraphsMatch(largeExternalTypedData, etd);
     }
   }
 
@@ -331,8 +339,8 @@
     ];
     for (int i = 0; i < 10; ++i) {
       final result = await sendReceive(graph);
-      final etd = result[1];
-      Expect.equals(42, etd[0]);
+      final etd = result[0];
+      expectGraphsMatch(largeExternalTypedData, etd);
     }
   }
 
@@ -359,7 +367,7 @@
     print('testExternalTypedData5');
     for (int i = 0; i < 10; ++i) {
       final etd = await sendReceive(largeExternalTypedData);
-      Expect.equals(42, etd[0]);
+      expectGraphsMatch(largeExternalTypedData, etd);
     }
   }
 
@@ -369,8 +377,8 @@
       smallExternalTypedData,
       largeExternalTypedData,
     ]);
-    Expect.equals(21, etd[0][0]);
-    Expect.equals(42, etd[1][0]);
+    expectGraphsMatch(smallExternalTypedData, etd[0]);
+    expectGraphsMatch(largeExternalTypedData, etd[1]);
   }
 
   Future testInternalTypedDataView() async {
diff --git a/runtime/tests/vm/dart_2/isolates/fast_object_copy_test.dart b/runtime/tests/vm/dart_2/isolates/fast_object_copy_test.dart
index fc62573..1e3773d 100644
--- a/runtime/tests/vm/dart_2/isolates/fast_object_copy_test.dart
+++ b/runtime/tests/vm/dart_2/isolates/fast_object_copy_test.dart
@@ -51,12 +51,20 @@
   })(),
 ];
 
+Uint8List initializeUint8List(Uint8List l) {
+  for (int i = 0; i < l.length; ++i) {
+    l[i] = i % 256;
+  }
+  return l;
+}
+
 final Uint8List largeExternalTypedData =
-    File(Platform.resolvedExecutable).readAsBytesSync()..[0] = 42;
-final Uint8List largeInternalTypedData = Uint8List(20 * 1024 * 1024)..[0] = 42;
+    initializeUint8List(File(Platform.resolvedExecutable).readAsBytesSync());
+final Uint8List largeInternalTypedData =
+    initializeUint8List(Uint8List(20 * 1024 * 1024));
 
 final Uint8List smallExternalTypedData =
-    File(Platform.script.toFilePath()).readAsBytesSync()..[0] = 21;
+    initializeUint8List(File(Platform.script.toFilePath()).readAsBytesSync());
 final Uint8List smallExternalTypedDataView =
     Uint8List.view(smallExternalTypedData.buffer, 1, 1);
 
@@ -321,7 +329,7 @@
     for (int i = 0; i < 10; ++i) {
       final result = await sendReceive(graph);
       final etd = result[1];
-      Expect.equals(42, etd[0]);
+      expectGraphsMatch(largeExternalTypedData, etd);
     }
   }
 
@@ -333,8 +341,8 @@
     ];
     for (int i = 0; i < 10; ++i) {
       final result = await sendReceive(graph);
-      final etd = result[1];
-      Expect.equals(42, etd[0]);
+      final etd = result[0];
+      expectGraphsMatch(largeExternalTypedData, etd);
     }
   }
 
@@ -361,7 +369,7 @@
     print('testExternalTypedData5');
     for (int i = 0; i < 10; ++i) {
       final etd = await sendReceive(largeExternalTypedData);
-      Expect.equals(42, etd[0]);
+      expectGraphsMatch(largeExternalTypedData, etd);
     }
   }
 
@@ -371,8 +379,8 @@
       smallExternalTypedData,
       largeExternalTypedData,
     ]);
-    Expect.equals(21, etd[0][0]);
-    Expect.equals(42, etd[1][0]);
+    expectGraphsMatch(smallExternalTypedData, etd[0]);
+    expectGraphsMatch(largeExternalTypedData, etd[1]);
   }
 
   Future testInternalTypedDataView() async {
diff --git a/runtime/vm/object_graph_copy.cc b/runtime/vm/object_graph_copy.cc
index c49af9a..86d3c8e 100644
--- a/runtime/vm/object_graph_copy.cc
+++ b/runtime/vm/object_graph_copy.cc
@@ -293,6 +293,46 @@
   raw_to->data_ = buffer;
 }
 
+template <typename T>
+void CopyTypedDataBaseWithSafepointChecks(Thread* thread,
+                                          const T& from,
+                                          const T& to,
+                                          intptr_t length) {
+  constexpr intptr_t kChunkSize = 100 * 1024;
+
+  const intptr_t chunks = length / kChunkSize;
+  const intptr_t remainder = length % kChunkSize;
+
+  // Notice we re-load the data pointer, since T may be TypedData in which case
+  // the interior pointer may change after checking into safepoints.
+  for (intptr_t i = 0; i < chunks; ++i) {
+    memmove(to.ptr().untag()->data_ + i * kChunkSize,
+            from.ptr().untag()->data_ + i * kChunkSize, kChunkSize);
+
+    thread->CheckForSafepoint();
+  }
+  if (remainder > 0) {
+    memmove(to.ptr().untag()->data_ + chunks * kChunkSize,
+            from.ptr().untag()->data_ + chunks * kChunkSize, remainder);
+  }
+}
+
+void InitializeExternalTypedDataWithSafepointChecks(
+    Thread* thread,
+    intptr_t cid,
+    const ExternalTypedData& from,
+    const ExternalTypedData& to) {
+  const intptr_t length_in_elements = from.Length();
+  const intptr_t length_in_bytes =
+      TypedData::ElementSizeInBytes(cid) * length_in_elements;
+
+  uint8_t* to_data = static_cast<uint8_t*>(malloc(length_in_bytes));
+  to.ptr().untag()->data_ = to_data;
+  to.ptr().untag()->length_ = Smi::New(length_in_elements);
+
+  CopyTypedDataBaseWithSafepointChecks(thread, from, to, length_in_bytes);
+}
+
 void InitializeTypedDataView(TypedDataViewPtr obj) {
   obj.untag()->typed_data_ = TypedDataBase::null();
   obj.untag()->offset_in_bytes_ = 0;
@@ -462,8 +502,10 @@
   void AddWeakReference(const WeakReference& from) {
     weak_references_.Add(&WeakReference::Handle(from.ptr()));
   }
-  void AddExternalTypedData(ExternalTypedDataPtr to) {
-    external_typed_data_.Add(&ExternalTypedData::Handle(to));
+  const ExternalTypedData& AddExternalTypedData(ExternalTypedDataPtr to) {
+    auto to_handle = &ExternalTypedData::Handle(to);
+    external_typed_data_.Add(to_handle);
+    return *to_handle;
   }
   void AddObjectToRehash(const Object& to) {
     objects_to_rehash_.Add(&Object::Handle(to.ptr()));
@@ -848,6 +890,7 @@
     if (Array::UseCardMarkingForAllocation(array_length)) {
       for (; offset < end_offset; offset += kCompressedWordSize) {
         ForwardCompressedLargeArrayPointer(src, dst, offset);
+        thread_->CheckForSafepoint();
       }
     } else {
       for (; offset < end_offset; offset += kCompressedWordSize) {
@@ -949,9 +992,11 @@
       to.untag()->SetCardRememberedBitUnsynchronized();
     }
     if (IsExternalTypedDataClassId(cid)) {
-      InitializeExternalTypedData(cid, ExternalTypedData::RawCast(from.ptr()),
-                                  ExternalTypedData::RawCast(to));
-      slow_forward_map_.AddExternalTypedData(ExternalTypedData::RawCast(to));
+      const auto& external_to = slow_forward_map_.AddExternalTypedData(
+          ExternalTypedData::RawCast(to));
+      InitializeExternalTypedDataWithSafepointChecks(
+          thread_, cid, ExternalTypedData::Cast(from), external_to);
+      return external_to.ptr();
     } else if (IsTypedDataViewClassId(cid)) {
       // We set the views backing store to `null` to satisfy an assertion in
       // GCCompactor::VisitTypedDataViewPointers().
@@ -1281,10 +1326,9 @@
 #endif
   }
 
-  void CopyTypedData(typename Types::TypedData from,
-                     typename Types::TypedData to) {
-    auto raw_from = UntagTypedData(from);
-    auto raw_to = UntagTypedData(to);
+  void CopyTypedData(TypedDataPtr from, TypedDataPtr to) {
+    auto raw_from = from.untag();
+    auto raw_to = to.untag();
     const intptr_t cid = Types::GetTypedDataPtr(from)->GetClassId();
     raw_to->length_ = raw_from->length_;
     raw_to->RecomputeDataField();
@@ -1293,6 +1337,17 @@
     memmove(raw_to->data_, raw_from->data_, length);
   }
 
+  void CopyTypedData(const TypedData& from, const TypedData& to) {
+    auto raw_from = from.ptr().untag();
+    auto raw_to = to.ptr().untag();
+    const intptr_t cid = Types::GetTypedDataPtr(from)->GetClassId();
+    raw_to->length_ = raw_from->length_;
+    raw_to->RecomputeDataField();
+    const intptr_t length =
+        TypedData::ElementSizeInBytes(cid) * Smi::Value(raw_from->length_);
+    CopyTypedDataBaseWithSafepointChecks(Base::thread_, from, to, length);
+  }
+
   void CopyTypedDataView(typename Types::TypedDataView from,
                          typename Types::TypedDataView to) {
     // This will forward & initialize the typed data.
@@ -1457,6 +1512,13 @@
           return root_copy;
         }
         fast_forward_map_.fill_cursor_ += 2;
+
+        // To maintain responsiveness we regularly check whether safepoints are
+        // requested - if so, we bail to slow path which will then checkin.
+        if (thread_->IsSafepointRequested()) {
+          exception_msg_ = kFastAllocationFailed;
+          return root_copy;
+        }
       }
 
       // Possibly forward values of [WeakProperty]s if keys became reachable.
@@ -1626,6 +1688,9 @@
         if (exception_msg_ != nullptr) {
           return Marker();
         }
+        // To maintain responsiveness we regularly check whether safepoints are
+        // requested.
+        thread_->CheckForSafepoint();
       }
 
       // Possibly forward values of [WeakProperty]s if keys became reachable.
diff --git a/runtime/vm/raw_object.h b/runtime/vm/raw_object.h
index f490865..d590114 100644
--- a/runtime/vm/raw_object.h
+++ b/runtime/vm/raw_object.h
@@ -2896,6 +2896,13 @@
   uint8_t* data_;
 
  private:
+  template <typename T>
+  friend void CopyTypedDataBaseWithSafepointChecks(
+      Thread*,
+      const T&,
+      const T&,
+      intptr_t);  // Access _data for memmove with safepoint checkins.
+
   RAW_HEAP_OBJECT_IMPLEMENTATION(PointerBase);
 };
 
@@ -2918,6 +2925,11 @@
       intptr_t,
       ExternalTypedDataPtr,
       ExternalTypedDataPtr);  // initialize fields.
+  friend void InitializeExternalTypedDataWithSafepointChecks(
+      Thread*,
+      intptr_t,
+      const ExternalTypedData&,
+      const ExternalTypedData&);  // initialize fields.
 
   RAW_HEAP_OBJECT_IMPLEMENTATION(TypedDataBase);
 };