blob: 487880200ab6755e035819abff3dd838c3cf952d [file] [log] [blame]
// Copyright 2023 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 <algorithm>
#include <cmath>
#include <string_view>
#include <vector>
#include "base/check_op.h"
#include "base/containers/enum_set.h"
#include "base/feature_list.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_split.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/devtools/devtools_preload_storage.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/preloading_confidence.h"
#include "content/browser/preloading/preloading_data_impl.h"
#include "content/browser/preloading/preloading_trigger_type_impl.h"
#include "content/browser/preloading/prerender/prerender_features.h"
#include "content/browser/preloading/prerenderer_impl.h"
#include "content/browser/preloading/speculation_rules/speculation_rules_util.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(std::string_view value) {
EagernessSet set;
for (std::string_view 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 OnPrefetchDestroyed(WeakDocumentPtr document, const GURL& url) {
PreloadingDecider* preloading_decider =
PreloadingDecider::GetForCurrentDocument(
document.AsRenderFrameHostIfValid());
if (preloading_decider) {
preloading_decider->OnPreloadDiscarded(
{url, blink::mojom::SpeculationAction::kPrefetch});
}
}
void OnPrerenderCanceled(WeakDocumentPtr document, const GURL& url) {
PreloadingDecider* preloading_decider =
PreloadingDecider::GetForCurrentDocument(
document.AsRenderFrameHostIfValid());
if (preloading_decider) {
preloading_decider->OnPreloadDiscarded(
{url, blink::mojom::SpeculationAction::kPrerender});
}
}
bool PredictionOccursInOtherWebContents(
const blink::mojom::SpeculationCandidate& candidate) {
return base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab) &&
candidate.action == blink::mojom::SpeculationAction::kPrerender &&
candidate.target_browsing_context_name_hint ==
blink::mojom::SpeculationTargetHint::kBlank;
}
} // namespace
class PreloadingDecider::BehaviorConfig {
public:
BehaviorConfig()
: ml_model_eagerness_{blink::mojom::SpeculationEagerness::kModerate},
ml_model_enacts_candidates_(
blink::features::kPreloadingModelEnactCandidates.Get()),
ml_model_prefetch_moderate_threshold_{std::clamp(
blink::features::kPreloadingModelPrefetchModerateThreshold.Get(),
0,
100)},
ml_model_prerender_moderate_threshold_{std::clamp(
blink::features::kPreloadingModelPrerenderModerateThreshold.Get(),
0,
100)} {
pointer_down_eagerness_ =
EagernessSet{blink::mojom::SpeculationEagerness::kConservative,
blink::mojom::SpeculationEagerness::kModerate};
pointer_hover_eagerness_ =
EagernessSet{blink::mojom::SpeculationEagerness::kModerate};
static const base::FeatureParam<std::string> kViewportHeuristicEagerness{
&blink::features::kPreloadingViewportHeuristics,
"viewport_heuristic_eagerness", "moderate"};
viewport_heuristic_eagerness_ =
EagernessSetFromFeatureParam(kViewportHeuristicEagerness.Get());
}
EagernessSet EagernessSetForPredictor(
const PreloadingPredictor& predictor) const {
if (predictor == preloading_predictor::kUrlPointerDownOnAnchor) {
return pointer_down_eagerness_;
} else if (predictor == preloading_predictor::kUrlPointerHoverOnAnchor) {
return pointer_hover_eagerness_;
} else if (predictor == preloading_predictor::kViewportHeuristic) {
return viewport_heuristic_eagerness_;
} else if (predictor ==
preloading_predictor::kPreloadingHeuristicsMLModel) {
return ml_model_eagerness_;
} else {
NOTREACHED() << "unexpected predictor " << predictor.name() << "/"
<< predictor.ukm_value();
}
}
PreloadingConfidence GetThreshold(
const PreloadingPredictor& predictor,
blink::mojom::SpeculationAction action) const {
if (predictor == preloading_predictor::kUrlPointerDownOnAnchor) {
return kNoThreshold;
} else if (predictor == preloading_predictor::kUrlPointerHoverOnAnchor) {
return kNoThreshold;
} else if (predictor == preloading_predictor::kViewportHeuristic) {
return kNoThreshold;
} else if (predictor ==
preloading_predictor::kPreloadingHeuristicsMLModel) {
switch (action) {
case blink::mojom::SpeculationAction::kPrefetch:
case blink::mojom::SpeculationAction::kPrefetchWithSubresources:
return ml_model_prefetch_moderate_threshold_;
case blink::mojom::SpeculationAction::kPrerender:
return ml_model_prerender_moderate_threshold_;
}
} else {
NOTREACHED() << "unexpected predictor " << predictor.name() << "/"
<< predictor.ukm_value();
}
}
bool ml_model_enacts_candidates() const {
return ml_model_enacts_candidates_;
}
private:
// Any confidence value is >= kNoThreshold, so the associated action will
// happen regardless of the confidence value.
static constexpr PreloadingConfidence kNoThreshold{0};
EagernessSet pointer_down_eagerness_;
EagernessSet pointer_hover_eagerness_;
EagernessSet viewport_heuristic_eagerness_;
const EagernessSet ml_model_eagerness_;
const bool ml_model_enacts_candidates_ = false;
const PreloadingConfidence ml_model_prefetch_moderate_threshold_{
kNoThreshold};
const PreloadingConfidence ml_model_prerender_moderate_threshold_{
kNoThreshold};
};
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())) {
PrefetchDocumentManager::GetOrCreateForCurrentDocument(rfh)
->SetPrefetchDestructionCallback(
base::BindRepeating(&OnPrefetchDestroyed, rfh->GetWeakDocumentPtr()));
prerenderer_->SetPrerenderCancellationCallback(
base::BindRepeating(&OnPrerenderCanceled, rfh->GetWeakDocumentPtr()));
// Forcibly create `DevToolsPreloadStorage` before we use it in
// `devtools_instrumentation`.
//
// For more details, see
// https://docs.google.com/document/d/1ZP7lYrtqZL9jC2xXieNY_UBMJL1sCrfmzTB8K6v4sD4/edit?resourcekey=0-fkbeQhkT3PhBb9FnnPgnZA&tab=t.e4x3d1nxzmy3#heading=h.4lvl0yr9vmh7
//
// We found that there is a case that
// `devtools_instrumentation::DidUpdatePrerenderStatus()` in the same stack
// that called `DocumentAssociatedData::dtor()`. The former needs an instance
// of `DevToolsPreloadStorage`. If we call
// `DevToolsPreloadStorage::GetOrCreateForCurrentDocument()` there, the call
// may try to create an instance, but it is forbidden as the holder
// `DocumentAssociatedData` is in dtor and will crash.
//
// To mitigate this crash, we'll call `GetOrCreateForCurrentDocument()` here
// and use `GetForCurrentDocument()` in
// `devtools_instrumentation::DidUpdatePrerenderStatus()`.
//
// This works because:
//
// - The issue happens only on speculation rules preload, not
// browser-initiated preloads. This is because browser-initiated preloads
// don't emit CDP events as they don't have an initiator document, thus
// don't use `DevToolsPreloadStorage`. This is guaranteed by
// `DevToolsPrerenderAttempt::SetFailureReason()`. Therefore, we do this
// workaround in `PreloadingDecider`, not the common layer.
// So, we can assume that the below
// `DevToolsPreloadStorage::GetOrCreateForCurrentDocument()` is called and
// an instance basically exists.
// - `SupportsUserData::ClearAllUserData()` (which is called from
// `DocumentAssociatedData::dtor()`) swaps user data with an empty map and
// then drops the swapped map at the end of scope, which calls each dtor.
// https://source.chromium.org/chromium/chromium/src/+/main:base/supports_user_data.cc;l=142;drc=5f14562c01775211a40ebc3056d0a773c3569008
// So, `DevToolsPreloadStorage::GetForCurrentDocument()` returns non null
// pointer iff the call is before `DocumentAssociatedData::dtor()` call. We
// can branch by the condition.
//
// Note that this is just a short-term fix. We are planning to fix the root
// cause.
//
// TODO(crbug.com/394631076): Fix the root cause and revert this.
DevToolsPreloadStorage::GetOrCreateForCurrentDocument(rfh);
}
PreloadingDecider::~PreloadingDecider() = default;
void PreloadingDecider::AddPreloadingPrediction(
const GURL& url,
PreloadingPredictor predictor,
PreloadingConfidence confidence) {
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
auto* preloading_data =
PreloadingDataImpl::GetOrCreateForWebContents(web_contents);
ukm::SourceId triggered_primary_page_source_id =
web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId();
preloading_data->AddPreloadingPrediction(
predictor, confidence, PreloadingData::GetSameURLMatcher(url),
triggered_primary_page_source_id);
}
void PreloadingDecider::OnPointerDown(const GURL& url) {
if (observer_for_testing_) {
observer_for_testing_->OnPointerDown(url);
}
MaybeEnactCandidate(url, preloading_predictor::kUrlPointerDownOnAnchor,
PreloadingConfidence{100},
/*fallback_to_preconnect=*/true);
}
void PreloadingDecider::OnPreloadingHeuristicsModelDone(const GURL& url,
float score) {
CHECK(base::FeatureList::IsEnabled(
blink::features::kPreloadingHeuristicsMLModel));
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
auto* preloading_data = static_cast<PreloadingDataImpl*>(
PreloadingData::GetOrCreateForWebContents(web_contents));
preloading_data->AddExperimentalPreloadingPrediction(
/*name=*/"OnPreloadingHeuristicsMLModel",
/*url_match_predicate=*/PreloadingData::GetSameURLMatcher(url),
/*score=*/score,
/*min_score=*/0.0,
/*max_score=*/1.0,
/*buckets=*/100);
if (!behavior_config_->ml_model_enacts_candidates()) {
return;
}
ml_model_available_ = true;
const PreloadingConfidence confidence{std::clamp(
base::saturated_cast<int>(std::nearbyint(score * 100.f)), 0, 100)};
MaybeEnactCandidate(url, preloading_predictor::kPreloadingHeuristicsMLModel,
confidence, /*fallback_to_preconnect=*/false);
}
void PreloadingDecider::OnPointerHover(
const GURL& url,
blink::mojom::AnchorElementPointerDataPtr mouse_data) {
if (observer_for_testing_) {
observer_for_testing_->OnPointerHover(url);
}
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
auto* preloading_data = static_cast<PreloadingDataImpl*>(
PreloadingData::GetOrCreateForWebContents(web_contents));
preloading_data->AddExperimentalPreloadingPrediction(
/*name=*/"OnPointerHoverWithMotionEstimator",
/*url_match_predicate=*/PreloadingData::GetSameURLMatcher(url),
/*score=*/std::clamp(mouse_data->mouse_velocity, 0.0, 500.0),
/*min_score=*/0,
/*max_score=*/500,
/*buckets=*/100);
// Preconnecting on hover events should not be done if the link is not safe
// to prefetch or prerender.
constexpr bool fallback_to_preconnect = false;
MaybeEnactCandidate(url, preloading_predictor::kUrlPointerHoverOnAnchor,
PreloadingConfidence{100}, fallback_to_preconnect);
}
void PreloadingDecider::OnViewportHeuristicTriggered(const GURL& url) {
CHECK(base::FeatureList::IsEnabled(
blink::features::kPreloadingViewportHeuristics));
static const base::FeatureParam<bool> kShouldEnactCandidates{
&blink::features::kPreloadingViewportHeuristics, "enact_candidates",
false};
const bool should_enact_candidates = kShouldEnactCandidates.Get();
if (!should_enact_candidates) {
AddPreloadingPrediction(url, preloading_predictor::kViewportHeuristic,
PreloadingConfidence(100));
return;
}
MaybeEnactCandidate(url, preloading_predictor::kViewportHeuristic,
PreloadingConfidence{100},
/*fallback_to_preconnect=*/false);
}
void PreloadingDecider::MaybeEnactCandidate(
const GURL& url,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence,
bool fallback_to_preconnect) {
if (const auto [found, added_prediction] =
MaybePrerender(url, enacting_predictor, confidence);
found) {
// If the prediction is associated with another WebContents, don't duplicate
// it here.
if (!added_prediction) {
AddPreloadingPrediction(url, enacting_predictor, confidence);
}
return;
}
AddPreloadingPrediction(url, enacting_predictor, confidence);
if (ShouldWaitForPrerenderResult(url)) {
// If there is a prerender in progress already, don't attempt a prefetch.
return;
}
if (MaybePrefetch(url, enacting_predictor, confidence)) {
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 (!fallback_to_preconnect || 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);
}
devtools_instrumentation::DidUpdateSpeculationCandidates(render_frame_host(),
candidates);
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
auto* preloading_data = static_cast<PreloadingDataImpl*>(
PreloadingData::GetOrCreateForWebContents(web_contents));
preloading_data->SetIsNavigationInDomainCallback(
content_preloading_predictor::kSpeculationRules,
base::BindRepeating([](NavigationHandle* navigation_handle) -> bool {
return ui::PageTransitionIsWebTriggerable(
navigation_handle->GetPageTransition());
}));
PredictorDomainCallback is_new_link_nav =
base::BindRepeating(&PreloadingDataImpl::IsLinkClickNavigation);
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerDownOnAnchor, is_new_link_nav);
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kUrlPointerHoverOnAnchor, is_new_link_nav);
if (base::FeatureList::IsEnabled(
blink::features::kPreloadingHeuristicsMLModel) &&
behavior_config_->ml_model_enacts_candidates()) {
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kPreloadingHeuristicsMLModel, is_new_link_nav);
}
if (base::FeatureList::IsEnabled(
blink::features::kPreloadingViewportHeuristics)) {
preloading_data->SetIsNavigationInDomainCallback(
preloading_predictor::kViewportHeuristic, is_new_link_nav);
}
// 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 (!IsImmediateSpeculationEagerness(candidate->eagerness) &&
processed_candidates_.find(key) == processed_candidates_.end()) {
// A PreloadingPrediction is intentionally not created for these
// candidates. Non-immediate rules aren't predictions per se, but a
// declaration to the browser that preloading would be safe.
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/40230530): Pass the action requested by speculation rules
// to PreloadingPrediction.
// A new web contents will be created for the case of prerendering into a
// new tab, so recording PreloadingPrediction is delayed until
// PrerenderNewTabHandle::StartPrerendering.
bool add_preloading_prediction =
!PredictionOccursInOtherWebContents(*candidate);
if (add_preloading_prediction) {
PreloadingTriggerType trigger_type =
PreloadingTriggerTypeFromSpeculationInjectionType(
candidate->injection_type);
// Immediate candidates are enacted by the same predictor that creates
// them.
PreloadingPredictor enacting_predictor =
GetPredictorForPreloadingTriggerType(trigger_type);
AddPreloadingPrediction(candidate->url, std::move(enacting_predictor),
PreloadingConfidence{100});
}
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 immediage candidates to the front. This will avoid unnecessarily
// marking some non-immediate candidates as on-standby when there is an
// immediate candidate with the same URL that will be processed immediately.
std::ranges::stable_partition(candidates, [](const auto& candidate) {
return IsImmediateSpeculationEagerness(candidate->eagerness);
});
// The candidates remaining after this call will be all immediate candidates,
// and all non-immediate candidates whose (url, action) pair has already been
// processed.
std::erase_if(candidates, should_mark_as_on_standby);
// TODO(crbug.com/381687257): Combine all speculation rules tags merging logic
// in PreloadingDecider to reduce code redundancy.
// Aggregate all tags for immediate candidates.
std::map<SpeculationCandidateKey, std::vector<std::optional<std::string>>>
tags_map_for_immediate_preloading;
for (auto& candidate : candidates) {
if (!IsImmediateSpeculationEagerness(candidate->eagerness)) {
continue;
}
SpeculationCandidateKey key{candidate->url, candidate->action};
for (const auto& tag : candidate->tags) {
tags_map_for_immediate_preloading[key].push_back(tag);
}
}
for (auto& candidate : candidates) {
if (!IsImmediateSpeculationEagerness(candidate->eagerness)) {
continue;
}
SpeculationCandidateKey key{candidate->url, candidate->action};
if (tags_map_for_immediate_preloading.count(key) != 0) {
candidate->tags = tags_map_for_immediate_preloading[key];
}
}
prefetcher_.ProcessCandidatesForPrefetch(candidates);
prerenderer_->ProcessCandidatesForPrerender(candidates);
}
void PreloadingDecider::OnLCPPredicted() {
prerenderer_->OnLCPPredicted();
}
std::vector<std::optional<std::string>>
PreloadingDecider::GetMergedSpeculationTagsFromSuitableCandidates(
const PreloadingDecider::SpeculationCandidateKey& lookup_key,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence) {
std::vector<std::optional<std::string>> merged_tags;
// Find all suitable candidates.
auto suitable_candidates =
FindSuitableCandidates(lookup_key, enacting_predictor, confidence);
// Iterate through all suitable candidates and merge their tags.
for (const auto& candidate_pair : suitable_candidates) {
for (const auto& tag : candidate_pair.second->tags) {
if (!base::Contains(merged_tags, tag)) {
merged_tags.push_back(tag);
}
}
}
return merged_tags;
}
bool PreloadingDecider::MaybePrefetch(
const GURL& url,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence) {
SpeculationCandidateKey key{url, blink::mojom::SpeculationAction::kPrefetch};
std::vector<std::optional<std::string>> merged_tags =
GetMergedSpeculationTagsFromSuitableCandidates(key, enacting_predictor,
confidence);
std::optional<std::pair<PreloadingDecider::SpeculationCandidateKey,
blink::mojom::SpeculationCandidatePtr>>
matched_candidate_pair =
GetMatchedPreloadingCandidate(key, enacting_predictor, confidence);
if (!matched_candidate_pair.has_value()) {
return false;
}
key = matched_candidate_pair.value().first;
matched_candidate_pair.value().second->tags = merged_tags;
bool result = prefetcher_.MaybePrefetch(
std::move(matched_candidate_pair.value().second), enacting_predictor);
auto it = on_standby_candidates_.find(key);
CHECK(it != on_standby_candidates_.end());
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;
}
std::optional<std::pair<PreloadingDecider::SpeculationCandidateKey,
blink::mojom::SpeculationCandidatePtr>>
PreloadingDecider::GetMatchedPreloadingCandidate(
const PreloadingDecider::SpeculationCandidateKey& lookup_key,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence) const {
// Find all suitable candidates.
auto suitable_candidates =
FindSuitableCandidates(lookup_key, enacting_predictor, confidence);
if (suitable_candidates.empty()) {
return std::nullopt;
}
// Return the first suitable candidate if any are found.
return std::move(suitable_candidates[0]);
}
// Enumerates all NVS-matched candidates and invokes the visitor for each match.
// If the visitor returns true, enumeration stops early.
template <typename Visitor>
void PreloadingDecider::EnumerateNoVarySearchMatchedCandidates(
const SpeculationCandidateKey& lookup_key,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence,
Visitor&& visitor) const {
// Remove query and ref from the URL for NVS matching.
GURL::Replacements replacements;
replacements.ClearRef();
replacements.ClearQuery();
const GURL url_without_query_and_ref =
lookup_key.first.ReplaceComponents(replacements);
auto nvs_it = no_vary_search_hint_on_standby_candidates_.find(
{url_without_query_and_ref, lookup_key.second});
if (nvs_it == no_vary_search_hint_on_standby_candidates_.end()) {
return;
}
for (const auto& standby_key : nvs_it->second) {
CHECK_EQ(standby_key.second, lookup_key.second);
const GURL& preload_url = standby_key.first;
auto standby_it = on_standby_candidates_.find(standby_key);
CHECK(standby_it != on_standby_candidates_.end());
for (const auto& on_standby_candidate : standby_it->second) {
if (on_standby_candidate->no_vary_search_hint &&
no_vary_search::ParseHttpNoVarySearchDataFromMojom(
on_standby_candidate->no_vary_search_hint)
.AreEquivalent(lookup_key.first, preload_url) &&
IsSuitableCandidate(on_standby_candidate, enacting_predictor,
confidence, standby_key.second)) {
// If visitor returns true, stop enumeration early.
if (visitor(standby_key, on_standby_candidate)) {
return;
}
}
}
}
}
std::vector<std::pair<PreloadingDecider::SpeculationCandidateKey,
blink::mojom::SpeculationCandidatePtr>>
PreloadingDecider::FindSuitableCandidates(
const PreloadingDecider::SpeculationCandidateKey& lookup_key,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence) const {
std::vector<
std::pair<SpeculationCandidateKey, blink::mojom::SpeculationCandidatePtr>>
suitable_candidates;
// First, attempt a direct lookup for the exact key.
auto it = on_standby_candidates_.find(lookup_key);
if (it != on_standby_candidates_.end()) {
for (const auto& candidate : it->second) {
if (IsSuitableCandidate(candidate, enacting_predictor, confidence,
lookup_key.second)) {
suitable_candidates.emplace_back(lookup_key, candidate.Clone());
}
}
}
// If a direct match is found, return early.
if (!suitable_candidates.empty()) {
return suitable_candidates;
}
// Use NVS matching to collect all suitable candidates.
EnumerateNoVarySearchMatchedCandidates(
lookup_key, enacting_predictor, confidence,
[&](const SpeculationCandidateKey& standby_key,
const blink::mojom::SpeculationCandidatePtr& candidate) {
suitable_candidates.emplace_back(standby_key, candidate.Clone());
return false; // Continue enumeration to collect all matches.
});
return suitable_candidates;
}
std::optional<std::pair<PreloadingDecider::SpeculationCandidateKey,
blink::mojom::SpeculationCandidatePtr>>
PreloadingDecider::GetMatchedPreloadingCandidateByNoVarySearchHint(
const PreloadingDecider::SpeculationCandidateKey& lookup_key,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence) const {
std::optional<
std::pair<SpeculationCandidateKey, blink::mojom::SpeculationCandidatePtr>>
result;
// Check all URLs that might match via NVS hint.
// If there are multiple candidates that match the first one.
EnumerateNoVarySearchMatchedCandidates(
lookup_key, enacting_predictor, confidence,
[&](const SpeculationCandidateKey& standby_key,
const blink::mojom::SpeculationCandidatePtr& candidate) {
result = std::make_pair(standby_key, candidate.Clone());
return true; // Stop enumeration after the first match.
});
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);
}
std::pair<bool, bool> PreloadingDecider::MaybePrerender(
const GURL& url,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence) {
std::pair<bool, bool> result{false, false};
SpeculationCandidateKey key{url, blink::mojom::SpeculationAction::kPrerender};
std::vector<std::optional<std::string>> merged_tags =
GetMergedSpeculationTagsFromSuitableCandidates(key, enacting_predictor,
confidence);
std::optional<std::pair<PreloadingDecider::SpeculationCandidateKey,
blink::mojom::SpeculationCandidatePtr>>
matched_candidate_pair =
GetMatchedPreloadingCandidate(key, enacting_predictor, confidence);
if (!matched_candidate_pair.has_value()) {
return result;
}
key = matched_candidate_pair.value().first;
matched_candidate_pair.value().second->tags = merged_tags;
blink::mojom::SpeculationCandidatePtr candidate =
std::move(matched_candidate_pair.value().second);
result.first =
prerenderer_->MaybePrerender(candidate, enacting_predictor, confidence);
result.second =
result.first && PredictionOccursInOtherWebContents(*candidate);
auto it = on_standby_candidates_.find(key);
CHECK(it != on_standby_candidates_.end());
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,
PreloadingConfidence confidence,
blink::mojom::SpeculationAction action) const {
EagernessSet eagerness_set_for_predictor =
behavior_config_->EagernessSetForPredictor(predictor);
// If the ML model is available, its decisions supersede the hover heuristic.
if (ml_model_available_ &&
predictor == preloading_predictor::kUrlPointerHoverOnAnchor) {
eagerness_set_for_predictor.RemoveAll(
behavior_config_->EagernessSetForPredictor(
preloading_predictor::kPreloadingHeuristicsMLModel));
}
return eagerness_set_for_predictor.Has(candidate->eagerness) &&
confidence >= behavior_config_->GetThreshold(predictor, action);
}
PreloadingDeciderObserverForTesting* PreloadingDecider::SetObserverForTesting(
PreloadingDeciderObserverForTesting* observer) {
return std::exchange(observer_for_testing_, observer);
}
Prerenderer& PreloadingDecider::GetPrerendererForTesting() {
CHECK(prerenderer_);
return *prerenderer_;
}
std::unique_ptr<Prerenderer> PreloadingDecider::SetPrerendererForTesting(
std::unique_ptr<Prerenderer> prerenderer) {
prerenderer->SetPrerenderCancellationCallback(base::BindRepeating(
&OnPrerenderCanceled, render_frame_host().GetWeakDocumentPtr()));
return std::exchange(prerenderer_, std::move(prerenderer));
}
bool PreloadingDecider::IsOnStandByForTesting(
const GURL& url,
blink::mojom::SpeculationAction action) const {
return on_standby_candidates_.contains({url, action});
}
bool PreloadingDecider::HasCandidatesForTesting() const {
return !on_standby_candidates_.empty() ||
!no_vary_search_hint_on_standby_candidates_.empty() ||
!processed_candidates_.empty();
}
void PreloadingDecider::OnPreloadDiscarded(SpeculationCandidateKey key) {
auto it = processed_candidates_.find(key);
// If the preload is triggered outside of `PreloadingDecider`, ignore it.
// Currently, `PrerendererImpl` triggers prefetch ahead of prerender.
if (it == processed_candidates_.end()) {
return;
}
std::vector<blink::mojom::SpeculationCandidatePtr> candidates =
std::move(it->second);
processed_candidates_.erase(it);
for (const auto& candidate : candidates) {
if (!IsImmediateSpeculationEagerness(candidate->eagerness)) {
AddStandbyCandidate(candidate);
}
// TODO(crbug.com/40064525): Add support for the case where |candidate|'s
// eagerness is immediate one like `kImmediate`. In a scenario where the
// prefetch evicted is a non-immediate prefetch, we could theoretically
// reprefetch using the immediate candidate (and have it use the immediate
// prefetch quota). In that scenario, perhaps not evicting and just making
// the prefetch use the immediate limit might be a better option too. In the
// case where an immediate 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.
// TODO(crbug.com/40275452): Add implementation for immediate cases for
// prerender.
}
}
} // namespace content