Use normal checkpoints for NotifyStartupCompleted

The API RunEmptyCheckpoint is only safe with one caller (the GC),
incorrect usage was resulting in occasional deadlocks.

The fix is to use a normal checkpoint to do the empty checkpoint.

Bug: 129150130
Test: test-art-host
Change-Id: I8ed73bc8b57c3493f4a995843d5ad63bd1a91ef8
diff --git a/runtime/runtime.cc b/runtime/runtime.cc
index 9ab4047..1eae7ae 100644
--- a/runtime/runtime.cc
+++ b/runtime/runtime.cc
@@ -2802,10 +2802,29 @@
       }
     }
     // Request empty checkpoint to make sure no threads are accessing the section when we madvise
-    // it.
+    // it. Avoid using RunEmptyCheckpoint since only one concurrent caller is supported. We could
+    // add a GC critical section here but that may cause significant jank if the GC is running.
     {
-      ScopedThreadStateChange tsc(Thread::Current(), kSuspended);
-      GetThreadList()->RunEmptyCheckpoint();
+      class EmptyClosure : public Closure {
+       public:
+        explicit EmptyClosure(Barrier* barrier) : barrier_(barrier) {}
+        void Run(Thread* thread ATTRIBUTE_UNUSED) override {
+          barrier_->Pass(Thread::Current());
+        }
+
+       private:
+        Barrier* const barrier_;
+      };
+      Barrier barrier(0);
+      EmptyClosure closure(&barrier);
+      size_t threads_running_checkpoint = GetThreadList()->RunCheckpoint(&closure);
+      // Now that we have run our checkpoint, move to a suspended state and wait
+      // for other threads to run the checkpoint.
+      Thread* self = Thread::Current();
+      ScopedThreadSuspension sts(self, kSuspended);
+      if (threads_running_checkpoint != 0) {
+        barrier.Increment(self, threads_running_checkpoint);
+      }
     }
     for (gc::space::ContinuousSpace* space : GetHeap()->GetContinuousSpaces()) {
       if (space->IsImageSpace()) {