| // 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 "components/viz/service/display/frame_interval_decider.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| #include <variant> |
| |
| #include "base/containers/flat_map.h" |
| #include "components/viz/common/quads/compositor_frame.h" |
| #include "components/viz/common/surfaces/frame_sink_id.h" |
| #include "components/viz/common/surfaces/surface_info.h" |
| #include "components/viz/service/surfaces/surface.h" |
| #include "components/viz/service/surfaces/surface_manager.h" |
| #include "components/viz/service/surfaces/surface_manager_delegate.h" |
| #include "components/viz/test/compositor_frame_helpers.h" |
| #include "components/viz/test/stub_surface_client.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace viz { |
| namespace { |
| |
| using ContinuousRangeSettings = FrameIntervalDecider::ContinuousRangeSettings; |
| using FixedIntervalSettings = FrameIntervalDecider::FixedIntervalSettings; |
| using FrameIntervalClass = FrameIntervalDecider::FrameIntervalClass; |
| using ResultInterval = FrameIntervalMatcher::ResultInterval; |
| using Result = FrameIntervalDecider::Result; |
| |
| constexpr base::TimeTicks kNow = base::TimeTicks() + base::Seconds(1234); |
| |
| void ExpectResult(Result result, FrameIntervalClass frame_interval_class) { |
| ASSERT_TRUE(std::holds_alternative<FrameIntervalClass>(result)); |
| EXPECT_EQ(frame_interval_class, std::get<FrameIntervalClass>(result)); |
| } |
| |
| void ExpectResult(Result result, base::TimeDelta interval) { |
| ASSERT_TRUE(std::holds_alternative<ResultInterval>(result)); |
| EXPECT_EQ(interval, std::get<ResultInterval>(result).interval); |
| } |
| |
| class TestFrameIntervalMatcher : public FrameIntervalMatcher { |
| public: |
| explicit TestFrameIntervalMatcher(FrameIntervalMatcherType type) |
| : FrameIntervalMatcher(type) {} |
| |
| std::optional<Result> Match(const Inputs& matcher_inputs) override { |
| last_matcher_inputs_.emplace(matcher_inputs); |
| return result_; |
| } |
| |
| bool has_last_matcher_inputs() const { |
| return last_matcher_inputs_.has_value(); |
| } |
| |
| Inputs TakeLastMatcherInputs() { |
| CHECK(last_matcher_inputs_); |
| Inputs matcher_inputs = last_matcher_inputs_.value(); |
| last_matcher_inputs_.reset(); |
| return matcher_inputs; |
| } |
| |
| void SetResult(std::optional<Result> result) { result_ = result; } |
| |
| base::WeakPtr<TestFrameIntervalMatcher> GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| private: |
| std::optional<Inputs> last_matcher_inputs_; |
| std::optional<Result> result_; |
| |
| base::WeakPtrFactory<TestFrameIntervalMatcher> weak_ptr_factory_{this}; |
| }; |
| |
| class FrameIntervalDeciderTest : public testing::Test, |
| public SurfaceManagerDelegate { |
| public: |
| FrameIntervalDeciderTest() : frame_(MakeDefaultCompositorFrame()) {} |
| ~FrameIntervalDeciderTest() override = default; |
| |
| void SetUp() override { |
| surface_manager_ = std::make_unique<SurfaceManager>( |
| this, /*activation_deadline_in_frames=*/std::nullopt, |
| /*max_uncommitted_frames=*/0); |
| decider_ = std::make_unique<FrameIntervalDecider>(); |
| } |
| |
| void TearDown() override { |
| decider_.reset(); |
| surface_manager_.reset(); |
| } |
| |
| // SurfaceManagerDelegate implementation. |
| std::string_view GetFrameSinkDebugLabel( |
| const FrameSinkId& frame_sink_id) const override { |
| return std::string_view(); |
| } |
| void AggregatedFrameSinksChanged() override {} |
| void AddObserver(FrameSinkObserver* obs) override {} |
| void RemoveObserver(FrameSinkObserver* obs) override {} |
| bool HasViewTransitionToken( |
| const blink::ViewTransitionToken& transition_token) override { |
| return false; |
| } |
| |
| protected: |
| base::WeakPtr<SurfaceClient> surface_client() { |
| return surface_client_.weak_factory.GetWeakPtr(); |
| } |
| |
| Surface* CreateSurface(const FrameSinkId& frame_sink_id, |
| FrameIntervalInputs frame_interval_inputs) { |
| LocalSurfaceId local_surface_id = |
| LocalSurfaceId(1u, base::UnguessableToken::Create()); |
| SurfaceId surface_id(frame_sink_id, local_surface_id); |
| SurfaceInfo surface_info(surface_id, frame_.device_scale_factor(), |
| frame_.size_in_pixels()); |
| Surface* surface = |
| surface_manager_ |
| ->CreateSurface(surface_client(), surface_info, SurfaceId()) |
| .value_or(nullptr); |
| |
| UpdateFrame(surface, std::move(frame_interval_inputs)); |
| |
| return surface; |
| } |
| |
| void UpdateFrame(Surface* surface, |
| FrameIntervalInputs frame_interval_inputs) { |
| uint32_t frame_index = surface->GetActiveFrameIndex() + 1u; |
| auto frame = MakeDefaultCompositorFrame(); |
| frame.metadata.frame_interval_inputs = std::move(frame_interval_inputs); |
| ASSERT_TRUE(surface->QueueFrame(std::move(frame), frame_index, |
| base::ScopedClosureRunner())); |
| surface->ActivatePendingFrameForDeadline(); |
| ASSERT_EQ(surface->GetActiveFrameIndex(), frame_index); |
| } |
| |
| void DrawSurfaces(std::vector<Surface*> surfaces, |
| base::TimeTicks frame_time) { |
| FrameIntervalDecider::ScopedAggregate scoped_aggregate( |
| decider_->WrapAggregate(*surface_manager_, frame_time)); |
| for (auto* surface : surfaces) { |
| static_cast<SurfaceObserver*>(&scoped_aggregate) |
| ->OnSurfaceWillBeDrawn(surface); |
| } |
| } |
| |
| void InitializeDecider() { |
| frame_.metadata.frame_interval_inputs.frame_time = kNow; |
| |
| std::vector<std::unique_ptr<FrameIntervalMatcher>> matchers; |
| { |
| auto matcher = std::make_unique<TestFrameIntervalMatcher>( |
| FrameIntervalMatcherType::kInputBoost); |
| matchers_.push_back(matcher->GetWeakPtr()); |
| matchers.push_back(std::move(matcher)); |
| } |
| { |
| auto matcher = std::make_unique<TestFrameIntervalMatcher>( |
| FrameIntervalMatcherType::kOnlyVideo); |
| matchers_.push_back(matcher->GetWeakPtr()); |
| matchers.push_back(std::move(matcher)); |
| } |
| settings_.result_callback = base::BindRepeating( |
| &FrameIntervalDeciderTest::SetFrameInterval, base::Unretained(this)); |
| decider_->UpdateSettings(settings_, std::move(matchers)); |
| } |
| |
| void SetFrameInterval(Result result, FrameIntervalMatcherType matcher_type) { |
| result_ = result; |
| matcher_type_ = matcher_type; |
| } |
| |
| bool has_result() const { return result_.has_value(); } |
| bool has_matcher_type() const { return matcher_type_.has_value(); } |
| |
| Result TakeLastResult() { |
| CHECK(result_); |
| Result result = result_.value(); |
| result_.reset(); |
| return result; |
| } |
| |
| FrameIntervalMatcherType TakeLastMatcherType() { |
| CHECK(matcher_type_); |
| FrameIntervalMatcherType matcher_type = matcher_type_.value(); |
| matcher_type_.reset(); |
| return matcher_type; |
| } |
| |
| CompositorFrame frame_; |
| StubSurfaceClient surface_client_; |
| |
| FrameIntervalDecider::Settings settings_; |
| // Matchers are probably owned by `decider_` so using WeakPtrs to catch |
| // lifetime issues. |
| std::vector<base::WeakPtr<TestFrameIntervalMatcher>> matchers_; |
| |
| std::unique_ptr<SurfaceManager> surface_manager_; |
| std::unique_ptr<FrameIntervalDecider> decider_; |
| |
| // Set by `result_callback`. |
| std::optional<Result> result_; |
| std::optional<FrameIntervalMatcherType> matcher_type_; |
| }; |
| |
| TEST_F(FrameIntervalDeciderTest, Basics) { |
| settings_.increase_frame_interval_timeout = base::Milliseconds(50); |
| InitializeDecider(); |
| |
| matchers_[0]->SetResult(FrameIntervalClass::kBoost); |
| FrameIntervalInputs inputs; |
| inputs.frame_time = kNow; |
| Surface* surface1 = CreateSurface(FrameSinkId(0, 1), inputs); |
| Surface* surface2 = CreateSurface(FrameSinkId(0, 2), inputs); |
| DrawSurfaces({surface1, surface2}, kNow); |
| |
| ExpectResult(TakeLastResult(), FrameIntervalClass::kBoost); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| FrameIntervalMatcher::Inputs inputs_from_matcher = |
| matchers_[0]->TakeLastMatcherInputs(); |
| EXPECT_EQ(kNow, inputs_from_matcher.aggregated_frame_time); |
| EXPECT_EQ(settings_.increase_frame_interval_timeout, |
| inputs_from_matcher.settings->increase_frame_interval_timeout); |
| EXPECT_EQ(2u, inputs_from_matcher.inputs_map.size()); |
| for (Surface* surface : std::vector<Surface*>{surface1, surface2}) { |
| auto itr = inputs_from_matcher.inputs_map.find( |
| surface->surface_id().frame_sink_id()); |
| ASSERT_NE(itr, inputs_from_matcher.inputs_map.end()); |
| |
| EXPECT_EQ(surface->surface_id().frame_sink_id(), itr->first); |
| EXPECT_EQ(kNow, itr->second.frame_time); |
| } |
| EXPECT_EQ(kNow, frame_.metadata.frame_interval_inputs.frame_time); |
| |
| EXPECT_FALSE(matchers_[1]->has_last_matcher_inputs()); |
| } |
| |
| TEST_F(FrameIntervalDeciderTest, NoMatch) { |
| InitializeDecider(); |
| FrameIntervalInputs inputs; |
| inputs.frame_time = kNow; |
| Surface* surface = CreateSurface(FrameSinkId(0, 1), inputs); |
| DrawSurfaces({surface}, kNow); |
| |
| // Check that matchers are consulted |
| for (auto matcher : matchers_) { |
| EXPECT_TRUE(matcher->has_last_matcher_inputs()); |
| } |
| |
| // Expect return kDefault if nothing matched. |
| ExpectResult(TakeLastResult(), FrameIntervalClass::kDefault); |
| } |
| |
| TEST_F(FrameIntervalDeciderTest, NoMatchFixedIntervals) { |
| FixedIntervalSettings fixed_interval_settings; |
| fixed_interval_settings.supported_intervals.insert(base::Milliseconds(8)); |
| fixed_interval_settings.supported_intervals.insert(base::Milliseconds(16)); |
| fixed_interval_settings.default_interval = base::Milliseconds(0); |
| settings_.interval_settings = fixed_interval_settings; |
| |
| InitializeDecider(); |
| FrameIntervalInputs inputs; |
| inputs.frame_time = kNow; |
| Surface* surface = CreateSurface(FrameSinkId(0, 1), inputs); |
| DrawSurfaces({surface}, kNow); |
| |
| EXPECT_TRUE(matchers_[1]->has_last_matcher_inputs()); |
| ExpectResult(TakeLastResult(), base::Milliseconds(0)); |
| } |
| |
| TEST_F(FrameIntervalDeciderTest, NoMatchContinuousRange) { |
| ContinuousRangeSettings continuous_range_settings; |
| continuous_range_settings.min_interval = base::Milliseconds(8); |
| continuous_range_settings.max_interval = base::Milliseconds(16); |
| continuous_range_settings.default_interval = base::Milliseconds(0); |
| settings_.interval_settings = continuous_range_settings; |
| |
| InitializeDecider(); |
| FrameIntervalInputs inputs; |
| inputs.frame_time = kNow; |
| Surface* surface = CreateSurface(FrameSinkId(0, 1), inputs); |
| DrawSurfaces({surface}, kNow); |
| |
| EXPECT_TRUE(matchers_[1]->has_last_matcher_inputs()); |
| ExpectResult(TakeLastResult(), base::Milliseconds(0)); |
| } |
| |
| TEST_F(FrameIntervalDeciderTest, FirstMatch) { |
| InitializeDecider(); |
| |
| matchers_[1]->SetResult(ResultInterval(base::Milliseconds(32))); |
| |
| FrameIntervalInputs inputs; |
| inputs.frame_time = kNow; |
| DrawSurfaces({CreateSurface(FrameSinkId(0, 1), inputs)}, kNow); |
| |
| ExpectResult(TakeLastResult(), base::Milliseconds(32)); |
| EXPECT_EQ(FrameIntervalMatcherType::kOnlyVideo, TakeLastMatcherType()); |
| EXPECT_TRUE(matchers_[0]->has_last_matcher_inputs()); |
| EXPECT_TRUE(matchers_[1]->has_last_matcher_inputs()); |
| } |
| |
| TEST_F(FrameIntervalDeciderTest, NoChange) { |
| InitializeDecider(); |
| |
| matchers_[0]->SetResult(ResultInterval(base::Milliseconds(32))); |
| |
| FrameIntervalInputs inputs; |
| inputs.frame_time = kNow; |
| Surface* surface = CreateSurface(FrameSinkId(0, 1), inputs); |
| DrawSurfaces({surface}, kNow); |
| |
| ExpectResult(TakeLastResult(), base::Milliseconds(32)); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| |
| base::TimeTicks now2 = kNow + base::Milliseconds(16); |
| inputs.frame_time = now2; |
| UpdateFrame(surface, inputs); |
| DrawSurfaces({surface}, now2); |
| |
| EXPECT_FALSE(has_result()); |
| EXPECT_FALSE(has_matcher_type()); |
| } |
| |
| TEST_F(FrameIntervalDeciderTest, IncreaseIntervalDelayFrameInterval) { |
| settings_.increase_frame_interval_timeout = base::Milliseconds(60); |
| InitializeDecider(); |
| |
| base::TimeTicks now = kNow; |
| matchers_[0]->SetResult(ResultInterval(base::Milliseconds(32))); |
| |
| FrameIntervalInputs inputs; |
| inputs.frame_time = now; |
| Surface* surface = CreateSurface(FrameSinkId(0, 1), inputs); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), base::Milliseconds(32)); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| |
| now = kNow + base::Milliseconds(16); |
| matchers_[0]->SetResult(ResultInterval(base::Milliseconds(16))); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), base::Milliseconds(16)); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| |
| now = kNow + base::Milliseconds(32); |
| matchers_[0]->SetResult(ResultInterval(base::Milliseconds(32))); |
| DrawSurfaces({surface}, now); |
| EXPECT_FALSE(has_result()); |
| EXPECT_FALSE(has_matcher_type()); |
| |
| now = kNow + base::Milliseconds(96); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), base::Milliseconds(32)); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| } |
| |
| TEST_F(FrameIntervalDeciderTest, IncreaseIntervalDelayIntervalClass) { |
| settings_.increase_frame_interval_timeout = base::Milliseconds(60); |
| InitializeDecider(); |
| |
| base::TimeTicks now = kNow; |
| matchers_[0]->SetResult(FrameIntervalClass::kDefault); |
| |
| FrameIntervalInputs inputs; |
| inputs.frame_time = now; |
| Surface* surface = CreateSurface(FrameSinkId(0, 1), inputs); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), FrameIntervalClass::kDefault); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| |
| now = kNow + base::Milliseconds(16); |
| matchers_[0]->SetResult(FrameIntervalClass::kBoost); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), FrameIntervalClass::kBoost); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| |
| now = kNow + base::Milliseconds(32); |
| matchers_[0]->SetResult(FrameIntervalClass::kDefault); |
| DrawSurfaces({surface}, now); |
| EXPECT_FALSE(has_result()); |
| EXPECT_FALSE(has_matcher_type()); |
| |
| now = kNow + base::Milliseconds(96); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), FrameIntervalClass::kDefault); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| } |
| |
| TEST_F(FrameIntervalDeciderTest, IncreaseIntervalDelayVariantSwitch) { |
| settings_.increase_frame_interval_timeout = base::Milliseconds(60); |
| InitializeDecider(); |
| |
| base::TimeTicks now = kNow; |
| matchers_[0]->SetResult(FrameIntervalClass::kBoost); |
| |
| FrameIntervalInputs inputs; |
| inputs.frame_time = now; |
| Surface* surface = CreateSurface(FrameSinkId(0, 1), inputs); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), FrameIntervalClass::kBoost); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| |
| now = kNow + base::Milliseconds(16); |
| matchers_[0]->SetResult(ResultInterval(base::Milliseconds(16))); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), base::Milliseconds(16)); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| |
| now = kNow + base::Milliseconds(32); |
| matchers_[0]->SetResult(FrameIntervalClass::kDefault); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), FrameIntervalClass::kDefault); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| |
| now = kNow + base::Milliseconds(48); |
| matchers_[0]->SetResult(ResultInterval(base::Milliseconds(32))); |
| DrawSurfaces({surface}, now); |
| ExpectResult(TakeLastResult(), base::Milliseconds(32)); |
| EXPECT_EQ(FrameIntervalMatcherType::kInputBoost, TakeLastMatcherType()); |
| } |
| |
| } // namespace |
| } // namespace viz |