blob: 6de729ae4d27c493947223e74a6cd35535937d9c [file] [log] [blame]
// 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/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/platform_thread.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
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. No-ops and returns false if a task is running.
bool DisallowRunTasks();
private:
friend class ScopedTaskEnvironment;
// internal::TaskSchedulerImpl::TaskTrackerImpl:
void RunOrSkipTask(std::unique_ptr<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_;
// 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),
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();
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_;
}
void ScopedTaskEnvironment::RunUntilIdle() {
for (int i = 0;; ++i) {
LOG_IF(WARNING, i % 1000 == 999)
<< "ScopedTaskEnvironment::RunUntilIdle() appears to be stuck in an "
"infinite loop (e.g. A posts B posts C posts A?).";
task_tracker_->AllowRunTasks();
// Another pass is only ever required when there is work remaining in the
// TaskScheduler (see logic below), yield the main thread to avoid it
// entering a DisallowRunTasks()/AllowRunTasks() busy loop that merely
// confirms TaskScheduler has work to do while preventing its execution.
if (i > 0)
PlatformThread::YieldCurrentThread();
// 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();
// TODO(gab): The complex piece of logic below can be boiled down to "loop
// again if >0 incomplete tasks" when TaskTracker is made aware of main
// thread tasks. https://crbug.com/660078.
// Then halt TaskScheduler. DisallowRunTasks() failing indicates that there
// are TaskScheduler tasks currently running, yield to them and try again
// (restarting from top 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();|
// (regardless of the use of a for-loop for |i|). 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 GetNumIncompleteUndelayedTasksForTesting() returning 0
// and the loop erroneously exiting with a pending task on the main thread.
if (task_tracker_->GetNumIncompleteUndelayedTasksForTesting() == 0)
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();
}
ScopedTaskEnvironment::TestTaskTracker::TestTaskTracker()
: can_run_tasks_cv_(&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)
return false;
can_run_tasks_ = false;
return true;
}
void ScopedTaskEnvironment::TestTaskTracker::RunOrSkipTask(
std::unique_ptr<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_;
}
}
} // namespace test
} // namespace base