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