blob: a9eae60994f2e7c8e027473e69db204d1769a7da [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/preloading/preloading_decider.h"
#include "base/check_op.h"
#include "base/containers/enum_set.h"
#include "base/strings/string_split.h"
#include "content/browser/preloading/prefetch/no_vary_search_helper.h"
#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include "content/browser/preloading/prefetch/prefetch_params.h"
#include "content/browser/preloading/preloading.h"
#include "content/browser/preloading/prerenderer_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/preloading.h"
#include "content/public/browser/weak_document_ptr.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/preloading/anchor_element_interaction_host.mojom.h"
namespace content {
namespace {
using EagernessSet =
base::EnumSet<blink::mojom::SpeculationEagerness,
blink::mojom::SpeculationEagerness::kMinValue,
blink::mojom::SpeculationEagerness::kMaxValue>;
EagernessSet EagernessSetFromFeatureParam(base::StringPiece value) {
EagernessSet set;
for (base::StringPiece piece : base::SplitStringPiece(
value, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
if (piece == "conservative") {
set.Put(blink::mojom::SpeculationEagerness::kConservative);
} else if (piece == "moderate") {
set.Put(blink::mojom::SpeculationEagerness::kModerate);
}
}
return set;
}
void PrefetchEvictionCallback(WeakDocumentPtr document, const GURL& url) {
PreloadingDecider* preloading_decider =
PreloadingDecider::GetForCurrentDocument(
document.AsRenderFrameHostIfValid());
if (preloading_decider) {
preloading_decider->OnPrefetchEvicted(url);
}
}
} // namespace
struct PreloadingDecider::BehaviorConfig {
BehaviorConfig() {
static const base::FeatureParam<std::string> kPointerDownEagerness{
&blink::features::kSpeculationRulesPointerDownHeuristics,
"pointer_down_eagerness", "conservative,moderate"};
pointer_down_eagerness =
EagernessSetFromFeatureParam(kPointerDownEagerness.Get());
static const base::FeatureParam<std::string> kPointerHoverEagerness{
&blink::features::kSpeculationRulesPointerHoverHeuristics,
"pointer_hover_eagerness", "moderate"};
pointer_hover_eagerness =
EagernessSetFromFeatureParam(kPointerHoverEagerness.Get());
}
EagernessSet EagernessSetForPredictor(
const PreloadingPredictor& predictor) const {
if (predictor.ukm_value() ==
preloading_predictor::kUrlPointerDownOnAnchor.ukm_value()) {
return pointer_down_eagerness;
} else if (predictor.ukm_value() ==
preloading_predictor::kUrlPointerHoverOnAnchor.ukm_value()) {
return pointer_hover_eagerness;
} else {
DLOG(WARNING) << "unexpected predictor " << predictor.name() << "/"
<< predictor.ukm_value();
return {};
}
}
EagernessSet pointer_down_eagerness;
EagernessSet pointer_hover_eagerness;
};
DOCUMENT_USER_DATA_KEY_IMPL(PreloadingDecider);
PreloadingDecider::PreloadingDecider(RenderFrameHost* rfh)
: DocumentUserData<PreloadingDecider>(rfh),
behavior_config_(std::make_unique<BehaviorConfig>()),
observer_for_testing_(nullptr),
preconnector_(render_frame_host()),
prefetcher_(render_frame_host()),
prerenderer_(std::make_unique<PrerendererImpl>(render_frame_host())) {
if (PrefetchContentRefactorIsEnabled() && PrefetchNewLimitsEnabled()) {
PrefetchDocumentManager::GetOrCreateForCurrentDocument(rfh)
->SetPrefetchEvictionCallback(base::BindRepeating(
&PrefetchEvictionCallback, rfh->GetWeakDocumentPtr()));
}
}
PreloadingDecider::~PreloadingDecider() = default;
void PreloadingDecider::AddPreloadingPrediction(const GURL& url,
PreloadingPredictor predictor) {
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
auto* preloading_data =
PreloadingData::GetOrCreateForWebContents(web_contents);
preloading_data->AddPreloadingPrediction(
predictor,
/*confidence=*/100, PreloadingData::GetSameURLMatcher(url));
}
void PreloadingDecider::OnPointerDown(const GURL& url) {
if (observer_for_testing_) {
observer_for_testing_->OnPointerDown(url);
}
// For pointer down link selection heuristics, we first call |MaybePrerender|
// to check whether it is safe to prerender the |url| and if so we request to
// prerender the |url| and return. Otherwise, by calling
// |ShouldWaitForPrerenderResult| we check whether there is an active
// prerender is in progress for |url| or will return if there is one. We then
// call |MaybePrefetch| to check whether prefetching the |url| is safe and if
// so we request the new prefetch and return. Otherwise, we call
// |ShouldWaitForPrefetchResult| to check whether there is an active prefetch
// in progress for the |url| and return if there is one. At last, we request a
// preconnect for the |url| if prefetching the |url| is not allowed or has
// failed before.
if (base::FeatureList::IsEnabled(
blink::features::kSpeculationRulesPointerDownHeuristics)) {
if (MaybePrerender(url, preloading_predictor::kUrlPointerDownOnAnchor)) {
AddPreloadingPrediction(url,
preloading_predictor::kUrlPointerDownOnAnchor);
return;
}
if (ShouldWaitForPrerenderResult(url))
return;
if (MaybePrefetch(url, preloading_predictor::kUrlPointerDownOnAnchor)) {
AddPreloadingPrediction(url,
preloading_predictor::kUrlPointerDownOnAnchor);
return;
}
// Ideally it is preferred to fallback to preconnect asynchronously if a
// prefetch attempt fails. We should revisit it later perhaps after having
// data showing it is worth doing so.
if (ShouldWaitForPrefetchResult(url))
return;
}
preconnector_.MaybePreconnect(url);
}
void PreloadingDecider::OnPointerHover(
const GURL& url,
blink::mojom::AnchorElementPointerDataPtr mouse_data) {
if (observer_for_testing_) {
observer_for_testing_->OnPointerHover(url);
}
if (base::FeatureList::IsEnabled(
blink::features::kSpeculationRulesPointerHoverHeuristics)) {
// First try to prerender the |url|, if not possible try to prefetch,
// otherwise try to preconnect to it.
if (MaybePrerender(url, preloading_predictor::kUrlPointerHoverOnAnchor)) {
AddPreloadingPrediction(url,
preloading_predictor::kUrlPointerHoverOnAnchor);
return;
}
if (ShouldWaitForPrerenderResult(url))
return;
if (MaybePrefetch(url, preloading_predictor::kUrlPointerHoverOnAnchor)) {
AddPreloadingPrediction(url,
preloading_predictor::kUrlPointerHoverOnAnchor);
return;
}
// ditto (async fallback)
if (ShouldWaitForPrefetchResult(url))
return;
preconnector_.MaybePreconnect(url);
}
}
void PreloadingDecider::AddStandbyCandidate(
const blink::mojom::SpeculationCandidatePtr& candidate) {
SpeculationCandidateKey key{candidate->url, candidate->action};
on_standby_candidates_[key].push_back(candidate.Clone());
GURL::Replacements replacements;
replacements.ClearRef();
replacements.ClearQuery();
if (candidate->no_vary_search_hint) {
SpeculationCandidateKey key_no_vary_search{
candidate->url.ReplaceComponents(replacements), candidate->action};
no_vary_search_hint_on_standby_candidates_[key_no_vary_search].insert(key);
}
}
void PreloadingDecider::RemoveStandbyCandidate(
const SpeculationCandidateKey key) {
GURL::Replacements replacements;
replacements.ClearRef();
replacements.ClearQuery();
SpeculationCandidateKey key_no_vary_search{
key.first.ReplaceComponents(replacements), key.second};
auto it = no_vary_search_hint_on_standby_candidates_.find(key_no_vary_search);
if (it != no_vary_search_hint_on_standby_candidates_.end()) {
it->second.erase(key);
if (it->second.empty()) {
no_vary_search_hint_on_standby_candidates_.erase(it);
}
}
on_standby_candidates_.erase(key);
}
void PreloadingDecider::ClearStandbyCandidates() {
no_vary_search_hint_on_standby_candidates_.clear();
on_standby_candidates_.clear();
}
void PreloadingDecider::UpdateSpeculationCandidates(
std::vector<blink::mojom::SpeculationCandidatePtr>& candidates) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (observer_for_testing_) {
observer_for_testing_->UpdateSpeculationCandidates(candidates);
}
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
auto* preloading_data =
PreloadingData::GetOrCreateForWebContents(web_contents);
preloading_data->SetIsNavigationInDomainCallback(
content_preloading_predictor::kSpeculationRules,
base::BindRepeating([](NavigationHandle* navigation_handle) -> bool {
return ui::PageTransitionIsWebTriggerable(
navigation_handle->GetPageTransition());
}));
if (base::FeatureList::IsEnabled(
blink::features::kSpeculationRulesPointerDownHeuristics)) {
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerDownOnAnchor,
base::BindRepeating([](NavigationHandle* navigation_handle) -> bool {
return ui::PageTransitionCoreTypeIs(
navigation_handle->GetPageTransition(),
ui::PageTransition::PAGE_TRANSITION_LINK) &&
ui::PageTransitionIsNewNavigation(
navigation_handle->GetPageTransition());
}));
}
if (base::FeatureList::IsEnabled(
blink::features::kSpeculationRulesPointerHoverHeuristics)) {
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerHoverOnAnchor,
base::BindRepeating([](NavigationHandle* navigation_handle) -> bool {
return ui::PageTransitionCoreTypeIs(
navigation_handle->GetPageTransition(),
ui::PageTransition::PAGE_TRANSITION_LINK) &&
ui::PageTransitionIsNewNavigation(
navigation_handle->GetPageTransition());
}));
}
// Here we look for all preloading candidates that are safe to perform, but
// their eagerness level is not high enough to perform without the trigger
// form link selection heuristics logic. We then remove them from the
// |candidates| list to prevent them from being initiated and will add them
// to |on_standby_candidates_| to be later considered by the heuristics logic.
auto should_mark_as_on_standby = [&](const auto& candidate) {
SpeculationCandidateKey key{candidate->url, candidate->action};
if (candidate->eagerness != blink::mojom::SpeculationEagerness::kEager &&
processed_candidates_.find(key) == processed_candidates_.end()) {
AddStandbyCandidate(candidate);
// TODO(isaboori) In current implementation, after calling prefetcher
// ProcessCandidatesForPrefetch, the prefetch_service starts checking the
// eligibility of the candidates and it will add any eligible candidates
// to the prefetch_queue_starts and starts prefetching them as soon as
// possible. For that reason here we remove on-standby candidates from the
// list. The prefetch service should be updated to let us pass the
// on-standby candidates to prefetch_service from here to let it check
// their eligibility right away without starting to prefetch them. It
// should also be possible to trigger the start of the prefetch based on
// heuristics.
return true;
}
processed_candidates_[key].push_back(candidate.Clone());
// TODO(crbug.com/1341019): Pass the action requested by speculation rules
// to PreloadingPrediction.
AddPreloadingPrediction(candidate->url, GetPredictorForSpeculationRules(
candidate->injection_world));
return false;
};
ClearStandbyCandidates();
// The lists of SpeculationCandidates cached in |processed_candidates_| will
// be stale now, so we clear the lists now and repopulate them below.
for (auto& entry : processed_candidates_) {
entry.second.clear();
}
// Move eager candidates to the front. This will avoid unnecessarily
// marking some non-eager candidates as on-standby when there is an eager
// candidate with the same URL that will be processed immediately.
base::ranges::stable_partition(candidates, [&](const auto& candidate) {
return candidate->eagerness == blink::mojom::SpeculationEagerness::kEager;
});
// The candidates remaining after this call will be all eager candidates,
// and all non-eager candidates whose (url, action) pair has already been
// processed.
base::EraseIf(candidates, should_mark_as_on_standby);
prefetcher_.ProcessCandidatesForPrefetch(candidates);
prerenderer_->ProcessCandidatesForPrerender(candidates);
}
bool PreloadingDecider::MaybePrefetch(const GURL& url,
const PreloadingPredictor& predictor) {
SpeculationCandidateKey key{url, blink::mojom::SpeculationAction::kPrefetch};
blink::mojom::SpeculationCandidatePtr candidate;
auto it = on_standby_candidates_.find(key);
if (it != on_standby_candidates_.end()) {
auto inner_it =
base::ranges::find_if(it->second, [&](const auto& candidate) {
return IsSuitableCandidate(candidate, predictor);
});
if (inner_it != it->second.end()) {
candidate = inner_it->Clone();
}
}
if (!candidate) {
// Check all URLs that might match via NVS hint.
// If there are multiple candidates that match prefetch the first one.
GURL::Replacements replacements;
replacements.ClearRef();
replacements.ClearQuery();
const GURL url_without_query_and_ref = url.ReplaceComponents(replacements);
auto nvs_it = no_vary_search_hint_on_standby_candidates_.find(
{url_without_query_and_ref,
blink::mojom::SpeculationAction::kPrefetch});
if (nvs_it == no_vary_search_hint_on_standby_candidates_.end()) {
return false;
}
for (const auto& standby_key : nvs_it->second) {
CHECK_EQ(standby_key.second, blink::mojom::SpeculationAction::kPrefetch);
const GURL& prefetch_url = standby_key.first;
// Every prefetch in this set might come back with NVS header of
// "params" and match. But we will consider only the first prefetch that
// has a No-Vary-Search hint that is matching.
auto standby_it = on_standby_candidates_.find(standby_key);
CHECK(standby_it != on_standby_candidates_.end());
auto inner_it = base::ranges::find_if(
standby_it->second, [&](const auto& on_standby_candidate) {
return on_standby_candidate->no_vary_search_hint &&
no_vary_search::ParseHttpNoVarySearchDataFromMojom(
on_standby_candidate->no_vary_search_hint)
.AreEquivalent(url, prefetch_url) &&
IsSuitableCandidate(on_standby_candidate, predictor);
});
if (inner_it != standby_it->second.end()) {
candidate = inner_it->Clone();
key = standby_key;
break;
}
}
}
if (!candidate) {
return false;
}
bool result = prefetcher_.MaybePrefetch(std::move(candidate));
// |key| might have changed since we first computed |it|.
it = on_standby_candidates_.find(key);
std::vector<blink::mojom::SpeculationCandidatePtr> candidates_for_key =
std::move(it->second);
RemoveStandbyCandidate(key);
processed_candidates_[std::move(key)] = std::move(candidates_for_key);
return result;
}
bool PreloadingDecider::ShouldWaitForPrefetchResult(const GURL& url) {
// TODO(liviutinta): Don't implement any No-Vary-Search hint matching here
// for now. It is not clear how to match `url` with a `processed_candidate`.
// Also, for a No-Vary-Search hint matched candidate we might end up not
// using the processed_candidate at all. We will revisit this later.
auto it = processed_candidates_.find(
{url, blink::mojom::SpeculationAction::kPrefetch});
if (it == processed_candidates_.end())
return false;
return !prefetcher_.IsPrefetchAttemptFailedOrDiscarded(url);
}
bool PreloadingDecider::MaybePrerender(const GURL& url,
const PreloadingPredictor& predictor) {
SpeculationCandidateKey key{url, blink::mojom::SpeculationAction::kPrerender};
auto it = on_standby_candidates_.find(key);
if (it == on_standby_candidates_.end()) {
return false;
}
auto inner_it = base::ranges::find_if(it->second, [&](const auto& candidate) {
return IsSuitableCandidate(candidate, predictor);
});
if (inner_it == it->second.end()) {
return false;
}
bool result = prerenderer_->MaybePrerender(inner_it->Clone());
std::vector<blink::mojom::SpeculationCandidatePtr> processed =
std::move(it->second);
RemoveStandbyCandidate(it->first);
processed_candidates_[std::move(key)] = std::move(processed);
return result;
}
bool PreloadingDecider::ShouldWaitForPrerenderResult(const GURL& url) {
auto it = processed_candidates_.find(
{url, blink::mojom::SpeculationAction::kPrerender});
if (it == processed_candidates_.end())
return false;
return prerenderer_->ShouldWaitForPrerenderResult(url);
}
bool PreloadingDecider::IsSuitableCandidate(
const blink::mojom::SpeculationCandidatePtr& candidate,
const PreloadingPredictor& predictor) const {
return behavior_config_->EagernessSetForPredictor(predictor).Has(
candidate->eagerness);
}
PreloadingDeciderObserverForTesting* PreloadingDecider::SetObserverForTesting(
PreloadingDeciderObserverForTesting* observer) {
return std::exchange(observer_for_testing_, observer);
}
std::unique_ptr<Prerenderer> PreloadingDecider::SetPrerendererForTesting(
std::unique_ptr<Prerenderer> prerenderer) {
return std::exchange(prerenderer_, std::move(prerenderer));
}
bool PreloadingDecider::IsOnStandByForTesting(
const GURL& url,
blink::mojom::SpeculationAction action) {
return on_standby_candidates_.find({url, action}) !=
on_standby_candidates_.end();
}
void PreloadingDecider::OnPrefetchEvicted(const GURL& url) {
SpeculationCandidateKey key{url, blink::mojom::SpeculationAction::kPrefetch};
auto it = processed_candidates_.find(key);
CHECK(it != processed_candidates_.end());
std::vector<blink::mojom::SpeculationCandidatePtr> candidates =
std::move(it->second);
processed_candidates_.erase(it);
for (const auto& candidate : candidates) {
if (candidate->eagerness != blink::mojom::SpeculationEagerness::kEager) {
AddStandbyCandidate(candidate);
}
// TODO(crbug.com/1445086): Add support for the case where |candidate|'s
// eagerness is kEager. In a scenario where the prefetch evicted is a
// non-eager prefetch, we could theoretically reprefetch using the eager
// candidate (and have it use the eager prefetch quota). In that scenario,
// perhaps not evicting and just making the prefetch use the eager limit
// might be a better option too. In the case where an eager prefetch is
// evicted, we don't want to immediately try and reprefetch the candidate;
// it would defeat the purpose of evicting in the first place, and due to a
// possible-rentrancy into PrefetchService::Prefetch(), it could cause us to
// exceed the limit.
}
}
} // namespace content