blob: 1652784b3fcc30d214d62c14a569c80a643affcf [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_discarding_helper.h"
#include <memory>
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequence_checker.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/performance_manager/mechanisms/page_discarder.h"
#include "chrome/browser/performance_manager/policies/policy_features.h"
#include "components/performance_manager/graph/node_attached_data_impl.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/public/graph/frame_node.h"
#include "components/performance_manager/public/graph/graph_operations.h"
#include "components/performance_manager/public/graph/node_data_describer_registry.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/graph/process_node.h"
#include "url/gurl.h"
namespace performance_manager {
namespace policies {
namespace {
#if !BUILDFLAG(IS_CHROMEOS)
// Time during which non visible pages are protected from urgent discarding
// (not on ChromeOS).
constexpr base::TimeDelta kNonVisiblePagesUrgentProtectionTime =
base::Minutes(10);
#endif
// Time during which a tab cannot be discarded after having played audio.
constexpr base::TimeDelta kTabAudioProtectionTime = base::Minutes(1);
// NodeAttachedData used to indicate that there's already been an attempt to
// discard a PageNode.
// TODO(sebmarchand): The only reason for a discard attempt to fail is if we try
// to discard a prerenderer, remove this once we can detect if a PageNode is a
// prerenderer in |CanUrgentlyDiscard|.
class DiscardAttemptMarker : public NodeAttachedDataImpl<DiscardAttemptMarker> {
public:
struct Traits : public NodeAttachedDataInMap<PageNodeImpl> {};
~DiscardAttemptMarker() override = default;
private:
friend class ::performance_manager::NodeAttachedDataImpl<
DiscardAttemptMarker>;
explicit DiscardAttemptMarker(const PageNodeImpl* page_node) {}
};
const char kDescriberName[] = "PageDiscardingHelper";
// Caches page node properties to facilitate sorting.
class PageNodeSortProxy {
public:
PageNodeSortProxy(const PageNode* page_node,
bool is_protected,
base::TimeDelta last_visible)
: page_node_(page_node),
is_protected_(is_protected),
last_visible_(last_visible) {}
const PageNode* page_node() { return page_node_; }
// Returns true if the rhs is more important.
bool operator<(const PageNodeSortProxy& rhs) const {
if (is_protected_ && !rhs.is_protected_)
return false;
if (!is_protected_ && rhs.is_protected_)
return true;
return last_visible_ > rhs.last_visible_;
}
private:
const PageNode* page_node_;
bool is_protected_;
// Delta between current time and last visibility change time.
base::TimeDelta last_visible_;
};
using NodeRssMap = base::flat_map<const PageNode*, uint64_t>;
// Returns the mapping from page_node to its RSS estimation.
NodeRssMap GetPageNodeRssEstimateKb(
const std::vector<PageNodeSortProxy>& candidates) {
// Initialize the result map in one shot for time complexity O(n * log(n)).
NodeRssMap::container_type result_container;
result_container.reserve(candidates.size());
for (auto candidate : candidates)
result_container.emplace_back(candidate.page_node(), 0);
NodeRssMap result(std::move(result_container));
// TODO(crbug/1240994): Use visitor to accumulate the result to avoid
// allocating extra lists of frame nodes behind the scenes.
// List all the processes associated with these page nodes.
base::flat_set<const ProcessNode*> process_nodes;
for (auto candidate : candidates) {
base::flat_set<const ProcessNode*> processes =
GraphOperations::GetAssociatedProcessNodes(candidate.page_node());
process_nodes.insert(processes.begin(), processes.end());
}
// Compute the resident set of each page by simply summing up the estimated
// resident set of all its frames.
for (const ProcessNode* process_node : process_nodes) {
base::flat_set<const FrameNode*> process_frames =
process_node->GetFrameNodes();
if (!process_frames.size())
continue;
// Get the resident set of the process and split it equally across its
// frames.
const uint64_t frame_rss_kb =
process_node->GetResidentSetKb() / process_frames.size();
for (const FrameNode* frame_node : process_frames) {
// Check if the frame belongs to a discardable page, if so update the
// resident set of the page.
auto iter = result.find(frame_node->GetPageNode());
if (iter == result.end())
continue;
iter->second += frame_rss_kb;
}
}
return result;
}
} // namespace
PageDiscardingHelper::PageDiscardingHelper()
: page_discarder_(std::make_unique<mechanism::PageDiscarder>()) {}
PageDiscardingHelper::~PageDiscardingHelper() = default;
void PageDiscardingHelper::UrgentlyDiscardAPage(
features::DiscardStrategy discard_strategy,
base::OnceCallback<void(bool)> post_discard_cb) {
UrgentlyDiscardMultiplePages(absl::nullopt, discard_strategy, false,
std::move(post_discard_cb));
}
void PageDiscardingHelper::UrgentlyDiscardMultiplePages(
absl::optional<uint64_t> reclaim_target_kb,
features::DiscardStrategy discard_strategy,
bool discard_protected_tabs,
base::OnceCallback<void(bool)> post_discard_cb) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Ensures running post_discard_cb on early return.
auto split_callback = base::SplitOnceCallback(std::move(post_discard_cb));
base::ScopedClosureRunner run_post_discard_cb_on_return(
base::BindOnce(std::move(split_callback.first), false));
std::vector<const PageNode*> page_nodes = graph_->GetAllPageNodes();
std::vector<PageNodeSortProxy> candidates;
for (const auto* page_node : page_nodes) {
CanUrgentlyDiscardResult can_discard_result = CanUrgentlyDiscard(page_node);
if (can_discard_result == CanUrgentlyDiscardResult::kMarked)
continue;
bool is_protected =
(can_discard_result == CanUrgentlyDiscardResult::kProtected);
if (!discard_protected_tabs && is_protected)
continue;
candidates.emplace_back(page_node, is_protected,
page_node->GetTimeSinceLastVisibilityChange());
}
// Sorts with ascending importance.
std::sort(candidates.begin(), candidates.end());
UMA_HISTOGRAM_COUNTS_100("Discarding.DiscardCandidatesCount",
candidates.size());
// Returns early when candidate is empty to avoid infinite loop in
// UrgentlyDiscardMultiplePages and PostDiscardAttemptCallback.
if (candidates.empty()) {
return;
}
std::vector<const PageNode*> discard_attempts;
if (discard_strategy == features::DiscardStrategy::LRU) {
if (reclaim_target_kb == absl::nullopt) {
const PageNode* oldest = candidates[0].page_node();
discard_attempts.emplace_back(oldest);
} else {
const uint64_t reclaim_target_kb_value = *reclaim_target_kb;
uint64_t total_reclaim_kb = 0;
NodeRssMap page_node_rss_kb = GetPageNodeRssEstimateKb(candidates);
for (auto& candidate : candidates) {
if (total_reclaim_kb >= reclaim_target_kb_value)
break;
const PageNode* node = candidate.page_node();
discard_attempts.emplace_back(node);
// The node RSS value is updated by ProcessMetricsDecorator
// periodically. The RSS value is 0 for nodes that have never been
// updated, estimate the RSS value to 80 MiB for these nodes. 80 MiB is
// the average Memory.Renderer.PrivateMemoryFootprint histogram value on
// Windows in August 2021.
total_reclaim_kb +=
(page_node_rss_kb[node]) ? page_node_rss_kb[node] : 80 * 1024;
}
}
} else if (discard_strategy == features::DiscardStrategy::BIGGEST_RSS) {
if (reclaim_target_kb == absl::nullopt) {
NodeRssMap page_node_rss_kb = GetPageNodeRssEstimateKb(candidates);
DCHECK(!page_node_rss_kb.empty());
const PageNode* oldest = candidates[0].page_node();
auto largest =
std::max_element(page_node_rss_kb.begin(), page_node_rss_kb.end(),
[](const std::pair<const PageNode*, uint64_t>& p1,
const std::pair<const PageNode*, uint64_t>& p2) {
return p1.second < p2.second;
});
// max_element should return a valid element unless the map is empty.
DCHECK(largest != page_node_rss_kb.end());
if (largest->second != 0) {
// Only report the memory usage metrics if we can compare them.
UMA_HISTOGRAM_COUNTS_1000("Discarding.LargestTabFootprint",
largest->second / 1024);
UMA_HISTOGRAM_COUNTS_1000("Discarding.OldestTabFootprint",
page_node_rss_kb[oldest] / 1024);
discard_attempts.emplace_back(largest->first);
} else {
// If RSS is 0 for every node, fallback to the oldest node.
discard_attempts.emplace_back(oldest);
}
} else {
NOTIMPLEMENTED() << "When DiscardStrategy is BIGGEST_RSS, multiple "
"discarding is not supported.";
return;
}
} else {
NOTIMPLEMENTED() << "Unknown discard strategy.";
}
if (discard_attempts.empty()) {
return;
}
// Adorns the PageNodes with a discard attempt marker to make sure that we
// don't try to discard it multiple times if it fails to be discarded. In
// practice this should only happen to prerenderers.
for (auto* attempt : discard_attempts) {
DiscardAttemptMarker::GetOrCreate(PageNodeImpl::FromNode(attempt));
}
// Got to the end successfully, don't call the early return callback.
run_post_discard_cb_on_return.ReplaceClosure(base::DoNothing());
page_discarder_->DiscardPageNodes(
discard_attempts,
base::BindOnce(&PageDiscardingHelper::PostDiscardAttemptCallback,
weak_factory_.GetWeakPtr(), reclaim_target_kb,
discard_strategy, discard_protected_tabs,
std::move(split_callback.second)));
}
void PageDiscardingHelper::OnBeforePageNodeRemoved(const PageNode* page_node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
last_change_to_non_audible_time_.erase(page_node);
}
void PageDiscardingHelper::OnIsAudibleChanged(const PageNode* page_node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!page_node->IsAudible())
last_change_to_non_audible_time_[page_node] = base::TimeTicks::Now();
}
void PageDiscardingHelper::SetMockDiscarderForTesting(
std::unique_ptr<mechanism::PageDiscarder> discarder) {
page_discarder_ = std::move(discarder);
}
// static
void PageDiscardingHelper::AddDiscardAttemptMarkerForTesting(
PageNode* page_node) {
DiscardAttemptMarker::GetOrCreate(PageNodeImpl::FromNode(page_node));
}
// static
void PageDiscardingHelper::RemovesDiscardAttemptMarkerForTesting(
PageNode* page_node) {
DiscardAttemptMarker::Destroy(PageNodeImpl::FromNode(page_node));
}
void PageDiscardingHelper::OnPassedToGraph(Graph* graph) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
graph_ = graph;
graph->AddPageNodeObserver(this);
graph->RegisterObject(this);
graph->GetNodeDataDescriberRegistry()->RegisterDescriber(this,
kDescriberName);
}
void PageDiscardingHelper::OnTakenFromGraph(Graph* graph) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
graph->GetNodeDataDescriberRegistry()->UnregisterDescriber(this);
graph->UnregisterObject(this);
graph->RemovePageNodeObserver(this);
graph_ = nullptr;
}
const PageLiveStateDecorator::Data*
PageDiscardingHelper::GetPageNodeLiveStateData(
const PageNode* page_node) const {
return PageLiveStateDecorator::Data::FromPageNode(page_node);
}
PageDiscardingHelper::CanUrgentlyDiscardResult
PageDiscardingHelper::CanUrgentlyDiscard(const PageNode* page_node) const {
if (DiscardAttemptMarker::Get(PageNodeImpl::FromNode(page_node)))
return CanUrgentlyDiscardResult::kMarked;
if (page_node->IsVisible())
return CanUrgentlyDiscardResult::kProtected;
if (page_node->IsAudible())
return CanUrgentlyDiscardResult::kProtected;
// Don't discard tabs that have recently played audio.
auto it = last_change_to_non_audible_time_.find(page_node);
if (it != last_change_to_non_audible_time_.end()) {
if (base::TimeTicks::Now() - it->second < kTabAudioProtectionTime)
return CanUrgentlyDiscardResult::kProtected;
}
#if !BUILDFLAG(IS_CHROMEOS)
if (page_node->GetTimeSinceLastVisibilityChange() <
kNonVisiblePagesUrgentProtectionTime) {
return CanUrgentlyDiscardResult::kProtected;
}
#endif
// Do not discard PDFs as they might contain entry that is not saved and they
// don't remember their scrolling positions. See crbug.com/547286 and
// crbug.com/65244.
if (page_node->GetContentsMimeType() == "application/pdf")
return CanUrgentlyDiscardResult::kProtected;
// Don't discard tabs that don't have a main frame yet.
auto* main_frame = page_node->GetMainFrameNode();
if (!main_frame)
return CanUrgentlyDiscardResult::kProtected;
// Only discard http(s) pages and internal pages to make sure that we don't
// discard extensions or other PageNode that don't correspond to a tab.
bool is_web_page_or_internal_page =
main_frame->GetURL().SchemeIsHTTPOrHTTPS() ||
main_frame->GetURL().SchemeIs("chrome");
if (!is_web_page_or_internal_page)
return CanUrgentlyDiscardResult::kProtected;
if (!main_frame->GetURL().is_valid() || main_frame->GetURL().is_empty())
return CanUrgentlyDiscardResult::kProtected;
const auto* live_state_data = GetPageNodeLiveStateData(page_node);
// The live state data won't be available if none of these events ever
// happened on the page.
if (live_state_data) {
if (!live_state_data->IsAutoDiscardable())
return CanUrgentlyDiscardResult::kProtected;
if (live_state_data->IsCapturingVideo())
return CanUrgentlyDiscardResult::kProtected;
if (live_state_data->IsCapturingAudio())
return CanUrgentlyDiscardResult::kProtected;
if (live_state_data->IsBeingMirrored())
return CanUrgentlyDiscardResult::kProtected;
if (live_state_data->IsCapturingWindow())
return CanUrgentlyDiscardResult::kProtected;
if (live_state_data->IsCapturingDisplay())
return CanUrgentlyDiscardResult::kProtected;
if (live_state_data->IsConnectedToBluetoothDevice())
return CanUrgentlyDiscardResult::kProtected;
if (live_state_data->IsConnectedToUSBDevice())
return CanUrgentlyDiscardResult::kProtected;
#if !BUILDFLAG(IS_CHROMEOS)
// TODO(sebmarchand): Skip this check if the Entreprise memory limit is set.
if (live_state_data->WasDiscarded())
return CanUrgentlyDiscardResult::kProtected;
// TODO(sebmarchand): Consider resetting the |WasDiscarded| value when the
// main frame document changes, also remove the DiscardAttemptMarker in
// this case.
#endif
}
if (page_node->HadFormInteraction())
return CanUrgentlyDiscardResult::kProtected;
// TODO(sebmarchand): Do not discard pages if they're connected to DevTools.
// TODO(sebmarchand): Do not discard crashed tabs.
// TODO(sebmarchand): Do not discard tabs that are the active ones in a tab
// strip.
// TODO(sebmarchand): Do not try to discard PageNode not attached to a tab
// strip.
return CanUrgentlyDiscardResult::kEligible;
}
base::Value PageDiscardingHelper::DescribePageNodeData(
const PageNode* node) const {
auto* data = DiscardAttemptMarker::Get(PageNodeImpl::FromNode(node));
if (data == nullptr)
return base::Value();
base::Value ret(base::Value::Type::DICTIONARY);
ret.SetKey("has_discard_attempt_marker", base::Value("true"));
return ret;
}
void PageDiscardingHelper::PostDiscardAttemptCallback(
absl::optional<uint64_t> reclaim_target_kb,
features::DiscardStrategy discard_strategy,
bool discard_protected_tabs,
base::OnceCallback<void(bool)> post_discard_cb,
bool success) {
// When there is no discard candidate, UrgentlyDiscardMultiplePages returns
// early and PostDiscardAttemptCallback is not called.
if (!success) {
// DiscardAttemptMarker will force the retry to choose different pages.
UrgentlyDiscardMultiplePages(reclaim_target_kb, discard_strategy,
discard_protected_tabs,
std::move(post_discard_cb));
return;
}
std::move(post_discard_cb).Run(true);
}
} // namespace policies
} // namespace performance_manager