// 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.

#include <memory>
#include <utility>
#include <vector>

#include "platform/assert.h"

#include "vm/heap/safepoint.h"
#include "vm/isolate.h"
#include "vm/isolate_reload.h"
#include "vm/lockers.h"
#include "vm/message_handler.h"
#include "vm/message_snapshot.h"
#include "vm/random.h"
#include "vm/thread_pool.h"
#include "vm/unit_test.h"

namespace dart {

class StateMachineTask : public ThreadPool::Task {
 public:
  enum State {
    kInitialized = 0,
    kEntered,
    kPleaseExit,
    kExited,
    kNext,
  };
  struct Data {
    explicit Data(IsolateGroup* isolate_group)
        : isolate_group_(isolate_group) {}

    void WaitUntil(intptr_t target_state) {
      MonitorLocker ml(&monitor_);
      while (state != target_state) {
        ml.Wait();
      }
    }
    void MarkAndNotify(intptr_t target_state) {
      MonitorLocker ml(&monitor_);
      state = target_state;
      ml.Notify();
    }
    void AssertIsIn(intptr_t expected_state) {
      MonitorLocker ml(&monitor_);
      EXPECT_EQ(expected_state, state);
    }
    void AssertIsNotIn(intptr_t expected_state) {
      MonitorLocker ml(&monitor_);
      EXPECT_NE(expected_state, state);
    }
    bool IsIn(intptr_t expected_state) {
      MonitorLocker ml(&monitor_);
      return expected_state == state;
    }

    intptr_t state = kInitialized;
    IsolateGroup* isolate_group_;

   private:
    Monitor monitor_;
  };

  explicit StateMachineTask(std::shared_ptr<Data> data)
      : data_(std::move(data)) {}

  virtual void Run() {
    const bool kBypassSafepoint = false;
    Thread::EnterIsolateGroupAsHelper(data_->isolate_group_,
                                      Thread::kUnknownTask, kBypassSafepoint);
    thread_ = Thread::Current();
    data_->MarkAndNotify(kEntered);
    RunInternal();
    data_->WaitUntil(kPleaseExit);
    Thread::ExitIsolateGroupAsHelper(kBypassSafepoint);
    thread_ = nullptr;
    data_->MarkAndNotify(kExited);
  }

 protected:
  virtual void RunInternal() = 0;

  std::shared_ptr<Data> data_;
  Thread* thread_ = nullptr;
};

class DeoptTask : public StateMachineTask {
 public:
  enum State {
    kStartDeoptOperation = StateMachineTask::kNext,
    kFinishedDeoptOperation,
  };

  explicit DeoptTask(std::shared_ptr<Data> data)
      : StateMachineTask(std::move(data)) {}

 protected:
  virtual void RunInternal() {
    data_->WaitUntil(kStartDeoptOperation);
    {
      DeoptSafepointOperationScope safepoint_operation(thread_);
    }
    data_->MarkAndNotify(kFinishedDeoptOperation);
  }
};

class GcWithoutDeoptTask : public StateMachineTask {
 public:
  enum State {
    kStartSafepointOperation = StateMachineTask::kNext,
    kEndSafepointOperation,
    kJoinDeoptOperation,
    kDeoptOperationDone,
  };

  explicit GcWithoutDeoptTask(std::shared_ptr<Data> data)
      : StateMachineTask(std::move(data)) {}

 protected:
  virtual void RunInternal() {
    data_->WaitUntil(kStartSafepointOperation);
    {
      RuntimeCallDeoptScope no_deopt(thread_,
                                     RuntimeCallDeoptAbility::kCannotLazyDeopt);
      GcSafepointOperationScope safepoint_operation(thread_);
    }
    data_->MarkAndNotify(kEndSafepointOperation);

    data_->WaitUntil(kJoinDeoptOperation);
    EXPECT(thread_->IsSafepointRequested());
    thread_->BlockForSafepoint();
    data_->MarkAndNotify(kDeoptOperationDone);
  }
};

// This test ensures that while a "deopt safepoint operation" is about to start
// but is still waiting for some threads to hit a "deopt safepoint" another
// safepoint operation can successfully start and finish.
ISOLATE_UNIT_TEST_CASE(
    SafepointOperation_SafepointOpWhileDeoptSafepointOpBlocked) {
  auto isolate_group = thread->isolate_group();

  std::shared_ptr<DeoptTask::Data> deopt(new DeoptTask::Data(isolate_group));
  std::shared_ptr<GcWithoutDeoptTask::Data> gc(
      new GcWithoutDeoptTask::Data(isolate_group));

  thread->EnterSafepoint();
  {
    // Will join outstanding threads on destruction.
    ThreadPool pool;

    pool.Run<DeoptTask>(deopt);
    pool.Run<GcWithoutDeoptTask>(gc);

    // Wait until both threads entered the isolate group.
    deopt->WaitUntil(DeoptTask::kEntered);
    gc->WaitUntil(GcWithoutDeoptTask::kEntered);

    // Let deopt task start deopt operation scope (it will block in
    // [SafepointOperationScope] constructor until all threads have checked-in).
    deopt->MarkAndNotify(DeoptTask::kStartDeoptOperation);
    OS::Sleep(200);  // Give it time to actually start the deopt operation

    // Now let the other thread do a full safepoint operation and wait until
    // it's done: We want to ensure that we can do normal safepoint operations
    // while a deopt operation is being started and is waiting for all mutators
    // to reach an appropriate place where they can be deopted.
    gc->MarkAndNotify(GcWithoutDeoptTask::kStartSafepointOperation);
    gc->WaitUntil(GcWithoutDeoptTask::kEndSafepointOperation);

    // We were successfully doing a safepoint operation, now let's ensure the
    // first thread is still stuck in the starting of deopt operation.
    deopt->AssertIsIn(DeoptTask::kStartDeoptOperation);

    // Now we'll let the other thread check-in and ensure the deopt operation
    // proceeded and finished.
    gc->MarkAndNotify(GcWithoutDeoptTask::kJoinDeoptOperation);
    gc->WaitUntil(GcWithoutDeoptTask::kDeoptOperationDone);
    deopt->WaitUntil(DeoptTask::kFinishedDeoptOperation);

    // Make both threads exit the isolate group.
    deopt->MarkAndNotify(DeoptTask::kPleaseExit);
    gc->MarkAndNotify(GcWithoutDeoptTask::kPleaseExit);

    deopt->WaitUntil(DeoptTask::kExited);
    gc->WaitUntil(GcWithoutDeoptTask::kExited);
  }
  thread->ExitSafepoint();
}

class LongDeoptTask : public StateMachineTask {
 public:
  enum State {
    kStartDeoptOperation = StateMachineTask::kNext,
    kInsideDeoptOperation,
    kFinishDeoptOperation,
    kFinishedDeoptOperation,
  };

  explicit LongDeoptTask(std::shared_ptr<Data> data)
      : StateMachineTask(std::move(data)) {}

 protected:
  virtual void RunInternal() {
    data_->WaitUntil(kStartDeoptOperation);
    {
      DeoptSafepointOperationScope safepoint_operation(thread_);
      data_->MarkAndNotify(kInsideDeoptOperation);
      data_->WaitUntil(kFinishDeoptOperation);
    }
    data_->MarkAndNotify(kFinishedDeoptOperation);
  }
};

class WaiterTask : public StateMachineTask {
 public:
  enum State {
    kEnterSafepoint = StateMachineTask::kNext,
    kInsideSafepoint,
    kPleaseExitSafepoint,
    kExitedSafepoint,
  };

  explicit WaiterTask(std::shared_ptr<Data> data)
      : StateMachineTask(std::move(data)) {}

 protected:
  virtual void RunInternal() {
    data_->WaitUntil(kEnterSafepoint);
    thread_->EnterSafepoint();
    data_->MarkAndNotify(kInsideSafepoint);
    data_->WaitUntil(kPleaseExitSafepoint);
    thread_->ExitSafepoint();
    data_->MarkAndNotify(kExitedSafepoint);
  }
};

// This test ensures that while a "deopt safepoint operation" is in-progress
// other threads cannot perform a normal "safepoint operation".
ISOLATE_UNIT_TEST_CASE(
    SafepointOperation_SafepointOpBlockedWhileDeoptSafepointOp) {
  auto isolate_group = thread->isolate_group();

  std::shared_ptr<LongDeoptTask::Data> deopt(
      new LongDeoptTask::Data(isolate_group));
  std::shared_ptr<WaiterTask::Data> gc(new WaiterTask::Data(isolate_group));

  thread->EnterSafepoint();
  {
    // Will join outstanding threads on destruction.
    ThreadPool pool;

    pool.Run<LongDeoptTask>(deopt);
    pool.Run<WaiterTask>(gc);

    // Wait until both threads entered the isolate group.
    deopt->WaitUntil(LongDeoptTask::kEntered);
    gc->WaitUntil(WaiterTask::kEntered);

    // Let gc task enter safepoint.
    gc->MarkAndNotify(WaiterTask::kEnterSafepoint);
    gc->WaitUntil(WaiterTask::kInsideSafepoint);

    // Now let the "deopt operation" run and block.
    deopt->MarkAndNotify(LongDeoptTask::kStartDeoptOperation);
    deopt->WaitUntil(LongDeoptTask::kInsideDeoptOperation);

    // Now let the gc task try to exit safepoint and do it's own safepoint
    // operation: We expect it to block on exiting safepoint, since the deopt
    // operation is still ongoing.
    gc->MarkAndNotify(WaiterTask::kPleaseExitSafepoint);
    OS::Sleep(200);
    gc->AssertIsNotIn(WaiterTask::kExitedSafepoint);

    // Now let's finish the deopt operation & ensure the waiter thread made
    // progress.
    deopt->MarkAndNotify(LongDeoptTask::kFinishDeoptOperation);
    gc->WaitUntil(WaiterTask::kExitedSafepoint);
    deopt->WaitUntil(LongDeoptTask::kFinishedDeoptOperation);

    // Make both threads exit the isolate group.
    deopt->MarkAndNotify(LongDeoptTask::kPleaseExit);
    gc->MarkAndNotify(WaiterTask::kPleaseExit);

    deopt->WaitUntil(LongDeoptTask::kExited);
    gc->WaitUntil(WaiterTask::kExited);
  }
  thread->ExitSafepoint();
}

class CheckinTask : public StateMachineTask {
 public:
  enum State {
    kStartLoop = StateMachineTask::kNext,
  };

  struct Data : public StateMachineTask::Data {
    Data(IsolateGroup* isolate_group,
         SafepointLevel level,
         std::atomic<intptr_t>* gc_only_checkins,
         std::atomic<intptr_t>* deopt_checkin,
         std::atomic<intptr_t>* reload_checkin,
         std::atomic<intptr_t>* timeout_checkin)
        : StateMachineTask::Data(isolate_group),
          level(level),
          gc_only_checkins(gc_only_checkins),
          deopt_checkin(deopt_checkin),
          reload_checkin(reload_checkin),
          timeout_checkin(timeout_checkin) {}

    SafepointLevel level;
    std::atomic<intptr_t>* gc_only_checkins;
    std::atomic<intptr_t>* deopt_checkin;
    std::atomic<intptr_t>* reload_checkin;
    std::atomic<intptr_t>* timeout_checkin;
  };

  explicit CheckinTask(std::shared_ptr<Data> data) : StateMachineTask(data) {}

 protected:
  Data* data() { return reinterpret_cast<Data*>(data_.get()); }

  virtual void RunInternal() {
    data_->WaitUntil(kStartLoop);

    uword last_sync = OS::GetCurrentMonotonicMicros();
    while (!data()->IsIn(kPleaseExit)) {
      OS::SleepMicros(100);  // Make test avoid consuming 100% CPU x kTaskCount.
      switch (data()->level) {
        case SafepointLevel::kGC: {
          // This thread should join only GC safepoint operations.
          RuntimeCallDeoptScope no_deopt(
              Thread::Current(), RuntimeCallDeoptAbility::kCannotLazyDeopt);
          if (SafepointIfRequested(thread_, data()->gc_only_checkins)) {
            last_sync = OS::GetCurrentMonotonicMicros();
          }
          break;
        }
        case SafepointLevel::kGCAndDeopt: {
          // This thread should join only GC and Deopt safepoint operations.
          if (SafepointIfRequested(thread_, data()->deopt_checkin)) {
            last_sync = OS::GetCurrentMonotonicMicros();
          }
          break;
        }
        case SafepointLevel::kGCAndDeoptAndReload: {
          // This thread should join any safepoint operations.
          ReloadParticipationScope allow_reload(thread_);
          if (SafepointIfRequested(thread_, data()->reload_checkin)) {
            last_sync = OS::GetCurrentMonotonicMicros();
          }
          break;
        }
        case SafepointLevel::kNumLevels:
        case SafepointLevel::kNoSafepoint:
          UNREACHABLE();
      }

      // If the main thread asks us to join a deopt safepoint but we are
      // instructed to only really collaborate with GC safepoints we won't
      // participate in the above cases (and therefore not register our
      // check-in by increasing the checkin counts).
      //
      // After being quite sure to not have joined deopt safepoint if we only
      // support GC safepoints, we will eventually comply here to make main
      // thread continue.
      const auto now = OS::GetCurrentMonotonicMicros();
      if ((now - last_sync) > 1000000) {
        ReloadParticipationScope allow_reload(thread_);
        if (SafepointIfRequested(thread_, data()->timeout_checkin)) {
          last_sync = now;
        }
      }
    }
  }

  bool SafepointIfRequested(Thread* thread, std::atomic<intptr_t>* checkins) {
    if (thread->IsSafepointRequested()) {
      // Collaborates by checking into the safepoint.
      thread->BlockForSafepoint();
      (*checkins)++;
      return true;
    }
    return false;
  }
};

// Test that mutators will not check-in to "deopt safepoint operations" at
// at places where the mutator cannot depot (which is indicated by the
// [Thread::runtime_call_deopt_ability_] value).
#if !defined(PRODUCT)
ISOLATE_UNIT_TEST_CASE(SafepointOperation_SafepointPointTest) {
  auto isolate_group = thread->isolate_group();

  const intptr_t kTaskCount = 6;
  std::atomic<intptr_t> gc_only_checkins[kTaskCount];
  std::atomic<intptr_t> deopt_checkins[kTaskCount];
  std::atomic<intptr_t> reload_checkins[kTaskCount];
  std::atomic<intptr_t> timeout_checkins[kTaskCount];
  for (intptr_t i = 0; i < kTaskCount; ++i) {
    gc_only_checkins[i] = 0;
    deopt_checkins[i] = 0;
    reload_checkins[i] = 0;
    timeout_checkins[i] = 0;
  }

  auto task_to_level = [](intptr_t task_id) -> SafepointLevel {
    switch (task_id) {
      case 0:
      case 1:
        return SafepointLevel::kGC;
      case 2:
      case 3:
        return SafepointLevel::kGCAndDeopt;
      case 4:
      case 5:
        return SafepointLevel::kGCAndDeoptAndReload;
      default:
        UNREACHABLE();
        return SafepointLevel::kGC;
    }
  };
  auto wait_for_sync = [&](intptr_t syncs) {
    while (true) {
      bool ready = true;
      for (intptr_t i = 0; i < kTaskCount; ++i) {
        const intptr_t all = gc_only_checkins[i] + deopt_checkins[i] +
                             reload_checkins[i] + timeout_checkins[i];
        if (all != syncs) {
          ready = false;
          break;
        }
      }
      if (ready) {
        return;
      }
      OS::SleepMicros(1000);
    }
  };

  std::vector<std::shared_ptr<CheckinTask::Data>> threads;
  for (intptr_t i = 0; i < kTaskCount; ++i) {
    const auto level = task_to_level(i);
    std::unique_ptr<CheckinTask::Data> data(new CheckinTask::Data(
        isolate_group, level, &gc_only_checkins[i], &deopt_checkins[i],
        &reload_checkins[i], &timeout_checkins[i]));
    threads.push_back(std::move(data));
  }

  {
    // Will join outstanding threads on destruction.
    ThreadPool pool;

    for (intptr_t i = 0; i < kTaskCount; i++) {
      pool.Run<CheckinTask>(threads[i]);
    }
    for (intptr_t i = 0; i < kTaskCount; i++) {
      threads[i]->WaitUntil(CheckinTask::kEntered);
    }
    for (intptr_t i = 0; i < kTaskCount; i++) {
      threads[i]->MarkAndNotify(CheckinTask::kStartLoop);
    }
    {
      {
        GcSafepointOperationScope safepoint_operation(thread);
      }
      wait_for_sync(1);  // Wait for threads to exit safepoint
      {
        DeoptSafepointOperationScope safepoint_operation(thread);
      }
      wait_for_sync(2);  // Wait for threads to exit safepoint
      {
        RELOAD_OPERATION_SCOPE(thread);
      }
      wait_for_sync(3);  // Wait for threads to exit safepoint
      {
        GcSafepointOperationScope safepoint_operation(thread);
      }
      wait_for_sync(4);  // Wait for threads to exit safepoint
      {
        DeoptSafepointOperationScope safepoint_operation(thread);
      }
      wait_for_sync(5);  // Wait for threads to exit safepoint
      {
        RELOAD_OPERATION_SCOPE(thread);
      }
    }
    for (intptr_t i = 0; i < kTaskCount; i++) {
      threads[i]->MarkAndNotify(CheckinTask::kPleaseExit);
    }
    for (intptr_t i = 0; i < kTaskCount; i++) {
      threads[i]->WaitUntil(CheckinTask::kExited);
    }
    // Unlucky scheduling may result in more timeout checkins than intended, but
    // never checkins of otherwise the wrong type.
    for (intptr_t i = 0; i < kTaskCount; ++i) {
      switch (task_to_level(i)) {
        case SafepointLevel::kGC:
          EXPECT_LE(gc_only_checkins[i], 2);
          EXPECT_EQ(deopt_checkins[i], 0);
          EXPECT_EQ(reload_checkins[i], 0);
          EXPECT_GE(timeout_checkins[i], 4);
          break;
        case SafepointLevel::kGCAndDeopt:
          EXPECT_EQ(gc_only_checkins[i], 0);
          EXPECT_LE(deopt_checkins[i], 4);
          EXPECT_EQ(reload_checkins[i], 0);
          EXPECT_GE(timeout_checkins[i], 2);
          break;
        case SafepointLevel::kGCAndDeoptAndReload:
          EXPECT_EQ(gc_only_checkins[i], 0);
          EXPECT_EQ(deopt_checkins[i], 0);
          EXPECT_LE(reload_checkins[i], 6);
          EXPECT_GE(timeout_checkins[i], 0);
          break;
        case SafepointLevel::kNumLevels:
        case SafepointLevel::kNoSafepoint:
          UNREACHABLE();
      }
    }
  }
}
#endif  // !defined(PRODUCT)

class StressTask : public StateMachineTask {
 public:
  enum State {
    kStart = StateMachineTask::kNext,
  };

  explicit StressTask(std::shared_ptr<Data> data) : StateMachineTask(data) {}

 protected:
  Data* data() { return reinterpret_cast<Data*>(data_.get()); }

  virtual void RunInternal() {
    data_->WaitUntil(kStart);

    Random random(thread_->random()->NextUInt64());
    while (!data()->IsIn(kPleaseExit)) {
      const auto us = random.NextUInt32() % 3;
      switch (random.NextUInt32() % 5) {
        case 0: {
          DeoptSafepointOperationScope safepoint_op(thread_);
          OS::SleepMicros(us);
          break;
        }
        case 1: {
          GcSafepointOperationScope safepoint_op(thread_);
          OS::SleepMicros(us);
          break;
        }
        case 2: {
          const bool kBypassSafepoint = false;
          Thread::ExitIsolateGroupAsHelper(kBypassSafepoint);
          OS::SleepMicros(us);
          Thread::EnterIsolateGroupAsHelper(
              data_->isolate_group_, Thread::kUnknownTask, kBypassSafepoint);
          thread_ = Thread::Current();
          break;
        }
        case 3: {
          thread_->EnterSafepoint();
          OS::SleepMicros(us);
          thread_->ExitSafepoint();
          break;
        }
        case 4: {
          if (thread_->IsSafepointRequested()) {
            thread_->BlockForSafepoint();
          }
          break;
        }
      }
    }
  }
};

ISOLATE_UNIT_TEST_CASE(SafepointOperation_StressTest) {
  auto isolate_group = thread->isolate_group();

  const intptr_t kTaskCount = 5;

  std::vector<std::shared_ptr<StressTask::Data>> threads;
  for (intptr_t i = 0; i < kTaskCount; ++i) {
    std::unique_ptr<StressTask::Data> data(new StressTask::Data(isolate_group));
    threads.push_back(std::move(data));
  }

  thread->EnterSafepoint();
  {
    // Will join outstanding threads on destruction.
    ThreadPool pool;

    for (intptr_t i = 0; i < kTaskCount; i++) {
      pool.Run<StressTask>(threads[i]);
    }
    for (intptr_t i = 0; i < kTaskCount; i++) {
      threads[i]->WaitUntil(StressTask::kEntered);
    }
    for (intptr_t i = 0; i < kTaskCount; i++) {
      threads[i]->MarkAndNotify(StressTask::kStart);
    }
    OS::Sleep(3 * 1000);
    for (intptr_t i = 0; i < kTaskCount; i++) {
      threads[i]->MarkAndNotify(StressTask::kPleaseExit);
    }
    for (intptr_t i = 0; i < kTaskCount; i++) {
      threads[i]->WaitUntil(StressTask::kExited);
    }
  }
  thread->ExitSafepoint();
}

ISOLATE_UNIT_TEST_CASE(SafepointOperation_DeoptAndNonDeoptNesting) {
  auto safepoint_handler = thread->isolate_group()->safepoint_handler();
  {
    DeoptSafepointOperationScope safepoint_scope(thread);
    EXPECT(safepoint_handler->InnermostSafepointOperation(thread) ==
           SafepointLevel::kGCAndDeopt);
    DeoptSafepointOperationScope safepoint_scope2(thread);
    EXPECT(safepoint_handler->InnermostSafepointOperation(thread) ==
           SafepointLevel::kGCAndDeopt);
    GcSafepointOperationScope safepoint_scope3(thread);
    EXPECT(safepoint_handler->InnermostSafepointOperation(thread) ==
           SafepointLevel::kGC);
    GcSafepointOperationScope safepoint_scope4(thread);
    EXPECT(safepoint_handler->InnermostSafepointOperation(thread) ==
           SafepointLevel::kGC);
  }
  {
    DeoptSafepointOperationScope safepoint_scope(thread);
    EXPECT(safepoint_handler->InnermostSafepointOperation(thread) ==
           SafepointLevel::kGCAndDeopt);
    GcSafepointOperationScope safepoint_scope2(thread);
    EXPECT(safepoint_handler->InnermostSafepointOperation(thread) ==
           SafepointLevel::kGC);
  }
  {
    GcSafepointOperationScope safepoint_scope1(thread);
    EXPECT(safepoint_handler->InnermostSafepointOperation(thread) ==
           SafepointLevel::kGC);
    GcSafepointOperationScope safepoint_scope2(thread);
    EXPECT(safepoint_handler->InnermostSafepointOperation(thread) ==
           SafepointLevel::kGC);
  }
}

ISOLATE_UNIT_TEST_CASE_WITH_EXPECTATION(
    SafepointOperation_NonDeoptAndDeoptNesting,
    "Crash") {
  GcSafepointOperationScope safepoint_scope(thread);
  DeoptSafepointOperationScope safepoint_scope2(thread);
}

class IsolateExitScope {
 public:
  IsolateExitScope() : saved_isolate_(Dart_CurrentIsolate()) {
    Dart_ExitIsolate();
  }
  ~IsolateExitScope() { Dart_EnterIsolate(saved_isolate_); }

 private:
  Dart_Isolate saved_isolate_;
};

ISOLATE_UNIT_TEST_CASE(ReloadScopes_Test) {
  // Unscheduling an isolate will enter a safepoint that is reloadable.
  {
    TransitionVMToNative transition(thread);
    IsolateExitScope isolate_leave_scope;
#if !defined(PRODUCT)
    EXPECT(thread->IsAtSafepoint(SafepointLevel::kGCAndDeoptAndReload));
#else
    EXPECT(thread->IsAtSafepoint(SafepointLevel::kGCAndDeopt));
    EXPECT(!thread->IsAtSafepoint(SafepointLevel::kGCAndDeoptAndReload));
#endif
  }

  // Unscheduling an isolate with active [NoReloadScope] will enter a safepoint
  // that is not reloadable.
  {
    NoReloadScope no_reload_scope(thread);
    TransitionVMToNative transition(thread);
    IsolateExitScope isolate_leave_scope;
    EXPECT(thread->IsAtSafepoint(SafepointLevel::kGCAndDeopt));
  }

  // Native/FFI calls are generally reloadable.
  {
    TransitionVMToNative transition(thread);
#if !defined(PRODUCT)
    EXPECT(thread->IsAtSafepoint(SafepointLevel::kGCAndDeoptAndReload));
#else
    EXPECT(thread->IsAtSafepoint(SafepointLevel::kGCAndDeopt));
    EXPECT(!thread->IsAtSafepoint(SafepointLevel::kGCAndDeoptAndReload));
#endif
  }

  // But in some contexts like compiling should not be reloadable.
  {
    NoReloadScope no_reload_scope(thread);
    TransitionVMToNative transition(thread);
    EXPECT(thread->IsAtSafepoint(SafepointLevel::kGCAndDeopt));
    EXPECT(!thread->IsAtSafepoint(SafepointLevel::kGCAndDeoptAndReload));
  }
}

#if !defined(PRODUCT)
class ReloadTask : public StateMachineTask {
 public:
  using Data = StateMachineTask::Data;

  explicit ReloadTask(std::shared_ptr<Data> data) : StateMachineTask(data) {}

 protected:
  virtual void RunInternal() { RELOAD_OPERATION_SCOPE(thread_); }
};

ISOLATE_UNIT_TEST_CASE(Reload_AtReloadSafepoint) {
  auto isolate = thread->isolate();
  auto messages = isolate->message_handler();

  ThreadPool pool;

  {
    ReloadParticipationScope allow_reload(thread);

    // We are not at a safepoint.
    ASSERT(!thread->IsAtSafepoint());

    // Enter a reload safepoint.
    thread->EnterSafepoint();
    {
      // The [ReloadTask] will trigger a reload safepoint operation, sees that
      // we are at reload safepoint & finishes without sending OOB message.
      std::shared_ptr<ReloadTask::Data> task(
          new ReloadTask::Data(isolate->group()));
      pool.Run<ReloadTask>(task);
      task->WaitUntil(ReloadTask::kEntered);
      task->MarkAndNotify(ReloadTask::kPleaseExit);
      task->WaitUntil(ReloadTask::kExited);
    }
    thread->ExitSafepoint();

    EXPECT(!messages->HasOOBMessages());
  }
}

static void EnsureValidOOBMessage(Thread* thread,
                                  Isolate* isolate,
                                  std::unique_ptr<Message> message) {
  EXPECT(message->IsOOB());
  EXPECT(message->dest_port() == isolate->main_port());

  const auto& msg = Object::Handle(ReadMessage(thread, message.get()));
  EXPECT(msg.IsArray());

  const auto& array = Array::Cast(msg);
  EXPECT(array.Length() == 3);
  EXPECT(Smi::Value(Smi::RawCast(array.At(0))) == Message::kIsolateLibOOBMsg);
  EXPECT(Smi::Value(Smi::RawCast(array.At(1))) == Isolate::kCheckForReload);
  // 3rd value is ignored.
}

ISOLATE_UNIT_TEST_CASE(Reload_NotAtSafepoint) {
  auto isolate = thread->isolate();
  auto messages = isolate->message_handler();

  ThreadPool pool;

  std::shared_ptr<ReloadTask::Data> task(
      new ReloadTask::Data(isolate->group()));

  {
    // Even if we are not running with an active isolate (e.g. due to being in
    // GC / Compiler) the reload safepoint operation should still send us an OOB
    // message (it should know this thread belongs to an isolate).
    NoActiveIsolateScope no_active_isolate(thread);

    pool.Run<ReloadTask>(task);
    task->WaitUntil(ReloadTask::kEntered);

    // We are not at a safepoint. The [ReloadTask] will trigger a reload
    // safepoint operation, sees that we are not at reload safepoint and instead
    // sends us an OOB.
    ASSERT(!thread->IsAtSafepoint());
    while (!messages->HasOOBMessages()) {
      OS::Sleep(1000);
    }
  }

  // Examine the OOB message for it's content.
  std::unique_ptr<Message> message = messages->StealOOBMessage();
  EnsureValidOOBMessage(thread, isolate, std::move(message));

  // Finally participate in the reload safepoint and finish.
  {
    ReloadParticipationScope allow_reload(thread);
    thread->BlockForSafepoint();
  }

  task->MarkAndNotify(ReloadTask::kPleaseExit);
  task->WaitUntil(ReloadTask::kExited);
}

ISOLATE_UNIT_TEST_CASE(Reload_AtNonReloadSafepoint) {
  auto isolate = thread->isolate();
  auto messages = isolate->message_handler();

  ThreadPool pool;

  // The [ReloadTask] will trigger a reload safepoint operation, sees that
  // we are at not at reload safepoint & sends us an OOB and waits for us to
  // check-in.
  std::shared_ptr<ReloadTask::Data> task(
      new ReloadTask::Data(isolate->group()));
  pool.Run<ReloadTask>(task);
  task->WaitUntil(ReloadTask::kEntered);

  {
    NoReloadScope no_reload(thread);

    // We are not at a safepoint.
    ASSERT(!thread->IsAtSafepoint());

    // Enter a non-reload safepoint.
    thread->EnterSafepoint();
    {
      // We are at a safepoint but not a reload safepoint. So we'll get an OOM.
      ASSERT(thread->IsAtSafepoint());
      while (!messages->HasOOBMessages()) {
        OS::Sleep(1000);
      }
      // Ensure we got a valid OOM
      std::unique_ptr<Message> message = messages->StealOOBMessage();
      EnsureValidOOBMessage(thread, isolate, std::move(message));
    }
    thread->ExitSafepoint();

    EXPECT(!messages->HasOOBMessages());
  }

  // We left the [NoReloadScope] which in it's destructor should detect
  // that a reload safepoint operation is requested and re-send OOM message to
  // current isolate.
  EXPECT(messages->HasOOBMessages());
  std::unique_ptr<Message> message = messages->StealOOBMessage();
  EnsureValidOOBMessage(thread, isolate, std::move(message));

  // Finally participate in the reload safepoint and finish.
  {
    ReloadParticipationScope allow_reload(thread);
    thread->BlockForSafepoint();
  }

  task->MarkAndNotify(ReloadTask::kPleaseExit);
  task->WaitUntil(ReloadTask::kExited);
}
#endif  // !defined(PRODUCT)

}  // namespace dart
