| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/performance_manager/policies/heuristic_memory_saver_policy.h" |
| |
| #include <memory> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/performance_manager/policies/page_discarding_helper.h" |
| #include "chrome/browser/performance_manager/test_support/page_discarding_utils.h" |
| #include "components/performance_manager/public/features.h" |
| #include "components/performance_manager/test_support/graph_test_harness.h" |
| #include "content/public/common/page_type.h" |
| |
| namespace performance_manager::policies { |
| |
| using GraphTestHarnessWithMockDiscarder = |
| testing::GraphTestHarnessWithMockDiscarder; |
| using ::testing::Return; |
| |
| const uint64_t kDefaultAvailableMemoryValue = 60; |
| const uint64_t kDefaultTotalMemoryValue = 100; |
| |
| const uint64_t kBytesPerGb = 1024 * 1024 * 1024; |
| |
| const base::TimeDelta kDefaultHeartbeatInterval = base::Seconds(10); |
| const base::TimeDelta kLongHeartbeatInterval = base::Minutes(1); |
| const base::TimeDelta kDefaultMinimumTimeInBackground = base::Seconds(11); |
| |
| std::string FormatTimeDeltaParam(base::TimeDelta delta) { |
| return base::StrCat({base::NumberToString(delta.InSeconds()), "s"}); |
| } |
| |
| class MemoryMetricsMocker { |
| public: |
| uint64_t GetAvailableMemory() { |
| ++available_memory_sampled_count; |
| return available_memory_; |
| } |
| |
| uint64_t GetTotalMemory() { return total_memory_; } |
| |
| void SetAvailableMemory(uint64_t available_memory) { |
| available_memory_ = available_memory; |
| } |
| |
| void SetTotalMemory(uint64_t total_memory) { total_memory_ = total_memory; } |
| |
| int available_memory_sampled_count = 0; |
| |
| private: |
| uint64_t available_memory_ = 0; |
| uint64_t total_memory_ = 0; |
| }; |
| |
| class HeuristicMemorySaverPolicyTest |
| : public GraphTestHarnessWithMockDiscarder { |
| protected: |
| void SetUp() override { |
| GraphTestHarnessWithMockDiscarder::SetUp(); |
| |
| // This is usually called when the profile is created. Fake it here since it |
| // doesn't happen in tests. |
| PageDiscardingHelper::GetFromGraph(graph())->SetNoDiscardPatternsForProfile( |
| page_node()->browser_context_id(), {}); |
| } |
| |
| void TearDown() override { |
| graph()->TakeFromGraph(policy_); |
| GraphTestHarnessWithMockDiscarder::TearDown(); |
| } |
| |
| // Creates the policy by forwarding it the arguments and passing it to the |
| // graph, with a default set of functions for memory measurements that |
| // always return 60 and 100. |
| void CreatePolicy( |
| uint64_t pmf_threshold_percent = 100, |
| uint64_t pmf_threshold_mb = 100, |
| base::TimeDelta threshold_reached_heartbeat_interval = |
| kDefaultHeartbeatInterval, |
| base::TimeDelta threshold_not_reached_heartbeat_interval = |
| kDefaultHeartbeatInterval, |
| base::TimeDelta minimum_time_in_background = |
| kDefaultMinimumTimeInBackground, |
| HeuristicMemorySaverPolicy::AvailableMemoryCallback |
| available_memory_callback = base::BindRepeating([] { |
| return kDefaultAvailableMemoryValue; |
| }), |
| HeuristicMemorySaverPolicy::TotalMemoryCallback total_memory_callback = |
| base::BindRepeating([] { return kDefaultTotalMemoryValue; })) { |
| feature_list_.InitAndEnableFeatureWithParameters( |
| features::kHeuristicMemorySaver, |
| { |
| {"threshold_percent", base::NumberToString(pmf_threshold_percent)}, |
| {"threshold_mb", base::NumberToString(pmf_threshold_mb)}, |
| {"threshold_reached_heartbeat_interval", |
| FormatTimeDeltaParam(threshold_reached_heartbeat_interval)}, |
| {"threshold_not_reached_heartbeat_interval", |
| FormatTimeDeltaParam(threshold_not_reached_heartbeat_interval)}, |
| {"minimum_time_in_background", |
| FormatTimeDeltaParam(minimum_time_in_background)}, |
| }); |
| |
| auto policy = std::make_unique<HeuristicMemorySaverPolicy>( |
| available_memory_callback, total_memory_callback); |
| policy_ = policy.get(); |
| graph()->PassToGraph(std::move(policy)); |
| } |
| |
| PageNodeImpl* CreateOtherPageNode() { |
| other_process_node_ = CreateNode<ProcessNodeImpl>(); |
| other_page_node_ = CreateNode<PageNodeImpl>(); |
| other_main_frame_node_ = CreateFrameNodeAutoId(other_process_node_.get(), |
| other_page_node_.get()); |
| other_main_frame_node_->SetIsCurrent(true); |
| testing::MakePageNodeDiscardable(other_page_node_.get(), task_env()); |
| |
| return other_page_node_.get(); |
| } |
| |
| HeuristicMemorySaverPolicy* policy() { return policy_; } |
| |
| private: |
| // Owned by the graph. |
| raw_ptr<HeuristicMemorySaverPolicy> policy_; |
| |
| TestNodeWrapper<PageNodeImpl> other_page_node_; |
| TestNodeWrapper<ProcessNodeImpl> other_process_node_; |
| TestNodeWrapper<FrameNodeImpl> other_main_frame_node_; |
| |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| TEST_F(HeuristicMemorySaverPolicyTest, NoDiscardIfPolicyInactive) { |
| CreatePolicy(); |
| policy()->SetActive(false); |
| |
| page_node()->SetType(PageType::kTab); |
| // Toggle visibility so that the page node updates its last visibility timing |
| // information. |
| page_node()->SetIsVisible(true); |
| page_node()->SetIsVisible(false); |
| |
| // Advance the time by at least `minimum_time_in_background` + |
| // `heartbeat_interval`. If a tab is to be discarded, it will be at this |
| // point. |
| task_env().FastForwardBy(kDefaultHeartbeatInterval + |
| kDefaultMinimumTimeInBackground); |
| // No discard. |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| } |
| |
| TEST_F(HeuristicMemorySaverPolicyTest, DiscardIfPolicyActive) { |
| CreatePolicy(); |
| policy()->SetActive(true); |
| |
| page_node()->SetType(PageType::kTab); |
| // Toggle visibility so that the page node updates its last visibility timing |
| // information. |
| page_node()->SetIsVisible(true); |
| page_node()->SetIsVisible(false); |
| |
| // Advance the time by at least `minimum_time_in_background` |
| task_env().FastForwardBy(kDefaultMinimumTimeInBackground); |
| // No discard yet. |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| |
| // Advance by at least the heartbeat interval, this should discard the |
| // now-eligible tab. |
| EXPECT_CALL(*discarder(), DiscardPageNodeImpl(page_node())) |
| .WillOnce(Return(true)); |
| task_env().FastForwardBy(kDefaultHeartbeatInterval); |
| |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| } |
| |
| TEST_F(HeuristicMemorySaverPolicyTest, NoDiscardIfAboveThreshold) { |
| CreatePolicy(/*pmf_threshold_percent=*/30); |
| policy()->SetActive(true); |
| |
| page_node()->SetType(PageType::kTab); |
| // Toggle visibility so that the page node updates its last visibility timing |
| // information. |
| page_node()->SetIsVisible(true); |
| page_node()->SetIsVisible(false); |
| |
| // Advance the time by at least `minimum_time_in_background` + |
| // `heartbeat_interval`. If a tab is to be discarded, it will be at this |
| // point. |
| task_env().FastForwardBy(kDefaultHeartbeatInterval + |
| kDefaultMinimumTimeInBackground); |
| // No discard. |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| } |
| |
| // Tests the case used as an example in the |
| // `kHeuristicMemorySaverAvailableMemoryThresholdPercent` comment: |
| // |
| // A device with 8Gb of installed RAM, 1Gb of which is available is under the |
| // threshold and will discard tabs (12.5% available and 1Gb < 2048Mb) |
| TEST_F(HeuristicMemorySaverPolicyTest, |
| DiscardIfPolicyActiveAndUnderBothThresholds) { |
| MemoryMetricsMocker mocker; |
| mocker.SetAvailableMemory(1 * kBytesPerGb); |
| mocker.SetTotalMemory(8 * kBytesPerGb); |
| |
| CreatePolicy( |
| /*pmf_threshold_percent=*/20, |
| /*pmf_threshold_mb=*/2048, |
| /*threshold_reached_heartbeat_interval=*/kDefaultHeartbeatInterval, |
| /*threshold_not_reached_heartbeat_interval=*/kDefaultHeartbeatInterval, |
| /*minimum_time_in_background=*/kDefaultMinimumTimeInBackground, |
| /*available_memory_callback=*/ |
| base::BindRepeating(&MemoryMetricsMocker::GetAvailableMemory, |
| base::Unretained(&mocker)), |
| /*total_memory_callback=*/ |
| base::BindRepeating(&MemoryMetricsMocker::GetTotalMemory, |
| base::Unretained(&mocker))); |
| |
| policy()->SetActive(true); |
| |
| page_node()->SetType(PageType::kTab); |
| // Toggle visibility so that the page node updates its last visibility timing |
| // information. |
| page_node()->SetIsVisible(true); |
| page_node()->SetIsVisible(false); |
| |
| // Advance the time by at least `minimum_time_in_background` |
| task_env().FastForwardBy(kDefaultMinimumTimeInBackground); |
| // No discard yet. |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| |
| // Advance by at least the heartbeat interval, this should discard the |
| // now-eligible tab. |
| EXPECT_CALL(*discarder(), DiscardPageNodeImpl(page_node())) |
| .WillOnce(Return(true)); |
| task_env().FastForwardBy(kDefaultHeartbeatInterval); |
| |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| } |
| |
| // Tests the case used as an example in the |
| // `kHeuristicMemorySaverAvailableMemoryThresholdPercent` comment: |
| // |
| // A device with 16Gb of installed RAM, 3Gb of which are available is under |
| // the percentage threshold but will not discard tabs because it's above the |
| // absolute Mb threshold (18.75% available, but 3Gb > 2048Mb) |
| TEST_F(HeuristicMemorySaverPolicyTest, |
| NoDiscardIfUnderPercentThresholdAboveMbThreshold) { |
| MemoryMetricsMocker mocker; |
| mocker.SetAvailableMemory(3 * kBytesPerGb); |
| mocker.SetTotalMemory(16 * kBytesPerGb); |
| |
| CreatePolicy( |
| /*pmf_threshold_percent=*/20, |
| /*pmf_threshold_mb=*/2048, |
| /*threshold_reached_heartbeat_interval=*/kDefaultHeartbeatInterval, |
| /*threshold_not_reached_heartbeat_interval=*/kDefaultHeartbeatInterval, |
| /*minimum_time_in_background=*/kDefaultMinimumTimeInBackground, |
| /*available_memory_callback=*/ |
| base::BindRepeating(&MemoryMetricsMocker::GetAvailableMemory, |
| base::Unretained(&mocker)), |
| /*total_memory_callback=*/ |
| base::BindRepeating(&MemoryMetricsMocker::GetTotalMemory, |
| base::Unretained(&mocker))); |
| |
| policy()->SetActive(true); |
| |
| page_node()->SetType(PageType::kTab); |
| // Toggle visibility so that the page node updates its last visibility timing |
| // information. |
| page_node()->SetIsVisible(true); |
| page_node()->SetIsVisible(false); |
| |
| // Advance the time by at least `minimum_time_in_background` + |
| // `heartbeat_interval`. If a tab is to be discarded, it will be at this |
| // point. |
| task_env().FastForwardBy(kDefaultHeartbeatInterval + |
| kDefaultMinimumTimeInBackground); |
| // No discard. |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| } |
| |
| TEST_F(HeuristicMemorySaverPolicyTest, DifferentThresholds) { |
| MemoryMetricsMocker mocker; |
| mocker.SetAvailableMemory(60); |
| mocker.SetTotalMemory(100); |
| |
| CreatePolicy( |
| /*pmf_threshold_percent=*/30, |
| /*pmf_threshold_mb=*/100, |
| /*threshold_reached_heartbeat_interval=*/kDefaultHeartbeatInterval, |
| /*threshold_not_reached_heartbeat_interval=*/kLongHeartbeatInterval, |
| /*minimum_time_in_background=*/kDefaultMinimumTimeInBackground, |
| /*available_memory_callback=*/ |
| base::BindRepeating(&MemoryMetricsMocker::GetAvailableMemory, |
| base::Unretained(&mocker)), |
| /*total_memory_callback=*/ |
| base::BindRepeating(&MemoryMetricsMocker::GetTotalMemory, |
| base::Unretained(&mocker))); |
| |
| policy()->SetActive(true); |
| |
| // Advance the time just enough to get the first heartbeat |
| task_env().FastForwardBy(kDefaultHeartbeatInterval); |
| // No discard yet. |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| // The memory was sampled once in the callback. |
| EXPECT_EQ(1, mocker.available_memory_sampled_count); |
| |
| // Simulate reaching the threshold so that the next heartbeat callback |
| // schedules the timer with the short interval. |
| mocker.SetAvailableMemory(10); |
| mocker.SetTotalMemory(100); |
| |
| // Advance the time by one short heartbeat again. Memory shouldn't be sampled |
| // a second time because the next heartbeat was scheduled for the long |
| // interval (since we weren't past the threshold). |
| task_env().FastForwardBy(kDefaultHeartbeatInterval); |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| EXPECT_EQ(1, mocker.available_memory_sampled_count); |
| |
| // Advance by the difference between the long and short heartbeats, so that we |
| // just reach the long one. This should trigger the timer's callback, sample |
| // memory and see that we're above the threshold, and discard a tab + schedule |
| // the next check using the short interval. |
| EXPECT_CALL(*discarder(), DiscardPageNodeImpl(page_node())) |
| .WillOnce(Return(true)); |
| task_env().FastForwardBy(kLongHeartbeatInterval - kDefaultHeartbeatInterval); |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| EXPECT_EQ(2, mocker.available_memory_sampled_count); |
| |
| // Simulate that the discard got us back under the threshold. |
| mocker.SetAvailableMemory(40); |
| mocker.SetTotalMemory(100); |
| |
| // After the short interval, the memory is sampled again (and seen under the |
| // threshold). No tab is discarded and the next heartbeat is scheduled using |
| // the long interval. |
| task_env().FastForwardBy(kDefaultHeartbeatInterval); |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| EXPECT_EQ(3, mocker.available_memory_sampled_count); |
| |
| // Verify that there was no sampling after the short interval but there was |
| // one after the long interval. |
| task_env().FastForwardBy(kDefaultHeartbeatInterval); |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| EXPECT_EQ(3, mocker.available_memory_sampled_count); |
| |
| task_env().FastForwardBy(kLongHeartbeatInterval - kDefaultHeartbeatInterval); |
| ::testing::Mock::VerifyAndClearExpectations(discarder()); |
| EXPECT_EQ(4, mocker.available_memory_sampled_count); |
| } |
| |
| } // namespace performance_manager::policies |