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