blob: 2b7d2e893d3036bffe718eb439fa36a99ae3a843 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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/page_freezing_policy.h"
#include <memory>
#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/time/time.h"
#include "chrome/browser/performance_manager/mechanisms/page_freezer.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/freezing/freezing.h"
#include "components/performance_manager/public/mojom/lifecycle.mojom-shared.h"
namespace performance_manager {
namespace policies {
namespace {
constexpr base::TimeDelta kUnfreezeInterval = base::TimeDelta::FromMinutes(5);
constexpr base::TimeDelta kUnfreezeDuration = base::TimeDelta::FromSeconds(10);
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
PageFreezingPolicy::PageFreezingPolicy()
: page_freezer_(std::make_unique<mechanism::PageFreezer>()) {}
PageFreezingPolicy::~PageFreezingPolicy() = default;
// static
const base::TimeDelta PageFreezingPolicy::GetUnfreezeIntervalForTesting() {
return kUnfreezeInterval;
}
// static
const base::TimeDelta PageFreezingPolicy::GetUnfreezeDurationForTesting() {
return kUnfreezeDuration;
}
void PageFreezingPolicy::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 PageFreezingPolicy::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 PageFreezingPolicy::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 PageFreezingPolicy::OnBeforePageNodeRemoved(const PageNode* page_node) {
page_node_being_removed_ = page_node;
page_nodes_unfreeze_tasks_.erase(page_node);
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 PageFreezingPolicy::OnIsAudibleChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, page_node->IsAudible(),
CannotFreezeReason::kAudible);
}
void PageFreezingPolicy::OnPageIsHoldingWebLockChanged(
const PageNode* page_node) {
OnPropertyChanged(page_node, page_node->IsHoldingWebLock(),
CannotFreezeReason::kHoldingWebLock);
}
void PageFreezingPolicy::OnPageIsHoldingIndexedDBLockChanged(
const PageNode* page_node) {
OnPropertyChanged(page_node, page_node->IsHoldingIndexedDBLock(),
CannotFreezeReason::kHoldingIndexedDBLock);
}
void PageFreezingPolicy::OnFreezingVoteChanged(
const PageNode* page_node,
absl::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).
page_freezer_->UnfreezePageNode(page_node);
page_nodes_unfreeze_tasks_.erase(page_node);
}
} else {
DCHECK_EQ(freezing::FreezingVoteValue::kCanFreeze, freezing_vote->value());
DCHECK(!base::Contains(page_nodes_unfreeze_tasks_, page_node));
// 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)) {
page_freezer_->MaybeFreezePageNode(page_node);
}
}
}
void PageFreezingPolicy::OnLoadingStateChanged(const PageNode* page_node) {
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) {
page_freezer_->MaybeFreezePageNode(page_node);
}
}
void PageFreezingPolicy::OnPageLifecycleStateChanged(
const PageNode* page_node) {
if (page_node->GetLifecycleState() == PageNode::LifecycleState::kFrozen) {
// Once the page is frozen start a timer to automatically unfreeze it after
// a fixed interval of time.
DCHECK(!base::Contains(page_nodes_unfreeze_tasks_, page_node));
base::OnceClosure cb =
base::BindOnce(&PageFreezingPolicy::TemporarilyUnfreezePageNode,
base::Unretained(this), page_node);
auto timer = std::make_unique<base::OneShotTimer>();
timer->Start(FROM_HERE, kUnfreezeInterval, std::move(cb));
page_nodes_unfreeze_tasks_[page_node] = std::make_pair(
PageNodeUnfreezeAction::kTemporaryUnfreeze, std::move(timer));
} else {
// If |page_node| is in |page_nodes_to_unfreeze_| it means that the tab
// lifecycle state has changed because of something external to this class
// (e.g. a frozen tab became visible). The unfreeze task should be
// cancelled.
auto iter = page_nodes_unfreeze_tasks_.find(page_node);
if (iter != page_nodes_unfreeze_tasks_.end() &&
iter->second.first == PageNodeUnfreezeAction::kTemporaryUnfreeze) {
page_nodes_unfreeze_tasks_.erase(iter);
}
}
}
void PageFreezingPolicy::OnIsConnectedToUSBDeviceChanged(
const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageConnectedToUSBDevice(page_node),
CannotFreezeReason::kConnectedToUsbDevice);
}
void PageFreezingPolicy::OnIsConnectedToBluetoothDeviceChanged(
const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageConnectedToBluetoothDevice(page_node),
CannotFreezeReason::kConnectedToBluetoothDevice);
}
void PageFreezingPolicy::OnIsCapturingVideoChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageCapturingVideo(page_node),
CannotFreezeReason::kCapturingVideo);
}
void PageFreezingPolicy::OnIsCapturingAudioChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageCapturingAudio(page_node),
CannotFreezeReason::kCapturingAudio);
}
void PageFreezingPolicy::OnIsBeingMirroredChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageBeingMirrored(page_node),
CannotFreezeReason::kBeingMirrored);
}
void PageFreezingPolicy::OnIsCapturingWindowChanged(const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageCapturingWindow(page_node),
CannotFreezeReason::kCapturingWindow);
}
void PageFreezingPolicy::OnIsCapturingDisplayChanged(
const PageNode* page_node) {
OnPropertyChanged(page_node, IsPageCapturingDisplay(page_node),
CannotFreezeReason::kCapturingDisplay);
}
void PageFreezingPolicy::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* PageFreezingPolicy::CannotFreezeReasonToString(
CannotFreezeReason reason) {
switch (reason) {
case CannotFreezeReason::kAudible:
return "Page is 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 PageFreezingPolicy::SubmitNegativeFreezingVote(const PageNode* page_node,
CannotFreezeReason reason) {
freezing::FreezingVote vote(freezing::FreezingVoteValue::kCannotFreeze,
CannotFreezeReasonToString(reason));
voting_channels_[reason].SubmitVote(page_node, vote);
}
void PageFreezingPolicy::InvalidateNegativeFreezingVote(
const PageNode* page_node,
CannotFreezeReason reason) {
voting_channels_[reason].InvalidateVote(page_node);
}
void PageFreezingPolicy::TemporarilyUnfreezePageNode(
const PageNode* page_node) {
// Update the task type to |kRefreeze| before sending the unfreeze signal to
// avoid |page_nodes_unfreeze_tasks_[page_node]| from being deleted in
// unittests where OnPageLifecycleStateChanged is called synchronously.
auto& refreeze_task = page_nodes_unfreeze_tasks_[page_node];
refreeze_task.first = PageNodeUnfreezeAction::kRefreeze;
// Unfreeze |page_node| and schedule the refreeze.
page_freezer_->UnfreezePageNode(page_node);
base::OnceClosure cb =
base::BindOnce(&PageFreezingPolicy::FreezePageNodeAfterTemporaryUnfreeze,
base::Unretained(this), page_node);
refreeze_task.second->Start(FROM_HERE, kUnfreezeDuration, std::move(cb));
}
void PageFreezingPolicy::FreezePageNodeAfterTemporaryUnfreeze(
const PageNode* page_node) {
page_nodes_unfreeze_tasks_.erase(page_node);
// Try to refreeze |page_node|. If this succeeds this will cause a lifecycle
// state change and schedule the next unfreeze task.
page_freezer_->MaybeFreezePageNode(page_node);
}
} // namespace policies
} // namespace performance_manager