blob: 846a283b24da2f1ac20a81a1f26c469ff7221eea [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/page_discarding_helper.h"
#include <memory>
#include <utility>
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequence_checker.h"
#include "base/strings/string_piece.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/decorators/page_live_state_decorator.h"
#include "components/performance_manager/public/decorators/site_data_recorder.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/node_data_describer_util.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/graph/process_node.h"
#include "components/performance_manager/public/persistence/site_data/site_data_reader.h"
#include "components/url_matcher/url_matcher.h"
#include "components/url_matcher/url_util.h"
#include "url/gurl.h"
namespace performance_manager {
namespace policies {
namespace {
// 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 CanDiscard().
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";
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>()),
site_data_reader_callback_(
base::BindRepeating(&SiteDataRecorder::Data::GetReaderForPageNode)) {}
PageDiscardingHelper::~PageDiscardingHelper() = default;
void PageDiscardingHelper::DiscardAPage(
base::OnceCallback<void(bool)> post_discard_cb,
DiscardReason discard_reason,
base::TimeDelta minimum_time_in_background) {
DiscardMultiplePages(absl::nullopt, false, std::move(post_discard_cb),
discard_reason, minimum_time_in_background);
}
void PageDiscardingHelper::DiscardMultiplePages(
absl::optional<uint64_t> reclaim_target_kb,
bool discard_protected_tabs,
base::OnceCallback<void(bool)> post_discard_cb,
DiscardReason discard_reason,
base::TimeDelta minimum_time_in_background) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LOG(WARNING) << "Discarding multiple pages with target (kb): "
<< (reclaim_target_kb ? *reclaim_target_kb : 0);
// 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) {
CanDiscardResult can_discard_result =
CanDiscard(page_node, discard_reason, minimum_time_in_background);
if (can_discard_result == CanDiscardResult::kMarked) {
continue;
}
bool is_protected = (can_discard_result == CanDiscardResult::kProtected);
if (!discard_protected_tabs && is_protected) {
continue;
}
candidates.emplace_back(page_node, false, 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
// DiscardMultiplePages and PostDiscardAttemptCallback.
if (candidates.empty()) {
return;
}
std::vector<const PageNode*> discard_attempts;
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;
}
}
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());
LOG(WARNING) << "Discarding " << discard_attempts.size() << " pages";
page_discarder_->DiscardPageNodes(
discard_attempts, discard_reason,
base::BindOnce(&PageDiscardingHelper::PostDiscardAttemptCallback,
weak_factory_.GetWeakPtr(), reclaim_target_kb,
discard_protected_tabs, std::move(split_callback.second),
discard_reason, minimum_time_in_background));
}
void PageDiscardingHelper::ImmediatelyDiscardSpecificPage(
const PageNode* page_node,
DiscardReason discard_reason,
base::OnceCallback<void(bool)> post_discard_cb) {
// Pass 0 TimeDelta to bypass the minimum time in background check.
if (CanDiscard(page_node, discard_reason,
/*minimum_time_in_background=*/base::TimeDelta()) ==
CanDiscardResult::kEligible) {
page_discarder_->DiscardPageNodes({page_node}, discard_reason,
std::move(post_discard_cb));
} else {
std::move(post_discard_cb).Run(false);
}
}
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::SetNoDiscardPatternsForProfile(
const std::string& browser_context_id,
const std::vector<std::string>& patterns) {
std::unique_ptr<url_matcher::URLMatcher>& entry =
profiles_no_discard_patterns_[browser_context_id];
entry = std::make_unique<url_matcher::URLMatcher>();
url_matcher::util::AddAllowFilters(entry.get(), patterns);
}
void PageDiscardingHelper::ClearNoDiscardPatternsForProfile(
const std::string& browser_context_id) {
profiles_no_discard_patterns_.erase(browser_context_id);
}
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::SetSiteDataReaderCallbackForTesting(
SiteDataReaderCallback callback) {
site_data_reader_callback_ = callback;
}
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;
}
PageDiscardingHelper::CanDiscardResult PageDiscardingHelper::CanDiscard(
const PageNode* page_node,
DiscardReason discard_reason,
base::TimeDelta minimum_time_in_background) const {
if (DiscardAttemptMarker::Get(PageNodeImpl::FromNode(page_node))) {
return CanDiscardResult::kMarked;
}
bool is_proactive;
switch (discard_reason) {
case DiscardReason::EXTERNAL:
// Always allow discards from external sources like extensions.
return CanDiscardResult::kEligible;
case DiscardReason::URGENT:
is_proactive = false;
break;
case DiscardReason::PROACTIVE:
is_proactive = true;
break;
}
if (page_node->IsVisible()) {
return CanDiscardResult::kProtected;
}
if (page_node->IsAudible()) {
return CanDiscardResult::kProtected;
}
// Don't discard tabs that have recently played audio.
// This is used instead of SiteDataReader::UsesAudioInBackground(), which
// returns whether the site has used audio in the background in previous
// sessions, to avoid protecting tabs like media players that are allowed to
// play audio in the background but aren't currently being used.
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 CanDiscardResult::kProtected;
}
}
if (page_node->GetTimeSinceLastVisibilityChange() <
minimum_time_in_background) {
return CanDiscardResult::kProtected;
}
// 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 CanDiscardResult::kProtected;
}
// Don't discard tabs that don't have a main frame yet.
auto* main_frame = page_node->GetMainFrameNode();
if (!main_frame) {
return CanDiscardResult::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 CanDiscardResult::kProtected;
}
if (!main_frame->GetURL().is_valid() || main_frame->GetURL().is_empty()) {
return CanDiscardResult::kProtected;
}
// `HadUserEdits()` is currently a superset of `HadFormInteraction()` but
// that may change so check both here (the check is not expensive).
if (page_node->HadFormInteraction() || page_node->HadUserEdits()) {
return CanDiscardResult::kProtected;
}
// The enterprise policy to except pages from discarding applies to both
// proactive and urgent discards.
if (IsPageOptedOutOfDiscarding(page_node->GetBrowserContextID(),
main_frame->GetURL())) {
return CanDiscardResult::kProtected;
}
const auto* live_state_data =
PageLiveStateDecorator::Data::FromPageNode(page_node);
// The live state data won't be available if none of these events ever
// happened on the page.
if (live_state_data) {
// Don't discard the page if an extension is protecting it from discards.
if (!live_state_data->IsAutoDiscardable()) {
return CanDiscardResult::kProtected;
}
if (live_state_data->IsCapturingVideo()) {
return CanDiscardResult::kProtected;
}
if (live_state_data->IsCapturingAudio()) {
return CanDiscardResult::kProtected;
}
if (live_state_data->IsBeingMirrored()) {
return CanDiscardResult::kProtected;
}
if (live_state_data->IsCapturingWindow()) {
return CanDiscardResult::kProtected;
}
if (live_state_data->IsCapturingDisplay()) {
return CanDiscardResult::kProtected;
}
if (live_state_data->IsConnectedToBluetoothDevice()) {
return CanDiscardResult::kProtected;
}
if (live_state_data->IsConnectedToUSBDevice()) {
return CanDiscardResult::kProtected;
}
// Don't discard the active tab in any window, even if the window is not
// visible. Otherwise the user would see a blank page when the window
// becomes visible again, as the tab isn't reloaded until they click on it.
if (live_state_data->IsActiveTab()) {
return CanDiscardResult::kProtected;
}
// Pinning a tab is a strong signal the user wants to keep it.
if (live_state_data->IsPinnedTab()) {
return CanDiscardResult::kProtected;
}
// Don't discard pages with devtools attached, because when it's restored
// the devtools window won't come back. The user may be monitoring the page
// in the background with devtools.
if (live_state_data->IsDevToolsOpen()) {
return CanDiscardResult::kProtected;
}
if (is_proactive) {
if (live_state_data->IsContentSettingTypeAllowed(
ContentSettingsType::NOTIFICATIONS)) {
return CanDiscardResult::kProtected;
}
}
#if !BUILDFLAG(IS_CHROMEOS)
// TODO(sebmarchand): Skip this check if the Entreprise memory limit is set.
if (live_state_data->WasDiscarded()) {
return CanDiscardResult::kProtected;
}
// TODO(sebmarchand): Consider resetting the |WasDiscarded| value when the
// main frame document changes, also remove the DiscardAttemptMarker in
// this case.
#endif
}
if (is_proactive) {
const auto* site_data_reader = site_data_reader_callback_.Run(page_node);
if (site_data_reader) {
if (site_data_reader->UpdatesFaviconInBackground() ==
SiteFeatureUsage::kSiteFeatureInUse) {
return CanDiscardResult::kProtected;
}
if (site_data_reader->UpdatesTitleInBackground() ==
SiteFeatureUsage::kSiteFeatureInUse) {
return CanDiscardResult::kProtected;
}
}
}
// TODO(sebmarchand): Do not discard crashed tabs.
return CanDiscardResult::kEligible;
}
bool PageDiscardingHelper::IsPageOptedOutOfDiscarding(
const std::string& browser_context_id,
const GURL& url) const {
auto it = profiles_no_discard_patterns_.find(browser_context_id);
// TODO(crbug.com/1308741): Change the CHECK to a DCHECK in Sept 2022, after
// verifying that there are no crash reports.
CHECK(it != profiles_no_discard_patterns_.end());
return !it->second->MatchURL(url).empty();
}
base::Value::Dict PageDiscardingHelper::DescribePageNodeData(
const PageNode* node) const {
auto can_discard = [this, node](DiscardReason discard_reason) {
switch (this->CanDiscard(node, discard_reason, base::TimeDelta())) {
case CanDiscardResult::kEligible:
return "eligible";
case CanDiscardResult::kProtected:
return "protected";
case CanDiscardResult::kMarked:
return "marked";
}
};
base::Value::Dict ret;
ret.Set("can_urgently_discard", can_discard(DiscardReason::URGENT));
ret.Set("can_proactively_discard", can_discard(DiscardReason::PROACTIVE));
auto it = last_change_to_non_audible_time_.find(node);
if (it != last_change_to_non_audible_time_.end()) {
ret.Set("non_audible_change_time", TimeDeltaFromNowToValue(it->second));
}
auto* main_frame = node->GetMainFrameNode();
if (main_frame) {
ret.Set("opted_out", IsPageOptedOutOfDiscarding(node->GetBrowserContextID(),
main_frame->GetURL()));
}
return ret;
}
void PageDiscardingHelper::PostDiscardAttemptCallback(
absl::optional<uint64_t> reclaim_target_kb,
bool discard_protected_tabs,
base::OnceCallback<void(bool)> post_discard_cb,
DiscardReason discard_reason,
base::TimeDelta minimum_time_in_background,
bool success) {
// When there is no discard candidate, DiscardMultiplePages returns
// early and PostDiscardAttemptCallback is not called.
if (!success) {
// DiscardAttemptMarker will force the retry to choose different pages.
DiscardMultiplePages(reclaim_target_kb, discard_protected_tabs,
std::move(post_discard_cb), discard_reason,
minimum_time_in_background);
return;
}
std::move(post_discard_cb).Run(true);
}
} // namespace policies
} // namespace performance_manager