blob: 052ef2fb9968c525d09603939ce2b340cdff03d0 [file] [log] [blame]
// Copyright 2020 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/performance_manager/freezing/freezing_policy.h"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/byte_count.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "components/performance_manager/freezing/freezer.h"
#include "components/performance_manager/graph/graph_impl.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/public/decorators/page_live_state_decorator.h"
#include "components/performance_manager/public/features.h"
#include "components/performance_manager/public/freezing/cannot_freeze_reason.h"
#include "components/performance_manager/public/freezing/freezing.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/resource_attribution/origin_in_browsing_instance_context.h"
#include "components/performance_manager/public/resource_attribution/queries.h"
#include "components/performance_manager/public/resource_attribution/query_results.h"
#include "components/performance_manager/public/resource_attribution/resource_contexts.h"
#include "components/performance_manager/test_support/graph_test_harness.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/browser/browsing_instance_id.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace performance_manager {
namespace {
using FreezingType = FreezingPolicy::FreezingType;
using freezing::CannotFreezeReason;
using freezing::CannotFreezeReasonSet;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::Mock;
class MockFreezingPolicy : public FreezingPolicy {
public:
explicit MockFreezingPolicy(
std::unique_ptr<freezing::Discarder> discarder,
std::unique_ptr<freezing::OptOutChecker> opt_out_checker = nullptr)
: FreezingPolicy(std::move(discarder), std::move(opt_out_checker)) {}
~MockFreezingPolicy() override = default;
base::TimeTicks GenerateRandomPeriodicUnfreezePhase() const override {
//  Make the periodic unfreeze phase non-random for tests.
return base::TimeTicks();
}
MOCK_METHOD(void,
RecordFreezingEligibilityUKMForPage,
(ukm::SourceId source_id,
double highest_cpu_current_interval,
double highest_cpu_without_battery_saver_cannot_freeze,
CannotFreezeReasonSet cannot_freeze_reasons),
(override));
};
// Mock version of a Freezer.
class LenientMockFreezer : public Freezer {
public:
LenientMockFreezer() = default;
~LenientMockFreezer() override = default;
LenientMockFreezer(const LenientMockFreezer& other) = delete;
LenientMockFreezer& operator=(const LenientMockFreezer&) = delete;
MOCK_METHOD(void,
MaybeFreezePageNode,
(const PageNode* page_node),
(override));
MOCK_METHOD(void, UnfreezePageNode, (const PageNode* page_node), (override));
};
using MockFreezer = ::testing::StrictMock<LenientMockFreezer>;
class LenientMockDiscarder : public freezing::Discarder {
public:
LenientMockDiscarder() = default;
~LenientMockDiscarder() override = default;
LenientMockDiscarder(const LenientMockDiscarder& other) = delete;
LenientMockDiscarder& operator=(const LenientMockDiscarder&) = delete;
MOCK_METHOD(void,
DiscardPages,
(Graph * graph, std::vector<const PageNode*> page_nodes),
(override));
};
using MockDiscarder = ::testing::StrictMock<LenientMockDiscarder>;
} // namespace
class FreezingPolicyTest_BaseWithNoPage : public GraphTestHarness {
public:
FreezingPolicyTest_BaseWithNoPage()
: GraphTestHarness(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
~FreezingPolicyTest_BaseWithNoPage() override = default;
FreezingPolicyTest_BaseWithNoPage(
const FreezingPolicyTest_BaseWithNoPage& other) = delete;
FreezingPolicyTest_BaseWithNoPage& operator=(
const FreezingPolicyTest_BaseWithNoPage&) = delete;
void OnGraphCreated(GraphImpl* graph) override {
// The freezing logic relies on the existence of the page live state data.
graph->PassToGraph(std::make_unique<PageLiveStateDecorator>());
auto discarder = std::make_unique<MockDiscarder>();
discarder_ = discarder.get();
// Create the policy and pass it to the graph.
auto policy = std::make_unique<MockFreezingPolicy>(
std::move(discarder), CreateTestOptOutChecker());
policy_ = policy.get();
auto freezer = std::make_unique<MockFreezer>();
freezer_ = freezer.get();
policy_->SetFreezerForTesting(std::move(freezer));
graph->PassToGraph(std::move(policy));
process_node_ = CreateNode<ProcessNodeImpl>();
}
// Override to create an OptOutChecker for the test.
virtual std::unique_ptr<freezing::OptOutChecker> CreateTestOptOutChecker() {
return nullptr;
}
// Reports private memory footprint for `context` to the freezing policy, with
// "now" as the measurement time.
void ReportMemoryUsage(resource_attribution::ResourceContext context,
base::ByteCount private_footprint) {
resource_attribution::QueryResultMap memory_result_map;
memory_result_map[context] = resource_attribution::QueryResults{
.memory_summary_result = resource_attribution::MemorySummaryResult{
.metadata = resource_attribution::ResultMetadata(
/* measurement_time=*/base::TimeTicks::Now(),
resource_attribution::MeasurementAlgorithm::kSum),
.resident_set_size = base::ByteCount(0),
.private_footprint = private_footprint}};
resource_attribution::QueryResultObserver* observer = policy();
observer->OnResourceUsageUpdated(std::move(memory_result_map));
}
// Adds CPU usage for `context` to `cpu_result_map`, with "now" as the
// measurement time. `cumulative_background_cpu` is used as cumulative
// background CPU and `cumulative_cpu` is used as cumulative CPU
// (`cumulative_background_cpu` is used as cumulative CPU if `cumulative_cpu`
// is nullopt).
void AddCPUMeasurement(
resource_attribution::QueryResultMap& cpu_result_map,
resource_attribution::ResourceContext context,
base::TimeDelta cumulative_background_cpu,
std::optional<base::TimeDelta> cumulative_cpu = std::nullopt) {
cpu_result_map[context] = resource_attribution::QueryResults{
.cpu_time_result = resource_attribution::CPUTimeResult{
.metadata = resource_attribution::ResultMetadata(
/* measurement_time=*/base::TimeTicks::Now(),
resource_attribution::MeasurementAlgorithm::kSum),
.start_time = base::TimeTicks(),
.cumulative_cpu = cumulative_cpu.has_value()
? cumulative_cpu.value()
: cumulative_background_cpu,
.cumulative_background_cpu = cumulative_background_cpu}};
}
// Reports CPU usage for `context` to the the freezing policy, with "now" as
// the measurement time. `cumulative_background_cpu` is used as cumulative
// background CPU and `cumulative_cpu` is used as cumulative CPU
// (`cumulative_background_cpu` is used as cumulative CPU if `cumulative_cpu`
// is nullopt).
void ReportCumulativeCPUUsage(
resource_attribution::ResourceContext context,
base::TimeDelta cumulative_background_cpu,
std::optional<base::TimeDelta> cumulative_cpu = std::nullopt) {
resource_attribution::QueryResultMap cpu_result_map;
AddCPUMeasurement(cpu_result_map, context, cumulative_background_cpu,
cumulative_cpu);
resource_attribution::QueryResultObserver* observer = policy();
observer->OnResourceUsageUpdated(std::move(cpu_result_map));
}
std::pair<TestNodeWrapper<PageNodeImpl>, TestNodeWrapper<FrameNodeImpl>>
CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId browsing_instance_id,
const std::string& browsing_context_id = "") {
auto page =
CreateNode<PageNodeImpl>(/*web_contents=*/nullptr, browsing_context_id);
page->SetType(PageType::kTab);
auto frame = CreateFrameNodeAutoId(process_node(), page.get(),
/* parent_frame_node=*/nullptr,
browsing_instance_id);
return std::make_pair(std::move(page), std::move(frame));
}
void VerifyFreezerExpectations() {
Mock::VerifyAndClearExpectations(freezer());
}
void VerifyDiscarderExpectations() {
Mock::VerifyAndClearExpectations(discarder());
}
// Expect that the `CannotFreezeReason`s applicable to `freezing_type` for
// `page_node` match `matcher`.
template <typename MatcherType>
void ExpectCannotFreezeReasons(
const PageNode* page_node,
FreezingType freezing_type,
MatcherType matcher,
const base::Location& location = base::Location::Current()) {
EXPECT_THAT(
base::Intersection(
policy()->GetCanFreezeDetails(page_node).cannot_freeze_reasons,
FreezingPolicy::CannotFreezeReasonsForType(freezing_type)),
matcher)
<< location.ToString();
}
// Expect that the `CannotFreezeReason`s applicable to `freezing_type` for
// pages connected to `page_node` match `matcher`.
template <typename MatcherType>
void ExpectConnectedCannotFreezeReasons(
const PageNode* page_node,
FreezingType freezing_type,
MatcherType matcher,
const base::Location& location = base::Location::Current()) {
EXPECT_THAT(base::Intersection(
policy()
->GetCanFreezeDetails(page_node)
.cannot_freeze_reasons_connected_pages,
FreezingPolicy::CannotFreezeReasonsForType(freezing_type)),
matcher)
<< location.ToString();
}
ProcessNodeImpl* process_node() { return process_node_.get(); }
MockFreezingPolicy* policy() { return policy_; }
MockFreezer* freezer() { return freezer_; }
MockDiscarder* discarder() { return discarder_; }
const content::BrowsingInstanceId kBrowsingInstanceA =
content::BrowsingInstanceId(1);
const content::BrowsingInstanceId kBrowsingInstanceB =
content::BrowsingInstanceId(2);
const content::BrowsingInstanceId kBrowsingInstanceC =
content::BrowsingInstanceId(3);
const resource_attribution::OriginInBrowsingInstanceContext kContext{
url::Origin(), kBrowsingInstanceA};
private:
TestNodeWrapper<ProcessNodeImpl> process_node_;
raw_ptr<MockFreezer> freezer_;
raw_ptr<MockDiscarder> discarder_;
raw_ptr<MockFreezingPolicy> policy_;
};
class FreezingPolicyTest : public FreezingPolicyTest_BaseWithNoPage {
public:
FreezingPolicyTest() = default;
~FreezingPolicyTest() override = default;
void OnGraphCreated(GraphImpl* graph) override {
FreezingPolicyTest_BaseWithNoPage::OnGraphCreated(graph);
std::tie(page_node_, frame_node_) =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
}
PageNodeImpl* page_node() { return page_node_.get(); }
private:
TestNodeWrapper<PageNodeImpl> page_node_;
TestNodeWrapper<FrameNodeImpl> frame_node_;
};
// A page with no `CannotFreezeReason` that is alone in its browsing instance is
// frozen when it has a freezing vote.
TEST_F(FreezingPolicyTest, Basic) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
}
// Multiple connected pages in the same browsing instance with no
// `CannotFreezeReason` are frozen when they all have a freezing vote.
TEST_F(FreezingPolicyTest, ManyPagesSameBrowsingInstance) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
// Adding a freezing vote to each of the 2 pages in the browsing instance
// freezes all pages.
policy()->AddFreezeVote(page_node());
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
policy()->AddFreezeVote(page2.get());
VerifyFreezerExpectations();
// Adding a 3rd page (with no freezing vote yet) to the browsing instance
// unfreezes all pages.
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
auto [page3, frame3] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
policy()->AddFreezeVote(page3.get());
VerifyFreezerExpectations();
// Multiple votes on the same page don't change anything.
policy()->AddFreezeVote(page3.get());
policy()->AddFreezeVote(page3.get());
// Removing a freezing vote from one page unfreezes all pages.
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page3.get()));
policy()->RemoveFreezeVote(page_node());
VerifyFreezerExpectations();
policy()->RemoveFreezeVote(page2.get());
policy()->RemoveFreezeVote(page3.get());
policy()->RemoveFreezeVote(page3.get());
policy()->RemoveFreezeVote(page3.get());
}
// Similar to ManyPagesSameBrowsingInstance, except that the 1st and 3rd pages
// don't have frames in the same browsing instance (they're indirectly connected
// via the 2nd page).
TEST_F(FreezingPolicyTest, ConnectedPages) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
auto frame2b =
CreateFrameNodeAutoId(process_node(), page2.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceB);
// Adding a freezing vote to the 2 connected pages freezes them.
policy()->AddFreezeVote(page_node());
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
policy()->AddFreezeVote(page2.get());
VerifyFreezerExpectations();
// Adding a 3rd page (with no freezing vote yet) to the set of connected pages
// unfreezes all pages.
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
auto [page3, frame3] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
policy()->AddFreezeVote(page3.get());
VerifyFreezerExpectations();
// Multiple votes on the same page don't change anything.
policy()->AddFreezeVote(page3.get());
policy()->AddFreezeVote(page3.get());
// Removing a freezing vote from one page unfreezes all pages.
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page3.get()));
policy()->RemoveFreezeVote(page_node());
VerifyFreezerExpectations();
policy()->RemoveFreezeVote(page2.get());
policy()->RemoveFreezeVote(page3.get());
policy()->RemoveFreezeVote(page3.get());
policy()->RemoveFreezeVote(page3.get());
}
// A browsing instance with many pages that each have a freeze vote is unfrozen
// when one of the pages gets a `CannotFreezeReason`.
TEST_F(FreezingPolicyTest,
AddCannotFreezeReasonToBrowsingInstanceWithManyPages) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
policy()->AddFreezeVote(page_node());
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
policy()->AddFreezeVote(page2.get());
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
IsEmpty());
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
page_node()->SetIsHoldingWebLockForTesting(true);
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(
page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
}
// Similar to AddCannotFreezeReasonToBrowsingInstanceWithManyPages, except that
// the 1st and 3rd pages don't have frames in the same browsing instance
// (they're indirectly connected via the 2nd page).
TEST_F(FreezingPolicyTest, AddCannotFreezeReasonToConnectedPages) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
auto frame2b =
CreateFrameNodeAutoId(process_node(), page2.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceB);
auto [page3, frame3] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
policy()->AddFreezeVote(page_node());
policy()->AddFreezeVote(page2.get());
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
policy()->AddFreezeVote(page3.get());
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(page3.get(), FreezingType::kVoting,
IsEmpty());
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), UnfreezePageNode(page3.get()));
page_node()->SetIsHoldingWebLockForTesting(true);
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(
page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectConnectedCannotFreezeReasons(
page3.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
}
// A browsing instance with one page that has a `CannotFreezeReason` is not
// frozen when all its pages get a freeze vote.
TEST_F(FreezingPolicyTest,
AddFreezeVotesToBrowsingInstanceWithManyPagesAndCannotFreezeReason) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
page_node()->SetIsHoldingWebLockForTesting(true);
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(
page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
policy()->AddFreezeVote(page2.get());
VerifyFreezerExpectations();
}
// Similar to
// AddFreezeVotesToBrowsingInstanceWithManyPagesAndCannotFreezeReason, except
// that the 1st and 3rd pages don't have frames in the same browsing instance
// (they're indirectly connected via the 2nd page).
TEST_F(FreezingPolicyTest,
AddFreezeVotesToConnectedPagesWithCannotFreezeReason) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
auto frame2b =
CreateFrameNodeAutoId(process_node(), page2.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceB);
auto [page3, frame3] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
page_node()->SetIsHoldingWebLockForTesting(true);
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(
page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectConnectedCannotFreezeReasons(
page3.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
policy()->AddFreezeVote(page2.get());
policy()->AddFreezeVote(page3.get());
VerifyFreezerExpectations();
}
// Verify that frozen state is correctly updated when a set of connected pages
// is broken in two by the deletion of a frame.
TEST_F(FreezingPolicyTest, BreakConnectedSet) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
auto frame2b =
CreateFrameNodeAutoId(process_node(), page2.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceB);
auto [page3, frame3] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
page_node()->SetIsHoldingWebLockForTesting(true);
policy()->AddFreezeVote(page_node());
policy()->AddFreezeVote(page2.get());
policy()->AddFreezeVote(page3.get());
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(
page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectConnectedCannotFreezeReasons(
page3.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
// Deleting `frame2` puts `page_node()` in a different connected set than
// `page2` and `page3`. `page_node()` cannot be frozen because it has a
// `CannotFreezeReason`. `page2` and `page3` can be frozen because they have
// freeze votes and no `CannotFreezeReason`.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
frame2.reset();
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(page3.get(), FreezingType::kVoting,
IsEmpty());
}
// Similar to BreakConnectedSet, but the connected set left by the page from
// which a page is deleted can be frozen.
TEST_F(FreezingPolicyTest, BreakConnectedSet_LeftSetIsFrozen) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
auto frame2b =
CreateFrameNodeAutoId(process_node(), page2.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceB);
auto [page3, frame3] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
page2->SetIsHoldingWebLockForTesting(true);
policy()->AddFreezeVote(page_node());
policy()->AddFreezeVote(page2.get());
policy()->AddFreezeVote(page3.get());
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(
page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(
page3.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
// Deleting `frame2` puts `page_node()` in a different connected set than
// `page2` and `page3`. `page_node()` cannot be frozen because it has a
// `CannotFreezeReason`. `page2` and `page3` can be frozen because they have
// freeze votes and no `CannotFreezeReason`.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
frame2.reset();
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
ExpectConnectedCannotFreezeReasons(page_node(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(page2.get(), FreezingType::kVoting,
IsEmpty());
ExpectConnectedCannotFreezeReasons(
page3.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kHoldingWebLock));
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenVisible) {
page_node()->SetIsVisible(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
}
TEST_F(FreezingPolicyTest, BecomesVisibleWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetIsVisible(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenRecentlyVisible) {
page_node()->SetIsVisible(true);
page_node()->SetIsVisible(false);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after visible protection time.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
task_env().FastForwardBy(features::kFreezingVisibleProtectionTime.Get());
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, BecomesRecentlyVisibleWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetIsVisible(true);
// Don't expect freezing, because the page is still "recently visible".
page_node()->SetIsVisible(false);
// Expect freezing after visible protection time.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
task_env().FastForwardBy(features::kFreezingVisibleProtectionTime.Get());
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenAudible) {
page_node()->SetIsAudible(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
}
TEST_F(FreezingPolicyTest, BecomesAudibleWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetIsAudible(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenRecentlyAudible) {
page_node()->SetIsAudible(true);
page_node()->SetIsAudible(false);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after audio protection time.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
task_env().FastForwardBy(features::kFreezingAudioProtectionTime.Get());
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, BecomesRecentlyAudibleWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetIsAudible(true);
// Don't expect freezing, because the page is still "recently audible".
page_node()->SetIsAudible(false);
// Expect freezing after audio protection time.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
task_env().FastForwardBy(features::kFreezingAudioProtectionTime.Get());
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWithOriginTrialOptOut) {
page_node()->SetHasFreezingOriginTrialOptOutForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after removing the origin trial opt-out.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
page_node()->SetHasFreezingOriginTrialOptOutForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, OriginTrialOptOutWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetHasFreezingOriginTrialOptOutForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenHoldingWebLock) {
page_node()->SetIsHoldingWebLockForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after releasing the lock.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
page_node()->SetIsHoldingWebLockForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, AcquiresWebLockWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetIsHoldingWebLockForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenHoldingBlockingIndexedDBLock) {
page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after the transaction completes.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, AcquiresBlockingIndexedDBLockWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenConnectedToUSBDevice) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToUSBDeviceForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after disconnecting from USB device.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToUSBDeviceForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, ConnectedToUSBDeviceWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToUSBDeviceForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenConnectedToBluetoothDevice) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToBluetoothDeviceForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after disconnecting from Bluetooth device.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToBluetoothDeviceForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, ConnectedToBluetoothDeviceWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToBluetoothDeviceForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenConnectedToHidDevice) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToHidDeviceForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after disconnecting from HID device.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToHidDeviceForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, ConnectedToHidDeviceWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToHidDeviceForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenConnectedToSerialPort) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToSerialPortForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after disconnecting from HID device.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToSerialPortForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, ConnectedToSerialPortWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsConnectedToSerialPortForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenCapturingVideo) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingVideoForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after stopping capture.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingVideoForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, StartsCapturingVideoWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingAudioForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenCapturingAudio) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingAudioForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after stopping capture.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingAudioForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, StartsCapturingAudioWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingAudioForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenMirrored) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsBeingMirroredForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after mirroring stops.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsBeingMirroredForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, StartsBeingMirroredWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsBeingMirroredForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenCapturingWindow) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingWindowForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after stopping capture.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingWindowForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, StartsCapturingWindowWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingWindowForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenCapturingDisplay) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingDisplayForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after stopping capture.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingDisplayForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, StartsCapturingDisplayWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node())
->SetIsCapturingDisplayForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenUsingWebRTC) {
page_node()->SetUsesWebRTCForTesting(true);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after stopping capture.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
page_node()->SetUsesWebRTCForTesting(false);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, StartsUsingWebRTCWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetUsesWebRTCForTesting(true);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWithNotificationPermission) {
page_node()->OnNotificationPermissionStatusChange(
blink::mojom::PermissionStatus::GRANTED);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing if the permission is revoked.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
page_node()->OnNotificationPermissionStatusChange(
blink::mojom::PermissionStatus::DENIED);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, NotificationPermissionWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->OnNotificationPermissionStatusChange(
blink::mojom::PermissionStatus::GRANTED);
VerifyFreezerExpectations();
// Changing to ASK removes the opt-out.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
page_node()->OnNotificationPermissionStatusChange(
blink::mojom::PermissionStatus::ASK);
VerifyFreezerExpectations();
// Changing to DENIED does nothing, since there is already no opt-out.
page_node()->OnNotificationPermissionStatusChange(
blink::mojom::PermissionStatus::DENIED);
// Changing to GRANTED adds the opt-out.
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->OnNotificationPermissionStatusChange(
blink::mojom::PermissionStatus::GRANTED);
VerifyFreezerExpectations();
// Changing to DENIED removes the opt-out.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
page_node()->OnNotificationPermissionStatusChange(
blink::mojom::PermissionStatus::DENIED);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, FreezeVoteWhenLoading) {
page_node()->SetLoadingState(PageNode::LoadingState::kLoadedBusy);
// Don't expect freezing.
policy()->AddFreezeVote(page_node());
// Expect freezing after finishing loading.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
page_node()->SetLoadingState(PageNode::LoadingState::kLoadedIdle);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, StartsLoadingWhenFrozen) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
page_node()->SetLoadingState(PageNode::LoadingState::kLoadedBusy);
VerifyFreezerExpectations();
}
TEST_F(FreezingPolicyTest, DiscardGrowingPrivateMemory_Basic) {
base::test::ScopedFeatureList feature_list{
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
// Pretend that the page is frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// First memory measurement after freezing.
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kContext, kInitialPMF);
// Another memory measurement, *not* crossing the growth threshold.
constexpr base::ByteCount kSecondPMF = base::KiB(20);
ASSERT_LT(kSecondPMF - kInitialPMF, growth_threshold);
ReportMemoryUsage(kContext, kSecondPMF);
// Another memory measurement, crossing the growth threshold. The page should
// be discarded.
EXPECT_CALL(*discarder(),
DiscardPages(testing::_, testing::ElementsAre(page_node())));
ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));
VerifyDiscarderExpectations();
}
// Regression test for crbug.com/407522185: When a (non-frozen) page is added to
// a browsing instance in which all pages are frozen, the post-freezing memory
// estimates are cleared.
TEST_F(FreezingPolicyTest, DiscardGrowingPrivateMemory_PageAddedAfterFreezing) {
base::test::ScopedFeatureList feature_list{
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
// Pretend that the page is frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// First memory measurement after freezing.
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kContext, kInitialPMF);
// Add a (non-frozen) page to the browsing instance.
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
// Memory measurement crossing the growth threshold. This should not result in
// discarding (or crash) since post-freezing memory estimates were cleared.
const base::ByteCount kSecondPMF =
kInitialPMF + growth_threshold + base::KiB(1);
ReportMemoryUsage(kContext, kSecondPMF);
// Pretend that the new page is frozen.
page2->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// Memory measurement crossing the growth threshold. Should not result in
// discarding since it's the first measurement since the new page was added.
const base::ByteCount kThirdPMF =
kSecondPMF + growth_threshold + base::KiB(1);
ReportMemoryUsage(kContext, kThirdPMF);
// Memory measurement crossing the growth threshold. This should result in
// discarding.
const base::ByteCount kFourthPMFKb =
kThirdPMF + growth_threshold + base::KiB(1);
EXPECT_CALL(*discarder(),
DiscardPages(testing::_, testing::UnorderedElementsAre(
page_node(), page2.get())));
ReportMemoryUsage(kContext, kFourthPMFKb);
VerifyDiscarderExpectations();
}
TEST_F(FreezingPolicyTest, DiscardGrowingPrivateMemory_FeatureDisabled) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF);
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
// Pretend that the page is frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// First memory measurement after freezing.
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kContext, kInitialPMF);
// Another memory measurement, crossing the growth threshold. The page should
// not be discarded since the feature is disabled.
ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));
VerifyDiscarderExpectations();
}
TEST_F(FreezingPolicyTest,
DiscardGrowingPrivateMemory_MultipleFrozenPagesInBrowsingInstance) {
base::test::ScopedFeatureList feature_list{
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
// Pretend that the pages are frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
page2->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// First memory measurement after freezing.
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kContext, kInitialPMF);
// Another memory measurement, crossing the growth threshold. The 2 pages
// should be discarded.
EXPECT_CALL(*discarder(),
DiscardPages(testing::_, testing::UnorderedElementsAre(
page_node(), page2.get())));
ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));
VerifyDiscarderExpectations();
}
TEST_F(FreezingPolicyTest,
DiscardGrowingPrivateMemory_FrozenAndUnfrozenPagesInBrowsingInstance) {
base::test::ScopedFeatureList feature_list{
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
// Pretend that the first page is frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// First memory measurement after freezing the page (2nd page still unfrozen).
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kContext, kInitialPMF);
// Pretend that the 2nd page is frozen.
page2->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// Another memory measurement, crossing the growth threshold since the first
// page was frozen (but not since *all* pages were frozen). No discarding
// expected.
ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));
// Another memory measurement, crossing the growth threshold since all pages
// were frozen. The 2 pages should be discarded.
EXPECT_CALL(*discarder(),
DiscardPages(testing::_, testing::UnorderedElementsAre(
page_node(), page2.get())));
ReportMemoryUsage(kContext,
kInitialPMF + 2 * (growth_threshold + base::KiB(1)));
VerifyDiscarderExpectations();
}
TEST_F(FreezingPolicyTest, DiscardGrowingPrivateMemory_Unfreeze) {
base::test::ScopedFeatureList feature_list{
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
// Pretend that the page is frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// First memory measurement after freezing.
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kContext, kInitialPMF);
// Pretend that the page is unfrozen and re-frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kRunning);
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// Another memory measurement, crossing the growth threshold since the
// measurement taken before unfreezing. The page should not be discarded,
// because this is the first measurement since re-freezing.
ReportMemoryUsage(kContext, kInitialPMF + growth_threshold + base::KiB(1));
// Another memory measurement, crossing the growth threshold since the
// measurement taken after re-freezing. The page should be discarded.
EXPECT_CALL(*discarder(),
DiscardPages(testing::_, testing::ElementsAre(page_node())));
ReportMemoryUsage(kContext,
kInitialPMF + 2 * (growth_threshold + base::KiB(1)));
VerifyDiscarderExpectations();
}
TEST_F(
FreezingPolicyTest,
DiscardGrowingPrivateMemory_MeasurementForNewOrigin_BelowGrowthThreshold) {
base::test::ScopedFeatureList feature_list{
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
const resource_attribution::OriginInBrowsingInstanceContext kOtherContext{
url::Origin(), kBrowsingInstanceA};
// Pretend that the page is frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// First memory measurement after freezing.
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kContext, kInitialPMF);
// A memory measurement below the growth threshold for an origin not seen in
// the first measurement. Nothing should happen.
ReportMemoryUsage(kOtherContext, growth_threshold - base::KiB(1));
VerifyDiscarderExpectations();
// A second memory measurement above the growth threshold for an origin not
// seen in the first measurement. The browsing instance should be discarded.
EXPECT_CALL(*discarder(),
DiscardPages(testing::_, testing::ElementsAre(page_node())));
ReportMemoryUsage(kOtherContext, growth_threshold + base::KiB(1));
VerifyDiscarderExpectations();
}
TEST_F(
FreezingPolicyTest,
DiscardGrowingPrivateMemory_MeasurementForNewOrigin_AboveGrowthThreshold) {
base::test::ScopedFeatureList feature_list{
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
const resource_attribution::OriginInBrowsingInstanceContext kOtherContext{
url::Origin(), kBrowsingInstanceA};
// Pretend that the page is frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// First memory measurement after freezing.
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kContext, kInitialPMF);
// A memory measurement above the growth threshold for an origin not seen in
// the first measurement. The browsing instance should be discarded.
EXPECT_CALL(*discarder(),
DiscardPages(testing::_, testing::ElementsAre(page_node())));
ReportMemoryUsage(kOtherContext, growth_threshold + base::KiB(1));
VerifyDiscarderExpectations();
}
TEST_F(FreezingPolicyTest,
DiscardGrowingPrivateMemory_MeasurementForNewBrowsingInstance) {
base::test::ScopedFeatureList feature_list{
features::kDiscardFrozenBrowsingInstancesWithGrowingPMF};
const base::ByteCount growth_threshold =
base::KiB(features::kFreezingMemoryGrowthThresholdToDiscardKb.Get());
const resource_attribution::OriginInBrowsingInstanceContext
kUnknownBrowsingInstanceContext{url::Origin(), kBrowsingInstanceB};
// Pretend that the page is frozen.
page_node()->SetLifecycleStateForTesting(PageNode::LifecycleState::kFrozen);
// Simulate memory usage growth above the threshold for a browsing instance
// not known to the `FreezingPolicy`. This should be gracefully ignored.
constexpr base::ByteCount kInitialPMF = base::KiB(10);
ReportMemoryUsage(kUnknownBrowsingInstanceContext, kInitialPMF);
ReportMemoryUsage(kUnknownBrowsingInstanceContext,
kInitialPMF + growth_threshold + base::KiB(1));
}
namespace {
class FreezingPolicyBatterySaverTest : public FreezingPolicyTest {
public:
FreezingPolicyBatterySaverTest() = default;
private:
base::test::ScopedFeatureList scoped_feature_list_{
features::kFreezingOnBatterySaver};
};
} // namespace
TEST_F(FreezingPolicyBatterySaverTest, Basic) {
policy()->ToggleFreezingOnBatterySaverMode(true);
ReportCumulativeCPUUsage(kContext, base::Seconds(60));
AdvanceClock(base::Seconds(60));
// The page should be frozen when a browsing instance connected to it consumes
// >=25% CPU in background.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
ReportCumulativeCPUUsage(kContext, base::Seconds(75));
}
TEST_F(FreezingPolicyBatterySaverTest, RecordFreezingEligibilityUKM) {
base::test::ScopedFeatureList feature_list(
features::kRecordFreezingEligibilityUKM);
base::MetricsSubSampler::ScopedAlwaysSampleForTesting always_sample;
// page_node(), page2 and page3 are connected. page4 is disjoint.
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
auto frame2b =
CreateFrameNodeAutoId(process_node(), page2.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceB);
auto [page3, frame3] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
auto [page4, frame4] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceC);
page_node()->SetUkmSourceId(ukm::AssignNewSourceId());
page2->SetUkmSourceId(ukm::AssignNewSourceId());
page3->SetUkmSourceId(ukm::AssignNewSourceId());
page4->SetUkmSourceId(ukm::AssignNewSourceId());
// kContext affects page_node(), page2 and page3. kContext2 and kContext3
// affect page4.
const resource_attribution::OriginInBrowsingInstanceContext kContext2{
url::Origin(), kBrowsingInstanceC};
const resource_attribution::OriginInBrowsingInstanceContext kContext3{
url::Origin(), kBrowsingInstanceC};
// A `CannotFreezeReason` applicable at the beginning of the CPU measurement
// interval affects the UKM event.
page_node()->SetIsHoldingWebLockForTesting(true);
// Simulate initial CPU measurement.
{
resource_attribution::QueryResultMap cpu_result_map;
AddCPUMeasurement(cpu_result_map, kContext, base::Seconds(0));
AddCPUMeasurement(cpu_result_map, kContext2, base::Seconds(0));
AddCPUMeasurement(cpu_result_map, kContext3, base::Seconds(0));
resource_attribution::QueryResultObserver* observer = policy();
observer->OnResourceUsageUpdated(std::move(cpu_result_map));
}
AdvanceClock(base::Seconds(60));
page_node()->SetIsHoldingWebLockForTesting(false);
// A `CannotFreezeReason` applicable transiently during the CPU measurement
// interval affects the UKM event.
page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(true);
page_node()->SetIsHoldingBlockingIndexedDBLockForTesting(false);
// Simulate 2nd CPU measurement. Expect UKM events to be reported.
{
CannotFreezeReasonSet expected_cannot_freeze_reasons{
CannotFreezeReason::kHoldingWebLock,
CannotFreezeReason::kHoldingBlockingIndexedDBLock};
EXPECT_CALL(*policy(),
RecordFreezingEligibilityUKMForPage(
page_node()->GetUkmSourceID(),
/*highest_cpu_current_interval=*/0.5,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0, expected_cannot_freeze_reasons));
EXPECT_CALL(*policy(),
RecordFreezingEligibilityUKMForPage(
page2->GetUkmSourceID(),
/*highest_cpu_current_interval=*/0.5,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0, expected_cannot_freeze_reasons));
EXPECT_CALL(*policy(),
RecordFreezingEligibilityUKMForPage(
page3->GetUkmSourceID(),
/*highest_cpu_current_interval=*/0.5,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0, expected_cannot_freeze_reasons));
EXPECT_CALL(*policy(),
RecordFreezingEligibilityUKMForPage(
page4->GetUkmSourceID(),
/*highest_cpu_current_interval=*/1.0,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
1.0, CannotFreezeReasonSet{}));
resource_attribution::QueryResultMap cpu_result_map;
AddCPUMeasurement(cpu_result_map, kContext, base::Seconds(30)); // 50%
AddCPUMeasurement(cpu_result_map, kContext2, base::Seconds(45)); // 75%
AddCPUMeasurement(cpu_result_map, kContext3, base::Seconds(60)); // 100%
resource_attribution::QueryResultObserver* observer = policy();
observer->OnResourceUsageUpdated(std::move(cpu_result_map));
}
AdvanceClock(base::Seconds(60));
// Simulate 3rd CPU measurement (no applicable `CannotFreezeReason` this
// time). Expect UKM events to be reported.
{
EXPECT_CALL(*policy(),
RecordFreezingEligibilityUKMForPage(
page_node()->GetUkmSourceID(),
/*highest_cpu_current_interval=*/0.25,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0.25, CannotFreezeReasonSet{}));
EXPECT_CALL(*policy(),
RecordFreezingEligibilityUKMForPage(
page2->GetUkmSourceID(),
/*highest_cpu_current_interval=*/0.25,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0.25, CannotFreezeReasonSet{}));
EXPECT_CALL(*policy(),
RecordFreezingEligibilityUKMForPage(
page3->GetUkmSourceID(),
/*highest_cpu_current_interval=*/0.25,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0.25, CannotFreezeReasonSet{}));
EXPECT_CALL(*policy(),
RecordFreezingEligibilityUKMForPage(
page4->GetUkmSourceID(),
/*highest_cpu_current_interval=*/0.75,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
1.0, CannotFreezeReasonSet{}));
resource_attribution::QueryResultMap cpu_result_map;
AddCPUMeasurement(cpu_result_map, kContext, base::Seconds(45)); // 25%
AddCPUMeasurement(cpu_result_map, kContext2, base::Seconds(90)); // 75%
AddCPUMeasurement(cpu_result_map, kContext3, base::Seconds(66)); // 10%
resource_attribution::QueryResultObserver* observer = policy();
observer->OnResourceUsageUpdated(std::move(cpu_result_map));
}
}
TEST_F(FreezingPolicyBatterySaverTest,
RecordFreezingEligibilityUKMForPageStatic) {
// No "opt-out" field is set.
// CPU usage is bucketed.
{
ukm::TestAutoSetUkmRecorder recorder;
FreezingPolicy::RecordFreezingEligibilityUKMForPageStatic(
ukm::SourceId(), /*highest_cpu_current_interval=*/0.1,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0.2, CannotFreezeReasonSet{});
auto entries = recorder.GetEntriesByName(
ukm::builders::PerformanceManager_FreezingEligibility::kEntryName);
EXPECT_EQ(entries.size(), 1U);
auto& entry = entries.front();
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Audible", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "BeingMirrored", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Capturing", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "ConnectedToDevice", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(
entry, "HighestCPUAnyIntervalWithoutOptOut", 16);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HighestCPUCurrentInterval",
8);
ukm::TestUkmRecorder::ExpectEntryMetric(entry,
"HoldingBlockingIndexedDBLock", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HoldingWebLock", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Loading", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "NotificationPermission", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "OriginTrialOptOut", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyAudible", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyVisible", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Visible", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "WebRTC", 0);
}
// All "opt-out" fields are set.
// CPU usage is bucketed.
{
ukm::TestAutoSetUkmRecorder recorder;
FreezingPolicy::RecordFreezingEligibilityUKMForPageStatic(
ukm::SourceId(), /*highest_cpu_current_interval=*/0.16,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0.32,
CannotFreezeReasonSet{
CannotFreezeReason::kAudible, CannotFreezeReason::kBeingMirrored,
CannotFreezeReason::kCapturingAudio,
CannotFreezeReason::kConnectedToBluetoothDevice,
CannotFreezeReason::kHoldingBlockingIndexedDBLock,
CannotFreezeReason::kHoldingWebLock, CannotFreezeReason::kLoading,
CannotFreezeReason::kNotificationPermission,
CannotFreezeReason::kFreezingOriginTrialOptOut,
CannotFreezeReason::kRecentlyAudible,
CannotFreezeReason::kRecentlyVisible, CannotFreezeReason::kVisible,
CannotFreezeReason::kWebRTC});
auto entries = recorder.GetEntriesByName(
ukm::builders::PerformanceManager_FreezingEligibility::kEntryName);
EXPECT_EQ(entries.size(), 1U);
auto& entry = entries.front();
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Audible", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "BeingMirrored", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Capturing", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "ConnectedToDevice", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(
entry, "HighestCPUAnyIntervalWithoutOptOut", 32);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HighestCPUCurrentInterval",
16);
ukm::TestUkmRecorder::ExpectEntryMetric(entry,
"HoldingBlockingIndexedDBLock", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HoldingWebLock", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Loading", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "NotificationPermission", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "OriginTrialOptOut", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyAudible", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyVisible", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Visible", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "WebRTC", 1);
}
// Opt-out fields from Audible -> Loading are set.
// CPU usage is zero.
{
ukm::TestAutoSetUkmRecorder recorder;
FreezingPolicy::RecordFreezingEligibilityUKMForPageStatic(
ukm::SourceId(), /*highest_cpu_current_interval=*/0.0,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0.0,
CannotFreezeReasonSet{
CannotFreezeReason::kAudible, CannotFreezeReason::kBeingMirrored,
CannotFreezeReason::kCapturingVideo,
CannotFreezeReason::kConnectedToUsbDevice,
CannotFreezeReason::kHoldingBlockingIndexedDBLock,
CannotFreezeReason::kHoldingWebLock, CannotFreezeReason::kLoading});
auto entries = recorder.GetEntriesByName(
ukm::builders::PerformanceManager_FreezingEligibility::kEntryName);
EXPECT_EQ(entries.size(), 1U);
auto& entry = entries.front();
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Audible", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "BeingMirrored", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Capturing", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "ConnectedToDevice", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(
entry, "HighestCPUAnyIntervalWithoutOptOut", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HighestCPUCurrentInterval",
0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry,
"HoldingBlockingIndexedDBLock", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HoldingWebLock", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Loading", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "NotificationPermission", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "OriginTrialOptOut", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyAudible", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyVisible", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Visible", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "WebRTC", 0);
}
// Opt-out fields from Notification -> WebRTC are set.
// CPU usage is very low (bucketing has no effect at this level).
{
ukm::TestAutoSetUkmRecorder recorder;
FreezingPolicy::RecordFreezingEligibilityUKMForPageStatic(
ukm::SourceId(), /*highest_cpu_current_interval=*/0.01,
/*highest_cpu_without_battery_saver_cannot_freeze=*/
0.02,
CannotFreezeReasonSet{CannotFreezeReason::kNotificationPermission,
CannotFreezeReason::kFreezingOriginTrialOptOut,
CannotFreezeReason::kRecentlyAudible,
CannotFreezeReason::kRecentlyVisible,
CannotFreezeReason::kVisible,
CannotFreezeReason::kWebRTC});
auto entries = recorder.GetEntriesByName(
ukm::builders::PerformanceManager_FreezingEligibility::kEntryName);
EXPECT_EQ(entries.size(), 1U);
auto& entry = entries.front();
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Audible", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "BeingMirrored", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Capturing", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "ConnectedToDevice", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(
entry, "HighestCPUAnyIntervalWithoutOptOut", 2);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HighestCPUCurrentInterval",
1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry,
"HoldingBlockingIndexedDBLock", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "HoldingWebLock", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Loading", 0);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "NotificationPermission", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "OriginTrialOptOut", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyAudible", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "RecentlyVisible", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "Visible", 1);
ukm::TestUkmRecorder::ExpectEntryMetric(entry, "WebRTC", 1);
}
}
TEST_F(FreezingPolicyBatterySaverTest, ConnectedPages) {
auto [page2, frame2] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
auto frame2b =
CreateFrameNodeAutoId(process_node(), page2.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceB);
auto [page3, frame3] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
policy()->ToggleFreezingOnBatterySaverMode(true);
ReportCumulativeCPUUsage(kContext, base::Seconds(60));
AdvanceClock(base::Seconds(60));
// The page should be frozen when a browsing instance connected to it consumes
// >=25% CPU in background.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
ReportCumulativeCPUUsage(kContext, base::Seconds(75));
}
TEST_F(FreezingPolicyBatterySaverTest, CannotFreeze) {
policy()->ToggleFreezingOnBatterySaverMode(true);
ReportCumulativeCPUUsage(kContext, base::Seconds(60));
AdvanceClock(base::Seconds(60));
// Add a `CannotFreezeReason`.
page_node()->SetIsHoldingWebLockForTesting(true);
// The page should not be frozen when a browsing instance connected to it
// consumes >=25% CPU in background, because it has a `CannotFreezeReason`.
ReportCumulativeCPUUsage(kContext, base::Seconds(75));
AdvanceClock(base::Seconds(60));
// Remove the `CannotFreezeReason`. This should not cause the page to be
// frozen, since there was a `CannotFreezeReason` when high CPU usage was
// measured.
page_node()->SetIsHoldingWebLockForTesting(false);
// The page should not be frozen when a browsing instance connected to it
// consumes >=25% CPU in background, because it transiently had a
// `CannotFreezeReason` during the measurement interval.
ReportCumulativeCPUUsage(kContext, base::Seconds(90));
AdvanceClock(base::Seconds(60));
// The page should be frozen when a browsing instance connected to it consumes
// >=25% CPU in background and there was no `CannotFreezeReason` at any point
// during the measurement interval.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
ReportCumulativeCPUUsage(kContext, base::Seconds(105));
}
TEST_F(FreezingPolicyBatterySaverTest, CannotFreezeTransient) {
policy()->ToggleFreezingOnBatterySaverMode(true);
ReportCumulativeCPUUsage(kContext, base::Seconds(60));
AdvanceClock(base::Seconds(60));
// Transiently add a `CannotFreezeReason`.
page_node()->SetIsHoldingWebLockForTesting(true);
page_node()->SetIsHoldingWebLockForTesting(false);
// The page should not be frozen when a browsing instance connected to it
// consumes >=25% CPU in background, because it transiently had a
// `CannotFreezeReason` during the measurement interval.
ReportCumulativeCPUUsage(kContext, base::Seconds(75));
}
TEST_F(FreezingPolicyBatterySaverTest, BatterySaverInactive) {
// Battery Saver is not active in this test.
ReportCumulativeCPUUsage(kContext, base::Seconds(60));
AdvanceClock(base::Seconds(60));
// The page should not be frozen when a browsing instance connected to it
// consumes >=25% CPU in background, because Battery Saver is not active.
ReportCumulativeCPUUsage(kContext, base::Seconds(75));
}
TEST_F(FreezingPolicyBatterySaverTest, ForegroundCPU) {
policy()->ToggleFreezingOnBatterySaverMode(true);
ReportCumulativeCPUUsage(kContext,
/*cumulative_background_cpu=*/base::Seconds(60),
/*cumulative_cpu=*/base::Seconds(60));
AdvanceClock(base::Seconds(60));
// The page should not be frozen when a browsing instance connected to it
// consumes >=25% CPU in foreground, but little CPU in background.
ReportCumulativeCPUUsage(kContext,
/*cumulative_background_cpu=*/base::Seconds(62),
/*cumulative_cpu=*/base::Seconds(90));
}
TEST_F(FreezingPolicyBatterySaverTest, DeactivateBatterySaver) {
policy()->ToggleFreezingOnBatterySaverMode(true);
ReportCumulativeCPUUsage(kContext, base::Seconds(60));
AdvanceClock(base::Seconds(60));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
ReportCumulativeCPUUsage(kContext, base::Seconds(75));
VerifyFreezerExpectations();
// The page should be unfrozen when Battery Saver becomes inactive.
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
policy()->ToggleFreezingOnBatterySaverMode(false);
}
TEST_F(FreezingPolicyBatterySaverTest, ActivateBatterySaverAfterHighCPU) {
// Battery Saver is not active at the beginning of this test.
// Report high background CPU usage.
ReportCumulativeCPUUsage(kContext, base::Seconds(60));
AdvanceClock(base::Seconds(60));
ReportCumulativeCPUUsage(kContext, base::Seconds(75));
// The page should be frozen when Battery Saver becomes active.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->ToggleFreezingOnBatterySaverMode(true);
}
namespace {
constexpr char kOptOutUrl1[] = "http://a.com/";
constexpr char kOptOutUrl2[] = "http://b.com/";
constexpr char kBrowsingContext1[] = "browsing-context-1";
constexpr char kBrowsingContext2[] = "browsing-context-2";
// A test implementation of OptOutChecker that opts out a single URL.
class TestOptOutChecker final : public freezing::OptOutChecker {
public:
TestOptOutChecker() = default;
~TestOptOutChecker() final = default;
TestOptOutChecker(const TestOptOutChecker&) = delete;
TestOptOutChecker& operator=(const TestOptOutChecker&) = delete;
// Sets the opted out url to `url`, and notifies `browser_contexts_to_notify`
// of the change.
void SetOptedOutUrl(
const std::string& url,
const std::vector<std::string>& browser_contexts_to_notify);
// OptOutChecker:
void SetOptOutPolicyChangedCallback(
OnPolicyChangedForBrowserContextCallback callback) final;
bool IsPageOptedOutOfFreezing(std::string_view browser_context_id,
const GURL& main_frame_url) final;
private:
OnPolicyChangedForBrowserContextCallback on_policy_changed_callback_;
GURL opted_out_url_;
};
void TestOptOutChecker::SetOptedOutUrl(
const std::string& url,
const std::vector<std::string>& browser_contexts_to_notify = {}) {
ASSERT_TRUE(on_policy_changed_callback_);
opted_out_url_ = GURL(url);
for (const std::string& browser_context_id : browser_contexts_to_notify) {
on_policy_changed_callback_.Run(browser_context_id);
}
}
void TestOptOutChecker::SetOptOutPolicyChangedCallback(
OnPolicyChangedForBrowserContextCallback callback) {
on_policy_changed_callback_ = std::move(callback);
}
bool TestOptOutChecker::IsPageOptedOutOfFreezing(
std::string_view browser_context_id,
const GURL& main_frame_url) {
return opted_out_url_.is_valid() && main_frame_url == opted_out_url_;
}
class FreezingPolicyOptOutTest : public FreezingPolicyTest {
public:
std::unique_ptr<freezing::OptOutChecker> CreateTestOptOutChecker() override {
auto opt_out_checker = std::make_unique<TestOptOutChecker>();
opt_out_checker_ = opt_out_checker.get();
return opt_out_checker;
}
void TearDown() override {
// Prevent dangling raw_ptr.
opt_out_checker_ = nullptr;
FreezingPolicyTest::TearDown();
}
void NavigateToUrl(PageNodeImpl* page_node, std::string_view url) {
page_node->OnMainFrameNavigationCommitted(
/*same_document=*/false, base::TimeTicks::Now(), next_navigation_id_++,
GURL(url), /*contents_mime_type=*/"",
/*notification_permission_status=*/std::nullopt);
}
protected:
raw_ptr<TestOptOutChecker> opt_out_checker_ = nullptr;
// page_node() navigates once when the GraphTestHarness is set up.
int next_navigation_id_ = 2;
};
} // namespace
TEST_F(FreezingPolicyOptOutTest, MainFrameUrlChanges) {
ASSERT_TRUE(opt_out_checker_.get());
opt_out_checker_->SetOptedOutUrl(kOptOutUrl1);
// Can freeze before an URL is set.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
policy()->AddFreezeVote(page_node());
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
// Stays frozen when navigating to an URL that's not opted out.
NavigateToUrl(page_node(), kOptOutUrl2);
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
// Unfreezes when navigating to an URL that's opted out.
EXPECT_CALL(*freezer(), UnfreezePageNode(page_node()));
NavigateToUrl(page_node(), kOptOutUrl1);
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kOptedOut));
// Navigating to the same URL does nothing.
NavigateToUrl(page_node(), kOptOutUrl1);
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kOptedOut));
// Freezes when navigating away from the opted out URL.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page_node()));
NavigateToUrl(page_node(), kOptOutUrl2);
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page_node(), FreezingType::kVoting, IsEmpty());
}
TEST_F(FreezingPolicyOptOutTest, OptOutPolicyChanges) {
ASSERT_TRUE(opt_out_checker_.get());
opt_out_checker_->SetOptedOutUrl(kOptOutUrl1);
// Give each page a unique browsing instance so that there are no connected
// pages. kBrowsingInstanceA is already used by page_node().
auto [page1, frame1] = CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId(10), kBrowsingContext1);
auto [page2, frame2] = CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId(11), kBrowsingContext1);
auto [page3, frame3] = CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId(12), kBrowsingContext2);
// Each VerifyFreezerExpectations() call is inside a SCOPED_TRACE to help
// interpret StrictMock "unexpected function call" failures, which don't
// include line numbers. Also dump the PageNode pointers for each failure to
// help interpret the arguments of unexpected functions.
SCOPED_TRACE(::testing::Message()
<< "page1: " << page1.get() << ", page2: " << page2.get()
<< ", page3: " << page3.get());
NavigateToUrl(page1.get(), kOptOutUrl1);
NavigateToUrl(page2.get(), kOptOutUrl2);
NavigateToUrl(page3.get(), kOptOutUrl2);
// Only page1's URL is opted out.
{
SCOPED_TRACE("add freeze votes");
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page1.get())).Times(0);
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
policy()->AddFreezeVote(page1.get());
policy()->AddFreezeVote(page2.get());
policy()->AddFreezeVote(page3.get());
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page1.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kOptedOut));
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
}
// Change which URL is opted out. Notify kBrowsingContext1 of the change.
{
SCOPED_TRACE("change opted out URL");
EXPECT_CALL(*freezer(), UnfreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page1.get()));
opt_out_checker_->SetOptedOutUrl(kOptOutUrl2, {kBrowsingContext1});
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page1.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kOptedOut));
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
}
// Now notify kBrowsingContext2.
{
SCOPED_TRACE("notify other context");
EXPECT_CALL(*freezer(), UnfreezePageNode(page3.get()));
opt_out_checker_->SetOptedOutUrl(kOptOutUrl2, {kBrowsingContext2});
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page1.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kOptedOut));
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting,
ElementsAre(CannotFreezeReason::kOptedOut));
}
// Remove the opt out and notify both contexts. Every page should freeze.
{
SCOPED_TRACE("remove opt outs");
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page2.get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page3.get()));
opt_out_checker_->SetOptedOutUrl("",
{kBrowsingContext1, kBrowsingContext2});
VerifyFreezerExpectations();
ExpectCannotFreezeReasons(page1.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page2.get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(page3.get(), FreezingType::kVoting, IsEmpty());
}
}
namespace {
class FreezingPolicyInfiniteTabsTest
: public FreezingPolicyTest_BaseWithNoPage {
protected:
FreezingPolicyInfiniteTabsTest() = default;
void OnGraphCreated(GraphImpl* graph) override {
FreezingPolicyTest_BaseWithNoPage::OnGraphCreated(graph);
// Start the test outside of the periodic unfreeze period.
AdvanceToAlignedTime(base::Minutes(1));
AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
// Create "num protected tabs" hidden pages. All pages have their own
// browsing instance, which is not equal to `kBrowsingInstance(A|B|C)`.
for (int i = 0; i < features::kInfiniteTabsFreezing_NumProtectedTabs.Get();
++i) {
auto [page, frame] =
CreatePageAndFrameWithBrowsingInstanceId(content::BrowsingInstanceId(
kBrowsingInstanceC.GetUnsafeValue() + i + 1));
ASSERT_FALSE(page->IsVisible());
pages_.push_back(std::move(page));
frames_.push_back(std::move(frame));
AdvanceClock(base::Milliseconds(1));
}
}
// Advances the clock to a time aligned on `interval`.
void AdvanceToAlignedTime(base::TimeDelta interval) {
const base::TimeTicks now = base::TimeTicks::Now();
const base::TimeTicks next_aligned_time =
now.SnappedToNextTick(base::TimeTicks(), interval);
AdvanceClock(next_aligned_time - now);
}
std::vector<TestNodeWrapper<PageNodeImpl>> pages_;
std::vector<TestNodeWrapper<FrameNodeImpl>> frames_;
private:
base::test::ScopedFeatureList feature_list_{features::kInfiniteTabsFreezing};
};
} // namespace
// Verify that under "Infinite Tabs Freezing", tabs are frozen if not in the
// list of most recently used.
TEST_F(FreezingPolicyInfiniteTabsTest, MostRecentlyUsed) {
// Create a new page, which takes the spot of the `pages_[0]` in the list
// of most recently used pages. `pages_[0]` should be frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
auto [page5, frame5] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
AdvanceClock(base::Milliseconds(1));
VerifyFreezerExpectations();
// Create a new page, which takes the spot of `pages_[1]` in the list
// of most recently used pages. `pages_[1]` should be frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
auto [page6, frame6] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
AdvanceClock(base::Milliseconds(1));
VerifyFreezerExpectations();
// Make `pages_[1]` visible. It should take the spot of `pages_[2]` in the
// list of most recently used pages. `pages_[2]` should be frozen.
EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[1].get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[2].get()));
pages_[1]->SetIsVisible(true);
VerifyFreezerExpectations();
// Make `pages_[2]` visible. It should take the spot of `pages_[3]]` in the
// list of most recently used pages. `pages_[4]` should be frozen.
EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[2].get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[3].get()));
pages_[2]->SetIsVisible(true);
VerifyFreezerExpectations();
// Hide `pages_[3]` and `pages_[1]`. This should have no effect on freezing.
pages_[2]->SetIsVisible(false);
AdvanceClock(base::Milliseconds(1));
pages_[1]->SetIsVisible(false);
AdvanceClock(base::Milliseconds(1));
// Create new pages. At each page creation, the least recently used protected
// page should loose its protection and be frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[4].get()));
auto [page7, frame7] = CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId(100));
VerifyFreezerExpectations();
AdvanceClock(base::Milliseconds(1));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page5.get()));
auto [page8, frame8] = CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId(101));
VerifyFreezerExpectations();
AdvanceClock(base::Milliseconds(1));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(page6.get()));
auto [page9, frame9] = CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId(102));
VerifyFreezerExpectations();
AdvanceClock(base::Milliseconds(1));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[2].get()));
auto [page10, frame10] = CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId(103));
VerifyFreezerExpectations();
AdvanceClock(base::Milliseconds(1));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
auto [page11, frame11] = CreatePageAndFrameWithBrowsingInstanceId(
content::BrowsingInstanceId(104));
VerifyFreezerExpectations();
AdvanceClock(base::Milliseconds(1));
}
// Verify that under "Infinite Tabs Freezing", a tab created visible (as opposed
// to hidden, as in other tests) doesn't have
// `CannotFreezeReason::kMostRecentlyUsed`, but still counts towards the limit
// of most recently used tabs protected against "Infinite Tabs Freezing".
TEST_F(FreezingPolicyInfiniteTabsTest, InitiallyVisible) {
// Create a new page, which takes the spot of the `pages_[0]` in the list
// of most recently used pages. `pages_[0]` should be frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
auto page5 = CreateNode<PageNodeImpl>(
/*web_contents=*/nullptr, /*browsing_context_id=*/std::string(), GURL(),
PagePropertyFlags{PagePropertyFlag::kIsVisible});
EXPECT_TRUE(page5->IsVisible());
page5->SetType(PageType::kTab);
auto frame5 =
CreateFrameNodeAutoId(process_node(), page5.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceA);
VerifyFreezerExpectations();
}
// Verify that under "Infinite Tabs Freezing", when there are more visible tabs
// than the desired number of protected tabs and they subsequently become
// hidden, freezing happens (as opposed to all tabs being transferred to the
// most recently used list, exceeding the limit).
TEST_F(FreezingPolicyInfiniteTabsTest, ManyVisibleTabs) {
// Create more visible tabs than the limit of protected tabs. All existing
// tabs are frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[2].get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[3].get()));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[4].get()));
std::vector<TestNodeWrapper<PageNodeImpl>> more_pages;
std::vector<TestNodeWrapper<FrameNodeImpl>> more_frames;
for (int i = 0;
i < features::kInfiniteTabsFreezing_NumProtectedTabs.Get() * 2; ++i) {
more_pages.push_back(CreateNode<PageNodeImpl>(
/*web_contents=*/nullptr, /*browsing_context_id=*/std::string(), GURL(),
PagePropertyFlags{PagePropertyFlag::kIsVisible}));
more_pages.back()->SetType(PageType::kTab);
more_frames.push_back(CreateFrameNodeAutoId(
process_node(), more_pages.back().get(),
/* parent_frame_node=*/nullptr, content::BrowsingInstanceId(100 + i)));
}
VerifyFreezerExpectations();
// Hide half of tabs. They should be frozen immediately, not put in the list
// of most recently used, as there are already more visible tabs that the
// limit of protected tabs.
for (int i = 0; i < features::kInfiniteTabsFreezing_NumProtectedTabs.Get();
++i) {
EXPECT_CALL(*freezer(), MaybeFreezePageNode(more_pages[i].get()));
more_pages[i]->SetIsVisible(false);
VerifyFreezerExpectations();
}
}
// Verify that under "Infinite Tabs Freezing", a `CannotFreezeReason` that
// applies to all types of freezing is honored.
TEST_F(FreezingPolicyInfiniteTabsTest, UniversalCannotFreezeReason) {
// Create a new page, which takes the spot of the `pages_[0]` in the list
// of most recently used pages. `pages_[0]` should be frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
auto [page5, frame5] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
AdvanceClock(base::Milliseconds(1));
VerifyFreezerExpectations();
// Add a `CannotFreezeReason` to `pages_[0]`. It should be unfrozen.
EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
pages_[0]->SetIsHoldingWebLockForTesting(true);
VerifyFreezerExpectations();
// Add a `CannotFreezeReason` to `pages_[1]`. This has no effect since it's
// not frozen.
pages_[1]->SetIsHoldingWebLockForTesting(true);
// Create a new page, which takes the spot of `pages_[1]` in the list of most
// recently used pages. `pages_[1]` should not be frozen since it holds a Web
// Lock.
auto [page6, frame6] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceB);
AdvanceClock(base::Milliseconds(1));
// When `pages_[0]` and `pages_[1]` release their lock, they're both frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
pages_[0]->SetIsHoldingWebLockForTesting(false);
VerifyFreezerExpectations();
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[1].get()));
pages_[1]->SetIsHoldingWebLockForTesting(false);
}
// Verify that under "Infinite Tabs Freezing", frozen tabs are periodically
// unfrozen.
TEST_F(FreezingPolicyInfiniteTabsTest, PeriodicUnfreeze) {
// Advance to the beginning of the next periodic unfreeze period.
AdvanceToAlignedTime(base::Minutes(1));
// Create a new page. This should remove
// `CannotFreezeReason::kMostRecentlyUsed` from `pages_[0]`. However, it's not
// frozen yet since it's still in its periodic unfreeze period.
auto [page, frame] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
ASSERT_FALSE(page->IsVisible());
// Advance to the end of the periodic unfreeze period. `pages_[0]` should be
// frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
VerifyFreezerExpectations();
// Advance to the beginning of the next periodic unfreeze period. `pages_[0]`
// should be unfrozen.
EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeInterval.Get() -
features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
VerifyFreezerExpectations();
// Advance to the end of the periodic unfreeze period. `pages_[0]` should be
// frozen again.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
VerifyFreezerExpectations();
// Add a `CannotFreezeReason`. `pages_[0]` is unfrozen.
EXPECT_CALL(*freezer(), UnfreezePageNode(pages_[0].get()));
pages_[0]->SetUsesWebRTCForTesting(true);
VerifyFreezerExpectations();
// At the next periodic unfreeze period, `pages_[0]` remains unfrozen.
AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeInterval.Get() -
features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
// When the periodic unfreeze period ends, `pages_[0]` is not re-frozen.
AdvanceClock(features::kInfiniteTabsFreezing_UnfreezeDuration.Get());
// When the `CannotFreezeReason` is removed, `pages_[0]` is frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
pages_[0]->SetUsesWebRTCForTesting(false);
VerifyFreezerExpectations();
}
// Verify that a page with a freeze vote can be frozen even if it's in the list
// of most recently used tabs (this list only affects Infinite Tabs Freezing).
TEST_F(FreezingPolicyInfiniteTabsTest, InteractionWithVoting) {
EXPECT_EQ(policy()->GetCanFreezeDetails(pages_[0].get()).can_freeze,
freezing::CanFreeze::kVaries);
ExpectCannotFreezeReasons(pages_[0].get(), FreezingType::kVoting, IsEmpty());
ExpectCannotFreezeReasons(pages_[0].get(), FreezingType::kInfiniteTabs,
ElementsAre(CannotFreezeReason::kMostRecentlyUsed));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
policy()->AddFreezeVote(pages_[0].get());
VerifyFreezerExpectations();
}
// Verify that a page which is CPU-intensive in the background while Battery
// Saver is active can be frozen even if it's in the list of most recently
// used tabs (this list only affects Infinite Tabs Freezing).
TEST_F(FreezingPolicyInfiniteTabsTest, InteractionWithBatterySaver) {
base::test::ScopedFeatureList feature_list{features::kFreezingOnBatterySaver};
policy()->ToggleFreezingOnBatterySaverMode(true);
EXPECT_EQ(policy()->GetCanFreezeDetails(pages_[0].get()).can_freeze,
freezing::CanFreeze::kVaries);
ExpectCannotFreezeReasons(pages_[0].get(), FreezingType::kBatterySaver,
IsEmpty());
ExpectCannotFreezeReasons(pages_[0].get(), FreezingType::kInfiniteTabs,
ElementsAre(CannotFreezeReason::kMostRecentlyUsed));
const resource_attribution::OriginInBrowsingInstanceContext kPage0Context{
url::Origin(), frames_[0]->GetBrowsingInstanceId()};
ReportCumulativeCPUUsage(kPage0Context, base::Seconds(60));
AdvanceClock(base::Seconds(60));
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
ReportCumulativeCPUUsage(kPage0Context, base::Seconds(75));
VerifyFreezerExpectations();
}
// Verify that visible pages which are not tabs don't affect Infinite Tabs
// Freezing.
TEST_F(FreezingPolicyInfiniteTabsTest, NonTab) {
// Create a new page of type `kTab` which takes the spot of the `pages_[0]` in
// the list of most recently used pages. `pages_[0]` should be frozen.
EXPECT_CALL(*freezer(), MaybeFreezePageNode(pages_[0].get()));
auto [page5, frame5] =
CreatePageAndFrameWithBrowsingInstanceId(kBrowsingInstanceA);
EXPECT_EQ(page5->GetType(), PageType::kTab);
AdvanceClock(base::Milliseconds(1));
VerifyFreezerExpectations();
// Create a new page of type `kExtension`. Unlike the previous case, this
// should have no effect on freezing.
auto non_tab_page = CreateNode<PageNodeImpl>(
/*web_contents=*/nullptr, /* browsing_context_id=*/std::string(), GURL(),
PagePropertyFlags{PagePropertyFlag::kIsVisible});
non_tab_page->SetType(PageType::kExtension);
auto non_tab_frame =
CreateFrameNodeAutoId(process_node(), non_tab_page.get(),
/* parent_frame_node=*/nullptr, kBrowsingInstanceB);
}
} // namespace performance_manager