[vm/ffi] Remove hacks and instability from FFI GC tests.

This is based on https://dart-review.googlesource.com/c/sdk/+/100583/2,
but instead of relying on private symbols, we expose an API function which
is a no-op outside specific tests.

Fixes https://github.com/dart-lang/sdk/issues/36723

Change-Id: Ieb78caa2d14956a2b5f985533b9b73e941d2d9c9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/103801
Commit-Queue: Samir Jindel <sjindel@google.com>
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Reviewed-by: Daco Harkes <dacoharkes@google.com>
diff --git a/runtime/bin/ffi_test_functions.cc b/runtime/bin/ffi_test_functions.cc
index 200d6c2..52b4512 100644
--- a/runtime/bin/ffi_test_functions.cc
+++ b/runtime/bin/ffi_test_functions.cc
@@ -22,6 +22,7 @@
 #include <limits>
 
 #include "include/dart_api.h"
+#include "include/dart_native_api.h"
 
 namespace dart {
 
@@ -458,42 +459,39 @@
 // Functions for stress-testing.
 
 DART_EXPORT int64_t MinInt64() {
+  Dart_ExecuteInternalCommand("gc-on-next-allocation");
   return 0x8000000000000000;
 }
 
 DART_EXPORT int64_t MinInt32() {
+  Dart_ExecuteInternalCommand("gc-on-next-allocation");
   return 0x80000000;
 }
 
 DART_EXPORT double SmallDouble() {
+  Dart_ExecuteInternalCommand("gc-on-next-allocation");
   return 0x80000000 * -1.0;
 }
 
 // Requires boxing on 32-bit and 64-bit systems, even if the top 32-bits are
 // truncated.
 DART_EXPORT void* LargePointer() {
+  Dart_ExecuteInternalCommand("gc-on-next-allocation");
   uint64_t origin = 0x8100000082000000;
   return reinterpret_cast<void*>(origin);
 }
 
-// Allocates 'count'-many Mint boxes, to stress-test GC during an FFI call.
-DART_EXPORT void AllocateMints(uint64_t count) {
-  Dart_EnterScope();
-  for (uint64_t i = 0; i < count; ++i) {
-    Dart_NewInteger(0x8000000000000001);
-  }
-  Dart_ExitScope();
+DART_EXPORT void TriggerGC(uint64_t count) {
+  Dart_ExecuteInternalCommand("gc-now");
 }
 
 // Calls a Dart function to allocate 'count' objects.
 // Used for stress-testing GC when re-entering the API.
-DART_EXPORT void AllocateThroughDart(uint64_t count) {
+DART_EXPORT void AllocateThroughDart() {
   Dart_EnterScope();
   Dart_Handle root = Dart_RootLibrary();
-  Dart_Handle arguments[1] = {Dart_NewIntegerFromUint64(count)};
   Dart_Handle result = Dart_Invoke(
-      root, Dart_NewStringFromCString("testAllocationsInDartHelper"), 1,
-      arguments);
+      root, Dart_NewStringFromCString("testAllocationsInDartHelper"), 0, NULL);
   const char* error;
   if (Dart_IsError(result)) {
     Dart_StringToCString(Dart_ToString(result), &error);
@@ -506,21 +504,6 @@
   Dart_ExitScope();
 }
 
-#if !defined(_WIN32)
-DART_EXPORT int RedirectStderr() {
-  char filename[256];
-  snprintf(filename, sizeof(filename), "/tmp/captured_stderr_%d", getpid());
-  FILE* f = freopen(filename, "w", stderr);
-  if (f == nullptr) {
-    fprintf(stderr, "Could not open temp file.\n");
-    Dart_PrepareToAbort();
-    abort();
-  }
-  printf("Got file %s\n", filename);
-  return getpid();
-}
-#endif
-
 ////////////////////////////////////////////////////////////////////////////////
 // Tests for callbacks.
 
diff --git a/runtime/include/dart_native_api.h b/runtime/include/dart_native_api.h
index 3b8f4d2..e45c010 100644
--- a/runtime/include/dart_native_api.h
+++ b/runtime/include/dart_native_api.h
@@ -175,4 +175,10 @@
  */
 DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_FinalizeAllClasses();
 
+/*  This function is intentionally undocumented.
+ *
+ *  It should not be used outside internal tests.
+ */
+DART_EXPORT void Dart_ExecuteInternalCommand(const char* command);
+
 #endif /* INCLUDE_DART_NATIVE_API_H_ */ /* NOLINT */
diff --git a/runtime/vm/heap/heap.cc b/runtime/vm/heap/heap.cc
index 2a53317..c8a1bdf 100644
--- a/runtime/vm/heap/heap.cc
+++ b/runtime/vm/heap/heap.cc
@@ -43,7 +43,8 @@
       barrier_done_(),
       read_only_(false),
       gc_new_space_in_progress_(false),
-      gc_old_space_in_progress_(false) {
+      gc_old_space_in_progress_(false),
+      gc_on_next_allocation_(false) {
   UpdateGlobalMaxUsed();
   for (int sel = 0; sel < kNumWeakSelectors; sel++) {
     new_weak_tables_[sel] = new WeakTable();
@@ -80,6 +81,7 @@
 
 uword Heap::AllocateNew(intptr_t size) {
   ASSERT(Thread::Current()->no_safepoint_scope_depth() == 0);
+  CollectForDebugging();
   Thread* thread = Thread::Current();
   uword addr = new_space_.TryAllocateInTLAB(thread, size);
   if (addr != 0) {
@@ -125,6 +127,7 @@
 
 uword Heap::AllocateOld(intptr_t size, HeapPage::PageType type) {
   ASSERT(Thread::Current()->no_safepoint_scope_depth() == 0);
+  CollectForDebugging();
   uword addr = old_space_.TryAllocate(size, type);
   if (addr != 0) {
     return addr;
@@ -652,6 +655,17 @@
   old_space_.AddRegionsToObjectSet(set);
 }
 
+void Heap::CollectOnNextAllocation() {
+  AbandonRemainingTLAB(Thread::Current());
+  gc_on_next_allocation_ = true;
+}
+
+void Heap::CollectForDebugging() {
+  if (!gc_on_next_allocation_) return;
+  CollectAllGarbage(kDebugging);
+  gc_on_next_allocation_ = false;
+}
+
 ObjectSet* Heap::CreateAllocatedObjectSet(
     Zone* zone,
     MarkExpectation mark_expectation) const {
diff --git a/runtime/vm/heap/heap.h b/runtime/vm/heap/heap.h
index 837f2c9..6bbcb19 100644
--- a/runtime/vm/heap/heap.h
+++ b/runtime/vm/heap/heap.h
@@ -302,6 +302,8 @@
   void MakeTLABIterable(Thread* thread);
   void AbandonRemainingTLAB(Thread* thread);
 
+  void CollectOnNextAllocation();
+
  private:
   class GCStats : public ValueObject {
    public:
@@ -373,6 +375,9 @@
 
   void AddRegionsToObjectSet(ObjectSet* set) const;
 
+  // Trigger major GC if 'gc_on_next_allocation_' is set.
+  void CollectForDebugging();
+
   Isolate* isolate_;
 
   // The different spaces used for allocation.
@@ -396,6 +401,11 @@
   bool gc_new_space_in_progress_;
   bool gc_old_space_in_progress_;
 
+  // Whether the next heap allocation (new or old) should trigger
+  // CollectAllGarbage. Used within unit tests for testing GC on certain
+  // sensitive codepaths.
+  bool gc_on_next_allocation_;
+
   friend class Become;       // VisitObjectPointers
   friend class GCCompactor;  // VisitObjectPointers
   friend class Precompiler;  // VisitObjects
diff --git a/runtime/vm/lockers.h b/runtime/vm/lockers.h
index 8cce8c3..de62b6c 100644
--- a/runtime/vm/lockers.h
+++ b/runtime/vm/lockers.h
@@ -46,7 +46,11 @@
 class MutexLocker : public ValueObject {
  public:
   explicit MutexLocker(Mutex* mutex, bool no_safepoint_scope = true)
-      : mutex_(mutex), no_safepoint_scope_(no_safepoint_scope) {
+      :
+#if defined(DEBUG)
+        no_safepoint_scope_(no_safepoint_scope),
+#endif
+        mutex_(mutex) {
     ASSERT(mutex != NULL);
 #if defined(DEBUG)
     if (no_safepoint_scope_) {
@@ -88,8 +92,8 @@
   }
 
  private:
+  DEBUG_ONLY(bool no_safepoint_scope_;)
   Mutex* const mutex_;
-  bool no_safepoint_scope_;
 
   DISALLOW_COPY_AND_ASSIGN(MutexLocker);
 };
diff --git a/runtime/vm/native_api_impl.cc b/runtime/vm/native_api_impl.cc
index df1859d..c225bff 100644
--- a/runtime/vm/native_api_impl.cc
+++ b/runtime/vm/native_api_impl.cc
@@ -237,4 +237,17 @@
 #endif  // defined(DART_PRECOMPILED_RUNTIME)
 }
 
+DART_EXPORT void Dart_ExecuteInternalCommand(const char* command) {
+  if (!FLAG_enable_testing_pragmas) return;
+
+  TransitionNativeToVM _(Thread::Current());
+  if (!strcmp(command, "gc-on-next-allocation")) {
+    Isolate::Current()->heap()->CollectOnNextAllocation();
+  } else if (!strcmp(command, "gc-now")) {
+    Isolate::Current()->heap()->CollectAllGarbage(Heap::kDebugging);
+  } else {
+    UNREACHABLE();
+  }
+}
+
 }  // namespace dart
diff --git a/tests/ffi/function_stress_test.dart b/tests/ffi/function_gc_test.dart
similarity index 61%
rename from tests/ffi/function_stress_test.dart
rename to tests/ffi/function_gc_test.dart
index 92e807c..1799ce1 100644
--- a/tests/ffi/function_stress_test.dart
+++ b/tests/ffi/function_gc_test.dart
@@ -2,8 +2,8 @@
 // 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.
 //
-// VMOptions=--deterministic --optimization-counter-threshold=500 --verbose-gc
-// VMOptions=--deterministic --optimization-counter-threshold=-1 --verbose-gc
+// VMOptions=--deterministic --optimization-counter-threshold=500 --enable-testing-pragmas
+// VMOptions=--deterministic --optimization-counter-threshold=-1 --enable-testing-pragmas
 //
 // Dart test program for stress-testing boxing and GC in return paths from FFI
 // trampolines.
@@ -16,45 +16,14 @@
 import 'dart:ffi' as ffi;
 import 'dylib_utils.dart';
 import "package:expect/expect.dart";
-import 'gc_helper.dart';
-
-test(GCWatcher watcher, Function testee,
-    {bool mustTriggerGC: true, bool batched: false}) async {
-  // Warmup.
-  for (int i = 0; i < 1000; ++i) {
-    batched ? testee(1) : testee();
-  }
-  int size = await watcher.size();
-  for (int i = 0; i < 1000000;) {
-    if (batched) {
-      testee(1000);
-      i += 1000;
-    } else {
-      testee();
-      i++;
-    }
-  }
-  int new_size = await watcher.size();
-  if (mustTriggerGC) {
-    print("Expect $new_size > $size.");
-    Expect.isTrue(new_size > size);
-  }
-}
 
 main() async {
-  final watcher = GCWatcher.ifAvailable();
-  try {
-    await test(watcher, testBoxInt64);
-    // On 64-bit platforms this won't trigger GC because the result fits into a
-    // Smi.
-    await test(watcher, testBoxInt32, mustTriggerGC: false);
-    await test(watcher, testBoxDouble);
-    await test(watcher, testBoxPointer);
-    await test(watcher, testAllocateMints, batched: true);
-    await test(watcher, testAllocationsInDart, batched: true);
-  } finally {
-    watcher.dispose();
-  }
+  testBoxInt64();
+  testBoxInt32();
+  testBoxDouble();
+  testBoxPointer();
+  testAllocateInNative();
+  testAllocateInDart();
 }
 
 ffi.DynamicLibrary ffiTestFunctions =
@@ -64,11 +33,13 @@
 typedef NativeNullaryOp32 = ffi.Int32 Function();
 typedef NativeNullaryOpDouble = ffi.Double Function();
 typedef NativeNullaryOpPtr = ffi.Pointer<ffi.Void> Function();
+typedef NativeNullaryOp = ffi.Void Function();
 typedef NativeUnaryOp = ffi.Void Function(ffi.Uint64);
 typedef NullaryOp = int Function();
 typedef NullaryOpDbl = double Function();
 typedef NullaryOpPtr = ffi.Pointer<ffi.Void> Function();
 typedef UnaryOp = void Function(int);
+typedef NullaryOpVoid = void Function();
 
 //// These functions return values that require boxing into different types.
 
@@ -111,33 +82,19 @@
   }
 }
 
-final allocateMint =
-    ffiTestFunctions.lookupFunction<NativeUnaryOp, UnaryOp>("AllocateMints");
+final triggerGc = ffiTestFunctions
+    .lookupFunction<NativeNullaryOp, NullaryOpVoid>("TriggerGC");
 
-// Test GC in the FFI call path by calling a C function which allocates through
-// the Dart API.
-void testAllocateMints(int batchSize) {
-  allocateMint(batchSize);
-}
+// Test GC in the FFI call path by calling a C function which triggers GC
+// directly.
+void testAllocateInNative() => triggerGc();
 
-class C {
-  final int i;
-  C(this.i);
-}
-
-C c = null;
 @pragma("vm:entry-point", "call")
-void testAllocationsInDartHelper(int count) {
-  for (int i = 0; i < count; ++i) {
-    c = C(i);
-  }
-}
+void testAllocationsInDartHelper() => triggerGc();
 
 final allocateThroughDart = ffiTestFunctions
-    .lookupFunction<NativeUnaryOp, UnaryOp>("AllocateThroughDart");
+    .lookupFunction<NativeNullaryOp, NullaryOpVoid>("AllocateThroughDart");
 
 // Test GC in the FFI call path by calling a C function which allocates by
 // calling back into Dart ('testAllocationsInDartHelper').
-void testAllocationsInDart(int batchSize) {
-  allocateThroughDart(batchSize * 10);
-}
+void testAllocateInDart() => allocateThroughDart();
diff --git a/tests/ffi/gc_helper.dart b/tests/ffi/gc_helper.dart
deleted file mode 100644
index abe0454..0000000
--- a/tests/ffi/gc_helper.dart
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (c) 2019, 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 'dylib_utils.dart';
-import 'dart:ffi';
-import 'dart:io' show Platform;
-
-DynamicLibrary ffiTestFunctions = dlopenPlatformSpecific("ffi_test_functions");
-
-const bool isProduct = const bool.fromEnvironment("dart.vm.product");
-
-abstract class GCWatcher {
-  factory GCWatcher() => _GCWatcherImpl();
-  factory GCWatcher.dummy() => _MockGCWatcher();
-  factory GCWatcher.ifAvailable() =>
-      (Platform.isWindows || Platform.isAndroid || isProduct)
-          ? GCWatcher.dummy()
-          : GCWatcher();
-
-  Future<int> size();
-  void dispose();
-}
-
-// Requires --verbose-gc.
-class _GCWatcherImpl implements GCWatcher {
-  int _suffix;
-
-  Future<int> size() async {
-    return await File("/tmp/captured_stderr_$_suffix").length();
-  }
-
-  _GCWatcherImpl() {
-    print("Starting...");
-    _suffix = ffiTestFunctions
-        .lookupFunction<Int32 Function(), int Function()>("RedirectStderr")();
-  }
-
-  dispose() {
-    try {
-      File("/tmp/captured_stderr_$_suffix").deleteSync();
-    } catch (e) {
-      print("deleting file failed");
-    }
-  }
-}
-
-class _MockGCWatcher implements GCWatcher {
-  int _ctr = 0;
-
-  Future<int> size() async => ++_ctr;
-  dispose() {}
-}
diff --git a/tests/ffi/subtype_test.dart b/tests/ffi/subtype_test.dart
index 22dbe13..54977b5 100644
--- a/tests/ffi/subtype_test.dart
+++ b/tests/ffi/subtype_test.dart
@@ -4,6 +4,7 @@
 //
 // Dart test program for testing dart:ffi Pointer subtypes.
 //
+// SharedObjects=ffi_test_functions
 // VMOptions=--verbose-gc
 
 library FfiTest;
@@ -12,21 +13,21 @@
 
 import "package:expect/expect.dart";
 
-import 'gc_helper.dart';
 import 'cstring.dart';
+import 'dylib_utils.dart';
+
+ffi.DynamicLibrary ffiTestFunctions =
+    dlopenPlatformSpecific("ffi_test_functions");
+
+final triggerGc = ffiTestFunctions
+    .lookupFunction<ffi.Void Function(), void Function()>("TriggerGC");
 
 void main() async {
   testAllocate();
   testSizeOf();
-  await testGC();
+  testGC();
 }
 
-class X {
-  X(this.i);
-  int i;
-}
-
-dynamic foo;
 dynamic bar;
 
 void testAllocate() {
@@ -36,15 +37,10 @@
 }
 
 Future<void> testGC() async {
-  CString cs = ffi.fromAddress<CString>(11);
-  bar = cs;
-  foo = "";
-  final watcher = GCWatcher.ifAvailable();
-  int counts = await watcher.size();
-  for (int i = 0; i < 1000000; ++i) {
-    foo = new X(i);
-  }
-  Expect.isTrue(await watcher.size() > counts);
+  bar = ffi.fromAddress<CString>(11);
+  // Verify that the objects manufactured by 'fromAddress' can be scanned by the
+  // GC.
+  triggerGc();
 }
 
 void testSizeOf() {