// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/test/scoped_task_environment.h"
#include "base/bind_helpers.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/synchronization/condition_variable.h"
#include "base/synchronization/lock.h"
#include "base/task_scheduler/post_task.h"
#include "base/task_scheduler/task_scheduler.h"
#include "base/task_scheduler/task_scheduler_impl.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/threading/sequence_local_storage_map.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#if defined(OS_POSIX)
#include "base/files/file_descriptor_watcher_posix.h"
#endif
namespace base {
namespace test {
namespace {
std::unique_ptr<MessageLoop> CreateMessageLoopForMainThreadType(
ScopedTaskEnvironment::MainThreadType main_thread_type) {
switch (main_thread_type) {
case ScopedTaskEnvironment::MainThreadType::DEFAULT:
return std::make_unique<MessageLoop>(MessageLoop::TYPE_DEFAULT);
case ScopedTaskEnvironment::MainThreadType::MOCK_TIME:
return nullptr;
case ScopedTaskEnvironment::MainThreadType::UI:
return std::make_unique<MessageLoop>(MessageLoop::TYPE_UI);
case ScopedTaskEnvironment::MainThreadType::IO:
return std::make_unique<MessageLoop>(MessageLoop::TYPE_IO);
}
NOTREACHED();
return nullptr;
}
} // namespace
class ScopedTaskEnvironment::TestTaskTracker
: public internal::TaskSchedulerImpl::TaskTrackerImpl {
public:
TestTaskTracker();
// Allow running tasks.
void AllowRunTasks();
// Disallow running tasks. Returns true on success; success requires there to
// be no tasks currently running. Returns false if >0 tasks are currently
// running. Prior to returning false, it will attempt to block until at least
// one task has completed (in an attempt to avoid callers busy-looping
// DisallowRunTasks() calls with the same set of slowly ongoing tasks). This
// block attempt will also have a short timeout (in an attempt to prevent the
// fallout of blocking: if the only task remaining is blocked on the main
// thread, waiting for it to complete results in a deadlock...).
bool DisallowRunTasks();
private:
friend class ScopedTaskEnvironment;
// internal::TaskSchedulerImpl::TaskTrackerImpl:
void RunOrSkipTask(internal::Task task,
internal::Sequence* sequence,
bool can_run_task) override;
// Synchronizes accesses to members below.
Lock lock_;
// True if running tasks is allowed.
bool can_run_tasks_ = true;
// Signaled when |can_run_tasks_| becomes true.
ConditionVariable can_run_tasks_cv_;
// Signaled when a task is completed.
ConditionVariable task_completed_;
// Number of tasks that are currently running.
int num_tasks_running_ = 0;
DISALLOW_COPY_AND_ASSIGN(TestTaskTracker);
};
ScopedTaskEnvironment::ScopedTaskEnvironment(
MainThreadType main_thread_type,
ExecutionMode execution_control_mode)
: execution_control_mode_(execution_control_mode),
message_loop_(CreateMessageLoopForMainThreadType(main_thread_type)),
mock_time_task_runner_(
main_thread_type == MainThreadType::MOCK_TIME
? MakeRefCounted<TestMockTimeTaskRunner>(
TestMockTimeTaskRunner::Type::kBoundToThread)
: nullptr),
slsm_for_mock_time_(
main_thread_type == MainThreadType::MOCK_TIME
? std::make_unique<internal::SequenceLocalStorageMap>()
: nullptr),
slsm_registration_for_mock_time_(
main_thread_type == MainThreadType::MOCK_TIME
? std::make_unique<
internal::ScopedSetSequenceLocalStorageMapForCurrentThread>(
slsm_for_mock_time_.get())
: nullptr),
#if defined(OS_POSIX)
file_descriptor_watcher_(
main_thread_type == MainThreadType::IO
? std::make_unique<FileDescriptorWatcher>(
static_cast<MessageLoopForIO*>(message_loop_.get()))
: nullptr),
#endif // defined(OS_POSIX)
task_tracker_(new TestTaskTracker()) {
CHECK(!TaskScheduler::GetInstance());
// Instantiate a TaskScheduler with 2 threads in each of its 4 pools. Threads
// stay alive even when they don't have work.
// Each pool uses two threads to prevent deadlocks in unit tests that have a
// sequence that uses WithBaseSyncPrimitives() to wait on the result of
// another sequence. This isn't perfect (doesn't solve wait chains) but solves
// the basic use case for now.
// TODO(fdoray/jeffreyhe): Make the TaskScheduler dynamically replace blocked
// threads and get rid of this limitation. http://crbug.com/738104
constexpr int kMaxThreads = 2;
const TimeDelta kSuggestedReclaimTime = TimeDelta::Max();
const SchedulerWorkerPoolParams worker_pool_params(kMaxThreads,
kSuggestedReclaimTime);
TaskScheduler::SetInstance(std::make_unique<internal::TaskSchedulerImpl>(
"ScopedTaskEnvironment", WrapUnique(task_tracker_)));
task_scheduler_ = TaskScheduler::GetInstance();
TaskScheduler::GetInstance()->Start({worker_pool_params, worker_pool_params,
worker_pool_params, worker_pool_params});
if (execution_control_mode_ == ExecutionMode::QUEUED)
CHECK(task_tracker_->DisallowRunTasks());
}
ScopedTaskEnvironment::~ScopedTaskEnvironment() {
// Ideally this would RunLoop().RunUntilIdle() here to catch any errors or
// infinite post loop in the remaining work but this isn't possible right now
// because base::~MessageLoop() didn't use to do this and adding it here would
// make the migration away from MessageLoop that much harder.
CHECK_EQ(TaskScheduler::GetInstance(), task_scheduler_);
// Without FlushForTesting(), DeleteSoon() and ReleaseSoon() tasks could be
// skipped, resulting in memory leaks.
task_tracker_->AllowRunTasks();
TaskScheduler::GetInstance()->FlushForTesting();
TaskScheduler::GetInstance()->Shutdown();
TaskScheduler::GetInstance()->JoinForTesting();
// Destroying TaskScheduler state can result in waiting on worker threads.
// Make sure this is allowed to avoid flaking tests that have disallowed waits
// on their main thread.
ScopedAllowBaseSyncPrimitivesForTesting allow_waits_to_destroy_task_tracker;
TaskScheduler::SetInstance(nullptr);
}
scoped_refptr<base::SingleThreadTaskRunner>
ScopedTaskEnvironment::GetMainThreadTaskRunner() {
if (message_loop_)
return message_loop_->task_runner();
DCHECK(mock_time_task_runner_);
return mock_time_task_runner_;
}
bool ScopedTaskEnvironment::MainThreadHasPendingTask() const {
if (message_loop_)
return !message_loop_->IsIdleForTesting();
DCHECK(mock_time_task_runner_);
return mock_time_task_runner_->HasPendingTask();
}
void ScopedTaskEnvironment::RunUntilIdle() {
// TODO(gab): This can be heavily simplified to essentially:
// bool HasMainThreadTasks() {
// if (message_loop_)
// return !message_loop_->IsIdleForTesting();
// return mock_time_task_runner_->NextPendingTaskDelay().is_zero();
// }
// while (task_tracker_->HasIncompleteTasks() || HasMainThreadTasks()) {
// base::RunLoop().RunUntilIdle();
// // Avoid busy-looping.
// if (task_tracker_->HasIncompleteTasks())
// PlatformThread::Sleep(TimeDelta::FromMilliSeconds(1));
// }
// Challenge: HasMainThreadTasks() requires support for proper
// IncomingTaskQueue::IsIdleForTesting() (check all queues).
//
// Other than that it works because once |task_tracker_->HasIncompleteTasks()|
// is false we know for sure that the only thing that can make it true is a
// main thread task (ScopedTaskEnvironment owns all the threads). As such we
// can't racily see it as false on the main thread and be wrong as if it the
// main thread sees the atomic count at zero, it's the only one that can make
// it go up. And the only thing that can make it go up on the main thread are
// main thread tasks and therefore we're done if there aren't any left.
//
// This simplification further allows simplification of DisallowRunTasks().
//
// This can also be simplified even further once TaskTracker becomes directly
// aware of main thread tasks. https://crbug.com/660078.
for (;;) {
task_tracker_->AllowRunTasks();
// First run as many tasks as possible on the main thread in parallel with
// tasks in TaskScheduler. This increases likelihood of TSAN catching
// threading errors and eliminates possibility of hangs should a
// TaskScheduler task synchronously block on a main thread task
// (TaskScheduler::FlushForTesting() can't be used here for that reason).
RunLoop().RunUntilIdle();
// Then halt TaskScheduler. DisallowRunTasks() failing indicates that there
// were TaskScheduler tasks currently running. In that case, try again from
// top when DisallowRunTasks() yields control back to this thread as they
// may have posted main thread tasks.
if (!task_tracker_->DisallowRunTasks())
continue;
// Once TaskScheduler is halted. Run any remaining main thread tasks (which
// may have been posted by TaskScheduler tasks that completed between the
// above main thread RunUntilIdle() and TaskScheduler DisallowRunTasks()).
// Note: this assumes that no main thread task synchronously blocks on a
// TaskScheduler tasks (it certainly shouldn't); this call could otherwise
// hang.
RunLoop().RunUntilIdle();
// The above RunUntilIdle() guarantees there are no remaining main thread
// tasks (the TaskScheduler being halted during the last RunUntilIdle() is
// key as it prevents a task being posted to it racily with it determining
// it had no work remaining). Therefore, we're done if there is no more work
// on TaskScheduler either (there can be TaskScheduler work remaining if
// DisallowRunTasks() preempted work and/or the last RunUntilIdle() posted
// more TaskScheduler tasks).
// Note: this last |if| couldn't be turned into a |do {} while();|. A
// conditional loop makes it such that |continue;| results in checking the
// condition (not unconditionally loop again) which would be incorrect for
// the above logic as it'd then be possible for a TaskScheduler task to be
// running during the DisallowRunTasks() test, causing it to fail, but then
// post to the main thread and complete before the loop's condition is
// verified which could result in HasIncompleteUndelayedTasksForTesting()
// returning false and the loop erroneously exiting with a pending task on
// the main thread.
if (!task_tracker_->HasIncompleteUndelayedTasksForTesting())
break;
}
// The above loop always ends with running tasks being disallowed. Re-enable
// parallel execution before returning unless in ExecutionMode::QUEUED.
if (execution_control_mode_ != ExecutionMode::QUEUED)
task_tracker_->AllowRunTasks();
}
void ScopedTaskEnvironment::FastForwardBy(TimeDelta delta) {
DCHECK(mock_time_task_runner_);
mock_time_task_runner_->FastForwardBy(delta);
}
void ScopedTaskEnvironment::FastForwardUntilNoTasksRemain() {
DCHECK(mock_time_task_runner_);
mock_time_task_runner_->FastForwardUntilNoTasksRemain();
}
const TickClock* ScopedTaskEnvironment::GetMockTickClock() {
DCHECK(mock_time_task_runner_);
return mock_time_task_runner_->GetMockTickClock();
}
std::unique_ptr<TickClock> ScopedTaskEnvironment::DeprecatedGetMockTickClock() {
DCHECK(mock_time_task_runner_);
return mock_time_task_runner_->DeprecatedGetMockTickClock();
}
size_t ScopedTaskEnvironment::GetPendingMainThreadTaskCount() const {
DCHECK(mock_time_task_runner_);
return mock_time_task_runner_->GetPendingTaskCount();
}
TimeDelta ScopedTaskEnvironment::NextMainThreadPendingTaskDelay() const {
DCHECK(mock_time_task_runner_);
return mock_time_task_runner_->NextPendingTaskDelay();
}
ScopedTaskEnvironment::TestTaskTracker::TestTaskTracker()
: internal::TaskSchedulerImpl::TaskTrackerImpl("ScopedTaskEnvironment"),
can_run_tasks_cv_(&lock_),
task_completed_(&lock_) {}
void ScopedTaskEnvironment::TestTaskTracker::AllowRunTasks() {
AutoLock auto_lock(lock_);
can_run_tasks_ = true;
can_run_tasks_cv_.Broadcast();
}
bool ScopedTaskEnvironment::TestTaskTracker::DisallowRunTasks() {
AutoLock auto_lock(lock_);
// Can't disallow run task if there are tasks running.
if (num_tasks_running_ > 0) {
// Attempt to wait a bit so that the caller doesn't busy-loop with the same
// set of pending work. A short wait is required to avoid deadlock
// scenarios. See DisallowRunTasks()'s declaration for more details.
task_completed_.TimedWait(TimeDelta::FromMilliseconds(1));
return false;
}
can_run_tasks_ = false;
return true;
}
void ScopedTaskEnvironment::TestTaskTracker::RunOrSkipTask(
internal::Task task,
internal::Sequence* sequence,
bool can_run_task) {
{
AutoLock auto_lock(lock_);
while (!can_run_tasks_)
can_run_tasks_cv_.Wait();
++num_tasks_running_;
}
internal::TaskSchedulerImpl::TaskTrackerImpl::RunOrSkipTask(
std::move(task), sequence, can_run_task);
{
AutoLock auto_lock(lock_);
CHECK_GT(num_tasks_running_, 0);
CHECK(can_run_tasks_);
--num_tasks_running_;
task_completed_.Broadcast();
}
}
} // namespace test
} // namespace base