blob: 15eedc59e653662319e430ec4650535bf2cac240 [file] [log] [blame]
// 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/lockers.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 sucessfully 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 sucessfully 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);
// 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)
: StateMachineTask::Data(isolate_group),
level(level),
gc_only_checkins(gc_only_checkins),
deopt_checkin(deopt_checkin) {}
SafepointLevel level;
std::atomic<intptr_t>* gc_only_checkins;
std::atomic<intptr_t>* deopt_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::GetCurrentTimeMillis();
while (!data()->IsIn(kPleaseExit)) {
switch (data()->level) {
case SafepointLevel::kGC: {
// This thread should join only GC safepoint operations.
RuntimeCallDeoptScope no_deopt(
Thread::Current(), RuntimeCallDeoptAbility::kCannotLazyDeopt);
SafepointIfRequested(thread_, data()->gc_only_checkins);
break;
}
case SafepointLevel::kGCAndDeopt: {
// This thread should join any safepoint operations.
SafepointIfRequested(thread_, data()->deopt_checkin);
break;
}
case SafepointLevel::kNumLevels:
UNREACHABLE();
}
// If we are asked to join a deopt safepoint operation we will comply with
// that but only every second.
const auto now = OS::GetCurrentTimeMillis();
if ((now - last_sync) > 200) {
thread_->EnterSafepoint();
thread_->ExitSafepoint();
last_sync = now;
}
}
}
void SafepointIfRequested(Thread* thread, std::atomic<intptr_t>* checkins) {
OS::SleepMicros(10);
if (thread->IsSafepointRequested()) {
// Collaborates by checking into the safepoint.
thread->BlockForSafepoint();
(*checkins)++;
}
}
};
// 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_kind_ value).
ISOLATE_UNIT_TEST_CASE(SafepointOperation_SafepointPointTest) {
auto isolate_group = thread->isolate_group();
const intptr_t kTaskCount = 5;
std::atomic<intptr_t> gc_only_checkins[kTaskCount];
std::atomic<intptr_t> deopt_checkin[kTaskCount];
for (intptr_t i = 0; i < kTaskCount; ++i) {
gc_only_checkins[i] = 0;
deopt_checkin[i] = 0;
}
std::vector<std::shared_ptr<CheckinTask::Data>> threads;
for (intptr_t i = 0; i < kTaskCount; ++i) {
const auto level =
(i % 2) == 0 ? SafepointLevel::kGC : SafepointLevel::kGCAndDeopt;
std::unique_ptr<CheckinTask::Data> data(new CheckinTask::Data(
isolate_group, level, &gc_only_checkins[i], &deopt_checkin[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); }
OS::SleepMicros(500);
{ DeoptSafepointOperationScope safepoint_operation(thread); }
OS::SleepMicros(500);
{ GcSafepointOperationScope safepoint_operation(thread); }
OS::SleepMicros(500);
{ DeoptSafepointOperationScope safepoint_operation(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);
}
for (intptr_t i = 0; i < kTaskCount; ++i) {
const auto level =
(i % 2) == 0 ? SafepointLevel::kGC : SafepointLevel::kGCAndDeopt;
switch (level) {
case SafepointLevel::kGC:
EXPECT_EQ(0, deopt_checkin[i]);
EXPECT_EQ(2, gc_only_checkins[i]);
break;
case SafepointLevel::kGCAndDeopt:
EXPECT_EQ(4, deopt_checkin[i]);
EXPECT_EQ(0, gc_only_checkins[i]);
break;
case SafepointLevel::kNumLevels:
UNREACHABLE();
}
}
}
}
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_->isolate_group()->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) {
{
DeoptSafepointOperationScope safepoint_scope(thread);
DeoptSafepointOperationScope safepoint_scope2(thread);
GcSafepointOperationScope safepoint_scope3(thread);
GcSafepointOperationScope safepoint_scope4(thread);
}
{
DeoptSafepointOperationScope safepoint_scope(thread);
GcSafepointOperationScope safepoint_scope2(thread);
}
}
ISOLATE_UNIT_TEST_CASE_WITH_EXPECTATION(
SafepointOperation_NonDeoptAndDeoptNesting,
"Crash") {
GcSafepointOperationScope safepoint_scope(thread);
DeoptSafepointOperationScope safepoint_scope2(thread);
}
} // namespace dart