| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/task/execution_fence.h" |
| |
| #include <map> |
| #include <optional> |
| #include <ostream> |
| #include <string> |
| |
| #include "base/barrier_closure.h" |
| #include "base/containers/enum_set.h" |
| #include "base/features.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/location.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/synchronization/lock.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/tracing/protos/chrome_track_event.pbzero.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace base { |
| |
| namespace { |
| |
| using ::testing::_; |
| |
| // Types of task to post while a fence is up. |
| enum class TaskType { |
| // ThreadPool task with default priority. |
| kThreadPoolDefault, |
| // ThreadPool task with best-effort priority. |
| kThreadPoolBestEffort, |
| // Task posted to a TaskQueue with default priority. |
| kTaskQueueDefault, |
| // Task posted to a TaskQueue with best-effort priority. |
| kTaskQueueBestEffort, |
| }; |
| using TaskTypeSet = EnumSet<TaskType, |
| TaskType::kThreadPoolDefault, |
| TaskType::kTaskQueueBestEffort>; |
| |
| std::ostream& operator<<(std::ostream& os, TaskType task_type) { |
| switch (task_type) { |
| case TaskType::kThreadPoolDefault: |
| return os << "kThreadPoolDefault"; |
| case TaskType::kThreadPoolBestEffort: |
| return os << "kThreadPoolBestEffort"; |
| case TaskType::kTaskQueueDefault: |
| return os << "kTaskQueueDefault"; |
| case TaskType::kTaskQueueBestEffort: |
| return os << "kTaskQueueBestEffort"; |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& os, TaskTypeSet task_types) { |
| std::string sep = ""; |
| os << "["; |
| for (TaskType task_type : task_types) { |
| os << sep << task_type; |
| sep = ","; |
| } |
| return os << "]"; |
| } |
| |
| } // namespace |
| |
| struct TestParams { |
| // Whether or not ScopedBestEffortExecutionFence should block TaskQueue tasks. |
| bool block_best_effort_task_queue = false; |
| |
| // All TaskQueue task types that should run while a |
| // ScopedBestEffortExecutionFence is up. |
| TaskTypeSet task_queue_types_during_best_effort_fence; |
| |
| // All TaskQueue task types that should run as soon as the last |
| // ScopedBestEffortExecutionFence comes down. |
| TaskTypeSet task_queue_types_after_best_effort_fence; |
| }; |
| |
| class ExecutionFenceTest : public ::testing::TestWithParam<TestParams> { |
| public: |
| ExecutionFenceTest() { |
| scoped_feature_list_.InitWithFeatureState( |
| features::kScopedBestEffortExecutionFenceForTaskQueue, |
| GetParam().block_best_effort_task_queue); |
| } |
| |
| ~ExecutionFenceTest() override { |
| // Flush every task runner being tested. |
| RepeatingClosure barrier_closure = |
| BarrierClosure(TaskTypeSet::All().size(), task_env_.QuitClosure()); |
| for (TaskType task_type : TaskTypeSet::All()) { |
| task_runners_.at(task_type)->PostTask(FROM_HERE, barrier_closure); |
| } |
| task_env_.RunUntilQuit(); |
| } |
| |
| // Wait for all tasks to get a chance to run. |
| void TinyWait() { |
| task_env_.GetMainThreadTaskRunner()->PostDelayedTask( |
| FROM_HERE, task_env_.QuitClosure(), TestTimeouts::tiny_timeout()); |
| task_env_.RunUntilQuit(); |
| } |
| |
| // Wait for all posted tasks to get a chance to run, and then expect that one |
| // each of `expected_tasks` ran since the last call. |
| void RunPostedTasksAndExpect(TaskTypeSet expected_tasks, |
| const Location& location = Location::Current()) { |
| SCOPED_TRACE(location.ToString()); |
| |
| // Wait for all expected tasks to run. If a task is blocked incorrectly, the |
| // test will time out. |
| { |
| AutoLock lock(tasks_that_ran_lock_); |
| while (!tasks_that_ran_.HasAll(expected_tasks)) { |
| AutoUnlock unlock(tasks_that_ran_lock_); |
| TinyWait(); |
| } |
| } |
| |
| // If we expect any blocked tasks, wait a bit to make sure they don't run. |
| // There's a chance that a task won't be scheduled until after TinyWait(), |
| // but it's small. Since this is testing for tasks that run when they're not |
| // supposed to, missing the timeout would be a false negative. So a flaky |
| // test should be considered a failure - it usually fails (correctly |
| // detecting an error) but occasionally succeeds (incorrectly). |
| if (expected_tasks != TaskTypeSet::All()) { |
| TinyWait(); |
| } |
| |
| // Whew. Now make sure the exact expected task types ran. |
| AutoLock lock(tasks_that_ran_lock_); |
| EXPECT_EQ(tasks_that_ran_, expected_tasks); |
| tasks_that_ran_.Clear(); |
| } |
| |
| // Post a task of each type. |
| void PostTestTasks() { |
| { |
| AutoLock lock(tasks_that_ran_lock_); |
| ASSERT_TRUE(tasks_that_ran_.empty()); |
| } |
| for (TaskType task_type : TaskTypeSet::All()) { |
| task_runners_.at(task_type)->PostTask( |
| FROM_HERE, BindLambdaForTesting([this, task_type] { |
| AutoLock lock(tasks_that_ran_lock_); |
| tasks_that_ran_.Put(task_type); |
| })); |
| } |
| } |
| |
| protected: |
| test::ScopedFeatureList scoped_feature_list_; |
| test::TaskEnvironmentWithMainThreadPriorities task_env_{ |
| test::TaskEnvironment::ScopedExecutionFenceBehaviour:: |
| MAIN_THREAD_AND_THREAD_POOL}; |
| |
| // A TaskRunner for each TaskType. |
| std::map<TaskType, scoped_refptr<SequencedTaskRunner>> task_runners_{ |
| {TaskType::kThreadPoolDefault, ThreadPool::CreateSequencedTaskRunner({})}, |
| {TaskType::kThreadPoolBestEffort, |
| ThreadPool::CreateSequencedTaskRunner({TaskPriority::BEST_EFFORT})}, |
| {TaskType::kTaskQueueDefault, task_env_.GetMainThreadTaskRunner()}, |
| {TaskType::kTaskQueueBestEffort, |
| task_env_.GetMainThreadTaskRunnerWithPriority( |
| TaskPriority::BEST_EFFORT)}, |
| }; |
| |
| // Lock protecting `tasks_that_ran_`. This doesn't need to be held while |
| // bringing down a fence since the test waits for all tasks to finish running |
| // before modifying the fence on the main thread, so there's no chance for |
| // tasks on other threads to run before taking the lock. (Unless there's a |
| // flake as described in RunPostedTasksAndExpect(), but then taking the lock |
| // would hide the flake - a task might incorrectly start running with the |
| // fence still up, but context switch before taking the lock, and then block |
| // while the main thread takes the lock and checks `tasks_that_ran_`, making |
| // it appear to run after the fence goes down.) |
| Lock tasks_that_ran_lock_; |
| |
| // Each type of tasks that executes between calls to |
| // RunPostedTasksAndExpect(). This is updated from multiple sequences and read |
| // from the main thread. |
| TaskTypeSet tasks_that_ran_ GUARDED_BY(tasks_that_ran_lock_); |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| ExecutionFenceTest, |
| ::testing::Values( |
| TestParams{ |
| .block_best_effort_task_queue = false, |
| .task_queue_types_during_best_effort_fence = |
| {TaskType::kTaskQueueDefault, |
| TaskType::kTaskQueueBestEffort}, |
| .task_queue_types_after_best_effort_fence = {}, |
| }, |
| TestParams{ |
| .block_best_effort_task_queue = true, |
| .task_queue_types_during_best_effort_fence = |
| {TaskType::kTaskQueueDefault}, |
| .task_queue_types_after_best_effort_fence = |
| {TaskType::kTaskQueueBestEffort}, |
| })); |
| |
| TEST_P(ExecutionFenceTest, BestEffortFence) { |
| { |
| ScopedBestEffortExecutionFence best_effort_fence; |
| |
| // While this fence is up, only default-priority tasks should run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect( |
| Union({TaskType::kThreadPoolDefault}, |
| GetParam().task_queue_types_during_best_effort_fence)); |
| } |
| |
| // After bringing the fence down, unblocked best-effort tasks should run. |
| RunPostedTasksAndExpect( |
| Union({TaskType::kThreadPoolBestEffort}, |
| GetParam().task_queue_types_after_best_effort_fence)); |
| |
| // Now that the fence is down all tasks should run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect(TaskTypeSet::All()); |
| } |
| |
| TEST_P(ExecutionFenceTest, ThreadPoolFence) { |
| { |
| ScopedThreadPoolExecutionFence thread_pool_fence; |
| |
| // While this fence is up, only TaskQueue tasks should run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect( |
| {TaskType::kTaskQueueDefault, TaskType::kTaskQueueBestEffort}); |
| } |
| |
| // After bringing the fence down, unblocked ThreadPool tasks should run. |
| RunPostedTasksAndExpect( |
| {TaskType::kThreadPoolDefault, TaskType::kThreadPoolBestEffort}); |
| |
| // No more fences. All posted tasks run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect(TaskTypeSet::All()); |
| } |
| |
| TEST_P(ExecutionFenceTest, NestedFences) { |
| auto best_effort_fence1 = |
| std::make_optional<ScopedBestEffortExecutionFence>(); |
| auto best_effort_fence2 = |
| std::make_optional<ScopedBestEffortExecutionFence>(); |
| |
| // While these fences are up, only default-priority tasks should run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect( |
| Union({TaskType::kThreadPoolDefault}, |
| GetParam().task_queue_types_during_best_effort_fence)); |
| |
| auto thread_pool_fence1 = |
| std::make_optional<ScopedThreadPoolExecutionFence>(); |
| auto thread_pool_fence2 = |
| std::make_optional<ScopedThreadPoolExecutionFence>(); |
| |
| // Now both types of fence are up, so only TaskQueue tasks should run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect(GetParam().task_queue_types_during_best_effort_fence); |
| |
| thread_pool_fence2.reset(); |
| |
| // Still a fence up, so nothing should be unblocked. |
| RunPostedTasksAndExpect({}); |
| |
| // New ThreadPool tasks still shouldn't run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect(GetParam().task_queue_types_during_best_effort_fence); |
| |
| thread_pool_fence1.reset(); |
| |
| // After bringing the last ThreadPool fence down, unblocked ThreadPool |
| // tasks should run. |
| RunPostedTasksAndExpect({TaskType::kThreadPoolDefault}); |
| |
| // But new best-effort tasks shouldn't. |
| PostTestTasks(); |
| RunPostedTasksAndExpect( |
| Union({TaskType::kThreadPoolDefault}, |
| GetParam().task_queue_types_during_best_effort_fence)); |
| |
| best_effort_fence2.reset(); |
| |
| // Still a best-effort fence up, so nothing should be unblocked. |
| RunPostedTasksAndExpect({}); |
| |
| // New best-effort tasks still shouldn't run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect( |
| Union({TaskType::kThreadPoolDefault}, |
| GetParam().task_queue_types_during_best_effort_fence)); |
| |
| best_effort_fence1.reset(); |
| |
| // After bringing the last fence down, unblocked best-effort tasks should |
| // run. |
| RunPostedTasksAndExpect( |
| Union({TaskType::kThreadPoolBestEffort}, |
| GetParam().task_queue_types_after_best_effort_fence)); |
| |
| // No more fences. All posted tasks run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect(TaskTypeSet::All()); |
| } |
| |
| TEST_P(ExecutionFenceTest, StaggeredFences) { |
| auto best_effort_fence1 = |
| std::make_optional<ScopedBestEffortExecutionFence>(); |
| |
| // Best-effort tasks don't run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect( |
| Union({TaskType::kThreadPoolDefault}, |
| GetParam().task_queue_types_during_best_effort_fence)); |
| |
| auto thread_pool_fence1 = |
| std::make_optional<ScopedThreadPoolExecutionFence>(); |
| |
| // Best-effort and ThreadPool tasks don't run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect(GetParam().task_queue_types_during_best_effort_fence); |
| |
| auto best_effort_fence2 = |
| std::make_optional<ScopedBestEffortExecutionFence>(); |
| auto thread_pool_fence2 = |
| std::make_optional<ScopedThreadPoolExecutionFence>(); |
| |
| // Best-effort and ThreadPool tasks still don't run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect(GetParam().task_queue_types_during_best_effort_fence); |
| |
| // Bring down the first best-effort fence. Another one's still up, so |
| // nothing's unblocked. |
| best_effort_fence1.reset(); |
| RunPostedTasksAndExpect({}); |
| |
| // Bring down the first ThreadPool fence. Another one's still up, so nothing's |
| // unblocked. |
| thread_pool_fence1.reset(); |
| RunPostedTasksAndExpect({}); |
| |
| // Bring down the second best-effort fence. Only best-effort TaskQueue tasks |
| // are unblocked. |
| best_effort_fence2.reset(); |
| RunPostedTasksAndExpect(GetParam().task_queue_types_after_best_effort_fence); |
| |
| // Bring down the second ThreadPool fence. All tasks are now unblocked. |
| thread_pool_fence2.reset(); |
| RunPostedTasksAndExpect( |
| {TaskType::kThreadPoolDefault, TaskType::kThreadPoolBestEffort}); |
| |
| // No more fences. All posted tasks run. |
| PostTestTasks(); |
| RunPostedTasksAndExpect(TaskTypeSet::All()); |
| } |
| |
| } // namespace base |