| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "gpu/command_buffer/service/task_graph.h" |
| |
| #include <map> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "gpu/command_buffer/service/sync_point_manager.h" |
| #include "gpu/config/gpu_finch_features.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace gpu { |
| |
| class TaskGraphTest : public testing::Test { |
| protected: |
| TaskGraphTest() |
| : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) { |
| scoped_feature_list_.InitAndEnableFeature( |
| features::kSyncPointGraphValidation); |
| // Initialize after the feature flag has set. |
| sync_point_manager_ = std::make_unique<SyncPointManager>(); |
| task_graph_ = std::make_unique<TaskGraph>(sync_point_manager_.get()); |
| |
| CHECK(task_graph_->graph_validation_enabled()); |
| } |
| |
| ~TaskGraphTest() override { |
| for (auto& info : sequence_info_) { |
| task_graph_->DestroySequence(info.second.sequence_id); |
| } |
| } |
| |
| void CreateSequence(int sequence_key, bool manual_validation = false) { |
| CommandBufferId command_buffer_id = |
| CommandBufferId::FromUnsafeValue(sequence_key); |
| |
| auto sequence = std::make_unique<TaskGraph::Sequence>( |
| task_graph_.get(), |
| manual_validation ? scoped_refptr<base::SingleThreadTaskRunner>() |
| : base::SingleThreadTaskRunner::GetCurrentDefault(), |
| kNamespaceId, command_buffer_id); |
| |
| sequence_info_.emplace( |
| sequence_key, SequenceInfo(sequence->sequence_id(), command_buffer_id)); |
| |
| task_graph_->AddSequence(std::move(sequence)); |
| } |
| |
| SequenceId GetSequenceId(int sequence_key) { |
| auto info_it = sequence_info_.find(sequence_key); |
| CHECK(info_it != sequence_info_.end()); |
| return info_it->second.sequence_id; |
| } |
| |
| void CreateSyncToken(int sequence_key, int release_sync) { |
| auto info_it = sequence_info_.find(sequence_key); |
| ASSERT_TRUE(info_it != sequence_info_.end()); |
| |
| uint64_t release = release_sync + 1; |
| sync_tokens_.emplace( |
| release_sync, |
| SyncToken(kNamespaceId, info_it->second.command_buffer_id, release)); |
| } |
| |
| TaskCallback GetTaskCallback() { |
| const int task_id = num_tasks_added_++; |
| |
| return base::BindLambdaForTesting( |
| [this, task_id](FenceSyncReleaseDelegate* release_delegate) { |
| if (release_delegate) { |
| release_delegate->Release(); |
| } |
| tasks_executed_.push_back(task_id); |
| }); |
| } |
| |
| void AddTask(int sequence_key, int wait_sync, int release_sync) { |
| AddTask(sequence_key, std::vector<int>{wait_sync}, release_sync); |
| } |
| |
| void AddTask(int sequence_key, |
| const std::vector<int>& wait_syncs, |
| int release_sync) { |
| auto info_it = sequence_info_.find(sequence_key); |
| ASSERT_TRUE(info_it != sequence_info_.end()); |
| |
| std::vector<SyncToken> waits; |
| for (int wait_sync : wait_syncs) { |
| if (wait_sync >= 0) { |
| waits.push_back(sync_tokens_[wait_sync]); |
| } |
| } |
| |
| SyncToken release; |
| if (release_sync >= 0) { |
| release = sync_tokens_[release_sync]; |
| } |
| |
| base::AutoLock auto_lock(task_graph_->lock()); |
| |
| task_graph_->GetSequence(info_it->second.sequence_id) |
| ->AddTask(GetTaskCallback(), std::move(waits), release, |
| /*report_callback=*/{}); |
| } |
| |
| void RunAllPendingTasks() { |
| size_t previous_tasks_executed; |
| base::AutoLock auto_lock(task_graph_->lock()); |
| do { |
| previous_tasks_executed = tasks_executed_.size(); |
| for (auto& info : sequence_info_) { |
| TaskGraph::Sequence* sequence = |
| task_graph_->GetSequence(info.second.sequence_id); |
| |
| while (sequence->IsFrontTaskUnblocked()) { |
| base::OnceClosure task_closure; |
| uint32_t order_num = sequence->BeginTask(&task_closure); |
| SyncToken release = sequence->current_task_release(); |
| |
| { |
| base::AutoUnlock auto_unlock(task_graph_->lock()); |
| sequence->order_data()->BeginProcessingOrderNumber(order_num); |
| std::move(task_closure).Run(); |
| |
| if (release.HasData()) { |
| task_graph_->sync_point_manager()->EnsureFenceSyncReleased( |
| release, ReleaseCause::kTaskCompletionRelease); |
| } |
| sequence->order_data()->FinishProcessingOrderNumber(order_num); |
| } |
| sequence->FinishTask(); |
| } |
| } |
| } while (previous_tasks_executed != tasks_executed_.size()); |
| } |
| |
| void RunValidation(int sequence_key) { |
| auto info_it = sequence_info_.find(sequence_key); |
| ASSERT_TRUE(info_it != sequence_info_.end()); |
| |
| TaskGraph::Sequence* sequence = nullptr; |
| { |
| base::AutoLock auto_lock(task_graph_->lock()); |
| sequence = task_graph_->GetSequence(info_it->second.sequence_id); |
| } |
| |
| task_graph_->ValidateSequenceTaskFenceDeps(sequence); |
| } |
| |
| struct SequenceInfo { |
| SequenceInfo(SequenceId sequence_id, CommandBufferId command_buffer_id) |
| : sequence_id(sequence_id), command_buffer_id(command_buffer_id) {} |
| |
| SequenceId sequence_id; |
| CommandBufferId command_buffer_id; |
| }; |
| |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| |
| std::vector<int> tasks_executed_; |
| |
| std::unique_ptr<SyncPointManager> sync_point_manager_; |
| |
| std::unique_ptr<TaskGraph> task_graph_; |
| |
| std::map<int, const SequenceInfo> sequence_info_; |
| |
| std::map<int, const SyncToken> sync_tokens_; |
| |
| private: |
| const CommandBufferNamespace kNamespaceId = CommandBufferNamespace::GPU_IO; |
| |
| int num_tasks_added_ = 0; |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(TaskGraphTest, DestroySequenceReleasesSyncPoints) { |
| // Test that when a sequence is destroyed, all wait fences that are supposed |
| // to be released by the destroyed sequence will be unblocked. No validation |
| // is required. |
| |
| CreateSequence(0); |
| CreateSequence(1); |
| |
| CreateSyncToken(1, 0); // declare sync_token 0 on seq 1 |
| |
| AddTask(0, 0, -1); // task 0: seq 0, wait 0, no release |
| |
| RunAllPendingTasks(); |
| |
| EXPECT_TRUE(tasks_executed_.empty()); |
| |
| task_graph_->DestroySequence(GetSequenceId(1)); |
| sequence_info_.erase(1); |
| |
| RunAllPendingTasks(); |
| |
| std::vector<int> expected_task_order{0}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| TEST_F(TaskGraphTest, ValidationWaitWithoutRelease) { |
| // Two tasks on the same sequence wait for unreleased fences. |
| CreateSequence(0); |
| CreateSequence(1); |
| CreateSequence(2); |
| |
| CreateSyncToken(1, 0); // declare sync_token 0 on seq 1 |
| CreateSyncToken(1, 1); // declare sync_token 1 on seq 1 |
| |
| CreateSyncToken(2, 2); // declare sync_token 2 on seq 2 |
| CreateSyncToken(2, 3); // declare sync_token 3 on seq 2 |
| |
| AddTask(0, {0, 3}, -1); // task 0: seq 0, wait {0,3}, no release |
| |
| // Submit a task close to the time when the validation timer will be fired. |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay - |
| TaskGraph::kMinValidationDelay + |
| base::Seconds(1)); |
| AddTask(0, {1, 2}, -1); // task 1: seq 0, wait {1,2}, no release |
| |
| // Cause the validation timer to fire. |
| task_environment_.FastForwardBy(TaskGraph::kMinValidationDelay); |
| RunAllPendingTasks(); |
| |
| // Only task 0 is supposed to be executed. |
| // Task 1 has unsatisfied waits, but it is too new to be validated. |
| std::vector<int> expected_task_order = {0}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| |
| // The validation timer should be fired again and resolve the invalid waits |
| // of task 1. |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay + |
| base::Seconds(1)); |
| RunAllPendingTasks(); |
| |
| expected_task_order = {0, 1}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| TEST_F(TaskGraphTest, ManuallyCallValidationWaitWithoutRelease) { |
| // Two tasks on the same sequence wait for unreleased fences. |
| // Also test histogram emission. |
| |
| base::HistogramTester histogram_tester; |
| |
| CreateSequence(0, /*manual_validation=*/true); |
| CreateSequence(1); |
| CreateSequence(2); |
| |
| CreateSyncToken(1, 0); // declare sync_token 0 on seq 1 |
| CreateSyncToken(1, 1); // declare sync_token 1 on seq 1 |
| |
| CreateSyncToken(2, 2); // declare sync_token 2 on seq 2 |
| CreateSyncToken(2, 3); // declare sync_token 3 on seq 2 |
| |
| AddTask(0, {0, 3}, -1); // task 0: seq 0, wait {0,3}, no release |
| |
| // Submit a task close to the time when the validation timer would fired if |
| // it were configured to run. |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay - |
| TaskGraph::kMinValidationDelay + |
| base::Seconds(1)); |
| AddTask(0, {1, 2}, -1); // task 1: seq 0, wait {1,2}, no release |
| |
| // Validation is configured to be triggered manually. Moving time forward |
| // shouldn't trigger validation. |
| task_environment_.FastForwardBy(TaskGraph::kMinValidationDelay); |
| RunAllPendingTasks(); |
| EXPECT_TRUE(tasks_executed_.empty()); |
| |
| RunValidation(0); |
| RunAllPendingTasks(); |
| |
| // Only task 0 is supposed to be executed. |
| // Task 1 has unsatisfied waits, but it is too new to be validated. |
| std::vector<int> expected_task_order = {0}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay + |
| base::Seconds(1)); |
| RunAllPendingTasks(); |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| |
| RunValidation(0); |
| RunAllPendingTasks(); |
| |
| expected_task_order = {0, 1}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| |
| histogram_tester.ExpectTotalCount("GPU.GraphValidation.NeedsForceRelease", 2); |
| histogram_tester.ExpectTotalCount("GPU.GraphValidation.Duration", 2); |
| } |
| |
| TEST_F(TaskGraphTest, ValidationWaitWithoutRelease2) { |
| // Task 0 waits for task 1 on a different sequence. Task 1 waits for an |
| // unreleased fence. |
| |
| CreateSequence(0); |
| CreateSequence(1); |
| CreateSequence(2); |
| |
| CreateSyncToken(1, 0); // declare sync_token 0 on seq 1 |
| CreateSyncToken(2, 1); // declare sync_token 1 on seq 2 |
| |
| AddTask(0, 0, -1); // task 0: seq 0, wait 0, no release |
| |
| // Submit task 1 on sequence 1 later. Validation on sequence 0 will be |
| // triggered first. |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay - |
| base::Seconds(1)); |
| |
| // Sync_token 1 that task 1 waits on is not released by anyone. |
| AddTask(1, 1, 0); // task 1: seq 1, wait 1, release 0 |
| |
| RunAllPendingTasks(); |
| EXPECT_TRUE(tasks_executed_.empty()); |
| |
| // Trigger validation on sequence 0. |
| task_environment_.FastForwardBy(base::Seconds(2)); |
| RunAllPendingTasks(); |
| |
| std::vector<int> expected_task_order{1, 0}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| TEST_F(TaskGraphTest, ValidationCircularWaits) { |
| // Task 0 and task 1 wait for each other. |
| // |
| // seq 0 seq 1 |
| // | | | | |
| // |(task 0)|<--->|(task 1)| |
| // | | | | |
| |
| CreateSequence(0); |
| CreateSequence(1); |
| |
| CreateSyncToken(1, 2); // declare sync_token 2 on seq 1 |
| CreateSyncToken(0, 3); // declare sync_token 3 on seq 0 |
| |
| AddTask(0, 2, 3); // task 0: seq 0, wait 2, release 3 |
| |
| // Submit task 1 on sequence 1 later. Validation on sequence 0 will be |
| // triggered first. |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay - |
| base::Seconds(1)); |
| |
| AddTask(1, 3, 2); // task 1: seq 1, wait 3, release 2 |
| |
| RunAllPendingTasks(); |
| EXPECT_TRUE(tasks_executed_.empty()); |
| |
| // Trigger validation on sequence 0. |
| task_environment_.FastForwardBy(base::Seconds(2)); |
| RunAllPendingTasks(); |
| |
| std::vector<int> expected_task_order{1, 0}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| TEST_F(TaskGraphTest, ValidationCircularWaits2) { |
| // Task 0 waits for task 1; while task 1 waits for task 2: |
| // |
| // seq 0 seq 1 |
| // | | | | |
| // |(task 0)|---->|(task 1)| |
| // | | /| | |
| // |(task 2)|<--/ | | |
| // | | | | |
| |
| CreateSequence(0); |
| CreateSequence(1); |
| |
| CreateSyncToken(1, 2); // declare sync_token 2 on seq 1 |
| CreateSyncToken(0, 3); // declare sync_token 3 on seq 0 |
| |
| AddTask(0, 2, -1); // task 0: seq 0, wait 2, no release |
| |
| // Submit task 1 on sequence 1 later. Validation on sequence 0 will be |
| // triggered first. |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay - |
| base::Seconds(1)); |
| |
| AddTask(1, 3, 2); // task 1: seq 1, wait 3, release 2 |
| AddTask(0, -1, 3); // task 2: seq 0, no wait, release 3 |
| |
| RunAllPendingTasks(); |
| EXPECT_TRUE(tasks_executed_.empty()); |
| |
| // Trigger validation on sequence 0. |
| task_environment_.FastForwardBy(base::Seconds(2)); |
| RunAllPendingTasks(); |
| |
| std::vector<int> expected_task_order{1, 0, 2}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| TEST_F(TaskGraphTest, ValidationCircularWaits3) { |
| // Task 0 waits for task 1 on the same sequence. |
| // |
| // seq 0 |
| // | | |
| // |(task 0)|---| |
| // | | | |
| // |(task 1)|<--| |
| // | | |
| |
| CreateSequence(0); |
| |
| CreateSyncToken(0, 0); // declare sync_token 0 on seq 0 |
| |
| AddTask(0, 0, -1); // task 0: seq 0, wait 0, no release |
| AddTask(0, -1, 0); // task 1: seq 0, no wait, release 0 |
| |
| // No need to trigger the validation. Because SyncPointManager::Wait() will |
| // refuse to add a wait for the same sequence, task 0 is not blocked with |
| // circular dependency. |
| RunAllPendingTasks(); |
| std::vector<int> expected_task_order{0, 1}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| TEST_F(TaskGraphTest, ValidationCircularWaits4) { |
| // A more complex graph with multiple cycles. |
| // |
| // seq 0 seq 1 seq 2 |
| // | | | | | | |
| // | | |(task 3)| | | |
| // | | | | | | |
| // |(task 0)|--2->|(task 4)|--1->|(task 7)|----| |
| // | | | | | | | |
| // |(task 1)|--5->|(task 5)|--4->|(task 8)|-| | |
| // | | | | | | |
| // |(task 2)|<-3--+--------+----------------| | |
| // | | | | | |
| // |(task 6)|<-0----------------| |
| // | | |
| |
| sync_point_manager_->set_suppress_fatal_log_for_testing(); |
| |
| CreateSequence(0); |
| CreateSequence(1); |
| CreateSequence(2); |
| |
| CreateSyncToken(0, 3); // declare sync_token 3 on seq 0 |
| |
| CreateSyncToken(1, 0); // declare sync_token 0 on seq 1 |
| CreateSyncToken(1, 2); // declare sync_token 2 on seq 1 |
| CreateSyncToken(1, 5); // declare sync_token 5 on seq 1 |
| |
| CreateSyncToken(2, 1); // declare sync_token 1 on seq 2 |
| CreateSyncToken(2, 4); // declare sync_token 4 on seq 2 |
| |
| AddTask(0, 2, -1); // task 0: seq 0, wait 2, no release |
| |
| // Submit task 1 on sequence 0 later, so that the first validation only covers |
| // task 0. |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay - |
| base::Seconds(2)); |
| |
| AddTask(0, 5, -1); // task 1: seq 0, wait 5, no release |
| AddTask(0, -1, 3); // task 2: seq 0, no wait, release 3 |
| |
| AddTask(1, -1, -1); // task 3: seq 1, no wait, no release |
| AddTask(1, 1, 2); // task 4: seq 1, wait 1, release 2 |
| AddTask(1, 4, 5); // task 5: seq 1, wait 4, release 5 |
| AddTask(1, -1, 0); // task 6: seq 1, no wait, release 0 |
| |
| // Submit tasks on sequence 2 later. The next validation will happen on |
| // sequence 1. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| AddTask(2, 0, 1); // task 7: seq 2, wait 0, release 1 |
| AddTask(2, 3, 4); // task 8: seq 2, wait 3, release 4 |
| |
| RunAllPendingTasks(); |
| std::vector<int> expected_task_order{3}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| |
| // Trigger validation on sequence 0 for task 0. |
| // It should forcefully release sync_token 0 to break circular dependencies. |
| task_environment_.FastForwardBy(base::Seconds(2)); |
| RunAllPendingTasks(); |
| |
| expected_task_order = {3, 7, 4, 0}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| |
| // Trigger another validation on sequence 1 for task 5 and 6. |
| // It should forcefully release sync_token 5 to break circular dependencies. |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay); |
| RunAllPendingTasks(); |
| |
| expected_task_order = {3, 7, 4, 0, 1, 2, 8, 5, 6}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| TEST_F(TaskGraphTest, ValidationPartiallyValidated) { |
| // Test validation when part of the task graph has been validated in previous |
| // validation rounds. |
| // |
| // seq 0 seq 1 |
| // | | | | |
| // |(task 0)|--4->|(task 1)| |
| // | | | | |
| // |(task 3)|<-5--|(task 2)| |
| // | | | | |
| // |
| // Task 0 and task 1 are validated by the first validation round triggered |
| // on seq 0; while task 2 and task 3 are validated by the second validation |
| // round triggered on seq 1. |
| |
| CreateSequence(0); |
| CreateSequence(1); |
| |
| CreateSyncToken(1, 4); // declare sync_token 4 on seq 1 |
| CreateSyncToken(0, 5); // declare sync_token 5 on seq 0 |
| |
| AddTask(0, 4, -1); // task 0: seq 0, wait 4, no release |
| |
| // Submit task 1 on sequence 1 later, so that the first validation is |
| // triggered on seq 0. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| AddTask(1, -1, 4); // task 1: seq 1, no wait, release 4 |
| |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| AddTask(1, 5, -1); // task 2: seq 1, wait 5, no release |
| |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay - |
| base::Seconds(3)); |
| |
| AddTask(0, -1, 5); // task 3: seq 0, no wait, release 5 |
| |
| // Trigger first validation round on seq 0. |
| task_environment_.FastForwardBy(base::Milliseconds(1500)); |
| |
| // Trigger second validation round on seq 1. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| RunAllPendingTasks(); |
| std::vector<int> expected_task_order{1, 0, 3, 2}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| TEST_F(TaskGraphTest, ValidationNonExistentReleaseSequence) { |
| // Test validation happens between |
| // (1) a sequence has been destroyed and |
| // (2) wait fences that are supposed to be released by the destroyed sequence |
| // haven't been cleaned up from other sequences. |
| |
| CreateSequence(0, /*manual_validation=*/true); |
| CreateSequence(1); |
| |
| CreateSyncToken(1, 0); // declare sync_token 0 on seq 1 |
| SyncToken sync_token = sync_tokens_[0]; |
| |
| // Add a callback waiting for `sync_token` before adding task 0. When |
| // sequence 1 is destroyed, this callback will be run before cleaning up |
| // wait fences of task 0. |
| sync_point_manager_->Wait( |
| sync_token, GetSequenceId(0), sync_point_manager_->GenerateOrderNumber(), |
| base::BindLambdaForTesting([this]() { RunValidation(0); })); |
| |
| AddTask(0, 0, -1); // task 0: seq 0, wait 0, no release |
| |
| task_environment_.FastForwardBy(TaskGraph::kMaxValidationDelay + |
| base::Seconds(1)); |
| |
| task_graph_->DestroySequence(GetSequenceId(1)); |
| sequence_info_.erase(1); |
| |
| RunAllPendingTasks(); |
| |
| std::vector<int> expected_task_order{0}; |
| EXPECT_THAT(tasks_executed_, testing::ElementsAreArray(expected_task_order)); |
| } |
| |
| } // namespace gpu |