blob: e2c03496eb3a8af223b420ad1c1bc4468c45bf99 [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 "chrome/browser/performance_manager/policies/freezing_policy.h"
#include <memory>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/time/time.h"
#include "chrome/browser/performance_manager/mechanisms/freezer.h"
#include "chrome/browser/performance_manager/policies/page_discarding_helper.h"
#include "components/performance_manager/freezing/freezing_vote_aggregator.h"
#include "components/performance_manager/public/decorators/page_live_state_decorator.h"
#include "components/performance_manager/public/mojom/lifecycle.mojom-shared.h"
namespace performance_manager::policies {
namespace {
bool IsPageNodeFrozen(const PageNode* page_node) {
return page_node->GetLifecycleState() ==
performance_manager::mojom::LifecycleState::kFrozen;
}
bool IsPageConnectedToUSBDevice(const PageNode* page_node) {
return PageLiveStateDecorator::Data::FromPageNode(page_node)
->IsConnectedToUSBDevice();
}
bool IsPageConnectedToBluetoothDevice(const PageNode* page_node) {
return PageLiveStateDecorator::Data::FromPageNode(page_node)
->IsConnectedToBluetoothDevice();
}
bool IsPageCapturingVideo(const PageNode* page_node) {
return PageLiveStateDecorator::Data::FromPageNode(page_node)
->IsCapturingVideo();
}
bool IsPageCapturingAudio(const PageNode* page_node) {
return PageLiveStateDecorator::Data::FromPageNode(page_node)
->IsCapturingAudio();
}
bool IsPageBeingMirrored(const PageNode* page_node) {
return PageLiveStateDecorator::Data::FromPageNode(page_node)
->IsBeingMirrored();
}
bool IsPageCapturingWindow(const PageNode* page_node) {
return PageLiveStateDecorator::Data::FromPageNode(page_node)
->IsCapturingWindow();
}
bool IsPageCapturingDisplay(const PageNode* page_node) {
return PageLiveStateDecorator::Data::FromPageNode(page_node)
->IsCapturingDisplay();
}
} // namespace
FreezingPolicy::FreezingPolicy()
: freezer_(std::make_unique<mechanism::Freezer>()) {}
FreezingPolicy::~FreezingPolicy() = default;
void FreezingPolicy::OnBeforeGraphDestroyed(Graph* graph) {
graph->RemovePageNodeObserver(this);
graph->RemoveGraphObserver(this);
// Clean up voting channels here as it must be done before the aggregator is
// torn down, which may happen before our OnTakenFromGraph() would be called.
for (int i = 0; i < CannotFreezeReason::kCount; ++i) {
voting_channels_[i].Reset();
}
}
void FreezingPolicy::OnPassedToGraph(Graph* graph) {
for (int i = 0; i < CannotFreezeReason::kCount; ++i) {
voting_channels_[i] =
graph->GetRegisteredObjectAs<freezing::FreezingVoteAggregator>()
->GetVotingChannel();
}
graph->AddGraphObserver(this);
graph->AddPageNodeObserver(this);
}
void FreezingPolicy::OnPageNodeAdded(const PageNode* page_node) {
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node)->AddObserver(
this);
if (page_node->IsAudible()) {
SubmitNegativeFreezingVote(page_node, kAudible);
}
DCHECK(!page_node->IsHoldingWebLock());
DCHECK(!page_node->IsHoldingIndexedDBLock());
DCHECK(!IsPageConnectedToUSBDevice(page_node));
DCHECK(!IsPageConnectedToBluetoothDevice(page_node));
DCHECK(!IsPageCapturingVideo(page_node));
DCHECK(!IsPageCapturingAudio(page_node));
DCHECK(!IsPageBeingMirrored(page_node));
DCHECK(!IsPageCapturingWindow(page_node));
DCHECK(!IsPageCapturingDisplay(page_node));
}
void FreezingPolicy::OnBeforePageNodeRemoved(const PageNode* page_node) {
page_node_being_removed_ = page_node;
if (auto iter = page_nodes_recently_audible_.find(page_node);
iter != page_nodes_recently_audible_.end()) {
// This will remove |page_node| from |page_nodes_recently_audible_|.
iter->second->FireNow();
}
PageLiveStateDecorator::Data::GetOrCreateForPageNode(page_node)
->RemoveObserver(this);
if (page_node->IsAudible()) {
InvalidateNegativeFreezingVote(page_node, kAudible);
}
if (page_node->IsHoldingWebLock()) {
InvalidateNegativeFreezingVote(page_node, kHoldingWebLock);
}
if (page_node->IsHoldingIndexedDBLock()) {
InvalidateNegativeFreezingVote(page_node, kHoldingIndexedDBLock);
}
if (IsPageConnectedToUSBDevice(page_node)) {
InvalidateNegativeFreezingVote(page_node, kConnectedToUsbDevice);
}
if (IsPageConnectedToBluetoothDevice(page_node)) {
InvalidateNegativeFreezingVote(page_node, kConnectedToBluetoothDevice);
}
if (IsPageCapturingVideo(page_node)) {
InvalidateNegativeFreezingVote(page_node, kCapturingVideo);
}
if (IsPageCapturingAudio(page_node)) {
InvalidateNegativeFreezingVote(page_node, kCapturingAudio);
}
if (IsPageBeingMirrored(page_node)) {
InvalidateNegativeFreezingVote(page_node, kBeingMirrored);
}
if (IsPageCapturingWindow(page_node)) {
InvalidateNegativeFreezingVote(page_node, kCapturingWindow);
}
if (IsPageCapturingDisplay(page_node)) {
InvalidateNegativeFreezingVote(page_node, kCapturingDisplay);
}
page_node_being_removed_ = nullptr;
}
void FreezingPolicy::OnIsAudibleChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, page_node->IsAudible(),
CannotFreezeReason::kAudible);
// Gives the page a grace period after being audible to avoid freezing it
// during a short silence (e.g. between tracks in an audio player).
if (!page_node->IsAudible()) {
DCHECK(!base::Contains(page_nodes_recently_audible_, page_node));
SubmitNegativeFreezingVote(page_node, CannotFreezeReason::kRecentlyAudible);
base::OnceClosure remove_was_recently_audible_vote_after_timeout =
base::BindOnce(
[](FreezingPolicy* policy, const PageNode* page_node) {
policy->InvalidateNegativeFreezingVote(
page_node, CannotFreezeReason::kRecentlyAudible);
// The OneShotTimer class allows the deletion of the timer from
// the callback.
policy->page_nodes_recently_audible_.erase(page_node);
},
base::Unretained(this), page_node);
auto timer = std::make_unique<base::OneShotTimer>();
timer->Start(FROM_HERE, kTabAudioProtectionTime,
std::move(remove_was_recently_audible_vote_after_timeout));
page_nodes_recently_audible_.emplace(page_node, std::move(timer));
} else {
if (auto iter = page_nodes_recently_audible_.find(page_node);
iter != page_nodes_recently_audible_.end()) {
// This will remove |page_node| from |page_nodes_recently_audible_|.
iter->second->FireNow();
}
}
}
void FreezingPolicy::OnPageIsHoldingWebLockChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, page_node->IsHoldingWebLock(),
CannotFreezeReason::kHoldingWebLock);
}
void FreezingPolicy::OnPageIsHoldingIndexedDBLockChanged(
const PageNode* page_node) {
OnPropertyChanged(page_node, page_node->IsHoldingIndexedDBLock(),
CannotFreezeReason::kHoldingIndexedDBLock);
}
void FreezingPolicy::OnFreezingVoteChanged(
const PageNode* page_node,
std::optional<performance_manager::freezing::FreezingVote> previous_vote) {
if (page_node == page_node_being_removed_) {
return;
}
auto freezing_vote = page_node->GetFreezingVote();
// Unfreeze the page if the freezing vote becomes negative or invalid, and was
// previously positive.
if (!freezing_vote.has_value() ||
freezing_vote->value() == freezing::FreezingVoteValue::kCannotFreeze) {
if (previous_vote.has_value() &&
previous_vote->value() == freezing::FreezingVoteValue::kCanFreeze) {
// Don't check if the page is actually frozen before sending the unfreeze
// event as it's not guaranteed that the freezing state will be properly
// reflected in PerformanceManager before the vote gets invalidated (e.g.
// if the vote has a really short lifetime).
freezer_->UnfreezePageNode(page_node);
}
} else {
DCHECK_EQ(freezing::FreezingVoteValue::kCanFreeze, freezing_vote->value());
// Don't attempt to freeze a page if it's not fully loaded yet.
if (page_node->GetLoadingState() != PageNode::LoadingState::kLoadedIdle) {
return;
}
if (!IsPageNodeFrozen(page_node)) {
freezer_->MaybeFreezePageNode(page_node);
}
}
}
void FreezingPolicy::OnLoadingStateChanged(
const PageNode* page_node,
PageNode::LoadingState previous_state) {
if (page_node->GetLoadingState() != PageNode::LoadingState::kLoadedIdle) {
return;
}
auto freezing_vote = page_node->GetFreezingVote();
if (freezing_vote.has_value() &&
freezing_vote->value() == freezing::FreezingVoteValue::kCanFreeze) {
freezer_->MaybeFreezePageNode(page_node);
}
}
void FreezingPolicy::OnIsConnectedToUSBDeviceChanged(
const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageConnectedToUSBDevice(page_node),
CannotFreezeReason::kConnectedToUsbDevice);
}
void FreezingPolicy::OnIsConnectedToBluetoothDeviceChanged(
const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageConnectedToBluetoothDevice(page_node),
CannotFreezeReason::kConnectedToBluetoothDevice);
}
void FreezingPolicy::OnIsCapturingVideoChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageCapturingVideo(page_node),
CannotFreezeReason::kCapturingVideo);
}
void FreezingPolicy::OnIsCapturingAudioChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageCapturingAudio(page_node),
CannotFreezeReason::kCapturingAudio);
}
void FreezingPolicy::OnIsBeingMirroredChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageBeingMirrored(page_node),
CannotFreezeReason::kBeingMirrored);
}
void FreezingPolicy::OnIsCapturingWindowChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageCapturingWindow(page_node),
CannotFreezeReason::kCapturingWindow);
}
void FreezingPolicy::OnIsCapturingDisplayChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageCapturingDisplay(page_node),
CannotFreezeReason::kCapturingDisplay);
}
void FreezingPolicy::OnPropertyChanged(const PageNode* page_node,
bool submit_vote,
CannotFreezeReason reason) {
if (submit_vote) {
SubmitNegativeFreezingVote(page_node, reason);
} else {
InvalidateNegativeFreezingVote(page_node, reason);
}
}
// static
const char* FreezingPolicy::CannotFreezeReasonToString(
CannotFreezeReason reason) {
switch (reason) {
case CannotFreezeReason::kAudible:
return "Page is audible";
case CannotFreezeReason::kRecentlyAudible:
return "Page was recently audible";
case CannotFreezeReason::kHoldingWebLock:
return "Page is holding a Web Lock";
case CannotFreezeReason::kHoldingIndexedDBLock:
return "Page is holding an IndexedDB lock";
case CannotFreezeReason::kConnectedToUsbDevice:
return "Page is connected to a USB device";
case CannotFreezeReason::kConnectedToBluetoothDevice:
return "Page is connected to a Bluetooth device";
case CannotFreezeReason::kCapturingVideo:
return "Page is capturing video";
case CannotFreezeReason::kCapturingAudio:
return "Page is capturing audio";
case CannotFreezeReason::kBeingMirrored:
return "Page is being mirrored";
case CannotFreezeReason::kCapturingWindow:
return "Page is capturing window";
case CannotFreezeReason::kCapturingDisplay:
return "Page is capturing display";
case CannotFreezeReason::kCount:
NOTREACHED();
return "";
}
}
void FreezingPolicy::SubmitNegativeFreezingVote(const PageNode* page_node,
CannotFreezeReason reason) {
freezing::FreezingVote vote(freezing::FreezingVoteValue::kCannotFreeze,
CannotFreezeReasonToString(reason));
voting_channels_[reason].SubmitVote(page_node, vote);
}
void FreezingPolicy::InvalidateNegativeFreezingVote(const PageNode* page_node,
CannotFreezeReason reason) {
voting_channels_[reason].InvalidateVote(page_node);
}
} // namespace performance_manager::policies