blob: 58e9ee5a619ab2073ac959313ffc42721f9843d0 [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 "content/browser/preloading/prerender/prerender_host_registry.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/memory_pressure_monitor.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/system/sys_info.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/devtools/render_frame_devtools_agent_host.h"
#include "content/browser/preloading/preloading_data_impl.h"
#include "content/browser/preloading/preloading_trigger_type_impl.h"
#include "content/browser/preloading/prerender/devtools_prerender_attempt.h"
#include "content/browser/preloading/prerender/prerender_features.h"
#include "content/browser/preloading/prerender/prerender_final_status.h"
#include "content/browser/preloading/prerender/prerender_metrics.h"
#include "content/browser/preloading/prerender/prerender_navigation_utils.h"
#include "content/browser/preloading/prerender/prerender_new_tab_handle.h"
#include "content/browser/preloading/speculation_rules/speculation_rules_util.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/frame.mojom.h"
#include "content/public/browser/client_hints_controller_delegate.h"
#include "content/public/browser/preloading.h"
#include "content/public/browser/preloading_data.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "net/base/load_flags.h"
#include "services/network/public/cpp/network_quality_tracker.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
namespace content {
namespace {
bool IsBackground(Visibility visibility) {
// PrerenderHostRegistry treats HIDDEN and OCCLUDED as background.
switch (visibility) {
case Visibility::HIDDEN:
case Visibility::OCCLUDED:
return true;
case Visibility::VISIBLE:
return false;
}
}
// Returns true when it is allowed to activate a prerendered page in a
// background tab.
bool IsAllowedToActivateInBackgroundForTesting() {
// Now it is allowed to activate a prerendered page in a background only on
// macOS for running web platform tests. See comments on the flag definition
// for more details.
#if BUILDFLAG(IS_MAC)
if (base::FeatureList::IsEnabled(
features::kPrerender2AllowActivationInBackground)) {
return true;
}
#endif
return false;
}
bool DeviceHasEnoughMemoryForPrerender() {
// This method disallows prerendering on low-end devices if the
// kPrerender2MemoryControls feature is enabled.
if (!base::FeatureList::IsEnabled(blink::features::kPrerender2MemoryControls))
return true;
// On Android, Prerender2 is only enabled for 2GB+ high memory devices. The
// default threshold value is set to 1700 MB to account for all 2GB devices
// which report lower RAM due to carveouts.
// Previously used the same default threshold as the back/forward cache. See
// comments in DeviceHasEnoughMemoryForBackForwardCache().
// TODO(crbug.com/40277975): experiment with 1200 MB threshold like
// back/forward cache.
static constexpr int kDefaultMemoryThresholdMb =
#if BUILDFLAG(IS_ANDROID)
1700;
#else
0;
#endif
// The default is overridable by field trial param.
int memory_threshold_mb = base::GetFieldTrialParamByFeatureAsInt(
blink::features::kPrerender2MemoryControls,
blink::features::kPrerender2MemoryThresholdParamName,
kDefaultMemoryThresholdMb);
return base::SysInfo::AmountOfPhysicalMemory().InMiB() > memory_threshold_mb;
}
base::MemoryPressureListener::MemoryPressureLevel
GetCurrentMemoryPressureLevel() {
// Ignore the memory pressure event if the memory control is disabled.
if (!base::FeatureList::IsEnabled(
blink::features::kPrerender2MemoryControls)) {
return base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
}
auto* monitor = base::MemoryPressureMonitor::Get();
if (!monitor) {
return base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
}
return monitor->GetCurrentPressureLevel();
}
// Create a resource request for `back_url` that only checks whether the
// resource is in the HTTP cache.
std::unique_ptr<network::SimpleURLLoader> CreateHttpCacheQueryingResourceLoad(
const GURL& back_url) {
std::unique_ptr<network::ResourceRequest> request =
std::make_unique<network::ResourceRequest>();
request->url = back_url;
request->load_flags =
net::LOAD_ONLY_FROM_CACHE | net::LOAD_SKIP_CACHE_VALIDATION;
if (base::FeatureList::IsEnabled(
blink::features::kAvoidTrustedParamsCopies)) {
request->trusted_params = network::ResourceRequest::TrustedParams();
url::Origin origin = url::Origin::Create(back_url);
request->trusted_params->isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromOrigin(origin));
request->site_for_cookies =
request->trusted_params->isolation_info.site_for_cookies();
} else {
url::Origin origin = url::Origin::Create(back_url);
net::IsolationInfo isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromOrigin(origin));
network::ResourceRequest::TrustedParams trusted_params;
trusted_params.isolation_info = isolation_info;
request->trusted_params = trusted_params;
request->site_for_cookies =
trusted_params.isolation_info.site_for_cookies();
}
request->credentials_mode = network::mojom::CredentialsMode::kOmit;
request->skip_service_worker = true;
request->do_not_prompt_for_login = true;
CHECK(!request->SendsCookies());
CHECK(!request->SavesCookies());
constexpr net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("back_navigation_cache_query",
R"(
semantics {
sender: "Prerender"
description:
"This is not actually a network request. It is used internally "
"by the browser to determine if the HTTP cache would be used if "
"the user were to navigate back in session history. It only "
"checks the cache and does not hit the network."
trigger:
"When the user performs an action that would suggest that they "
"intend to navigate back soon. Examples include hovering the "
"mouse over the back button and the start of a gestural back "
"navigation."
user_data {
type: NONE
}
data: "None. The request doesn't hit the network."
destination: LOCAL
internal {
contacts {
email: "chrome-brapp-loading@chromium.org"
}
}
last_reviewed: "2023-03-24"
}
policy {
cookies_allowed: NO
setting:
"This is not controlled by a setting."
policy_exception_justification: "This is not a network request."
})");
return network::SimpleURLLoader::Create(std::move(request),
traffic_annotation);
}
// Returns true if the given navigation is meant to be predicted by a predictor
// related to session history (e.g. hovering over the back button could have
// predicted the navigation).
bool IsNavigationInSessionHistoryPredictorDomain(NavigationHandle* handle) {
CHECK(handle->IsInPrimaryMainFrame());
CHECK(!handle->IsSameDocument());
if (handle->IsRendererInitiated()) {
return false;
}
// Note that currently the only predictors are for back navigations of a
// single step, however we still include all session history navigations in
// the domain. The preloading of back navigations could generalize to session
// history navigations of other offsets, but we haven't explored this due to
// the higher usage of the back button compared to the forward button or
// history menu.
if (!(handle->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK)) {
return false;
}
if (handle->IsPost()) {
return false;
}
if (handle->IsServedFromBackForwardCache()) {
return false;
}
if (!handle->GetURL().SchemeIsHTTPOrHTTPS()) {
return false;
}
// Note that even though the current predictors do not handle session history
// navigations that are same-site or which don't use the HTTP cache, they are
// still included in the domain.
return true;
}
PreloadingEligibility ToEligibility(PrerenderFinalStatus status) {
switch (status) {
case PrerenderFinalStatus::kActivated:
case PrerenderFinalStatus::kDestroyed:
NOTREACHED();
case PrerenderFinalStatus::kLowEndDevice:
return PreloadingEligibility::kLowMemory;
case PrerenderFinalStatus::kInvalidSchemeRedirect:
NOTREACHED();
case PrerenderFinalStatus::kInvalidSchemeNavigation:
return PreloadingEligibility::kHttpOrHttpsOnly;
case PrerenderFinalStatus::kNavigationRequestBlockedByCsp:
case PrerenderFinalStatus::kMojoBinderPolicy:
case PrerenderFinalStatus::kRendererProcessCrashed:
case PrerenderFinalStatus::kRendererProcessKilled:
case PrerenderFinalStatus::kDownload:
case PrerenderFinalStatus::kTriggerDestroyed:
case PrerenderFinalStatus::kNavigationNotCommitted:
case PrerenderFinalStatus::kNavigationBadHttpStatus:
case PrerenderFinalStatus::kClientCertRequested:
case PrerenderFinalStatus::kNavigationRequestNetworkError:
case PrerenderFinalStatus::kCancelAllHostsForTesting:
case PrerenderFinalStatus::kDidFailLoad:
case PrerenderFinalStatus::kStop:
case PrerenderFinalStatus::kSslCertificateError:
case PrerenderFinalStatus::kLoginAuthRequested:
case PrerenderFinalStatus::kUaChangeRequiresReload:
case PrerenderFinalStatus::kBlockedByClient:
case PrerenderFinalStatus::kMixedContent:
NOTREACHED();
case PrerenderFinalStatus::kTriggerBackgrounded:
return PreloadingEligibility::kHidden;
case PrerenderFinalStatus::kMemoryLimitExceeded:
NOTREACHED();
case PrerenderFinalStatus::kDataSaverEnabled:
return PreloadingEligibility::kDataSaverEnabled;
case PrerenderFinalStatus::kTriggerUrlHasEffectiveUrl:
return PreloadingEligibility::kHasEffectiveUrl;
case PrerenderFinalStatus::kActivatedBeforeStarted:
case PrerenderFinalStatus::kInactivePageRestriction:
case PrerenderFinalStatus::kStartFailed:
case PrerenderFinalStatus::kTimeoutBackgrounded:
case PrerenderFinalStatus::kCrossSiteRedirectInInitialNavigation:
NOTREACHED();
case PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation:
return PreloadingEligibility::kCrossOrigin;
case PrerenderFinalStatus::
kSameSiteCrossOriginRedirectNotOptInInInitialNavigation:
case PrerenderFinalStatus::
kSameSiteCrossOriginNavigationNotOptInInInitialNavigation:
case PrerenderFinalStatus::kActivationNavigationParameterMismatch:
case PrerenderFinalStatus::kActivatedInBackground:
case PrerenderFinalStatus::kActivationNavigationDestroyedBeforeSuccess:
case PrerenderFinalStatus::kTabClosedByUserGesture:
case PrerenderFinalStatus::kTabClosedWithoutUserGesture:
case PrerenderFinalStatus::kPrimaryMainFrameRendererProcessCrashed:
case PrerenderFinalStatus::kPrimaryMainFrameRendererProcessKilled:
case PrerenderFinalStatus::kActivationFramePolicyNotCompatible:
NOTREACHED();
case PrerenderFinalStatus::kPreloadingDisabled:
return PreloadingEligibility::kPreloadingDisabled;
case PrerenderFinalStatus::kBatterySaverEnabled:
return PreloadingEligibility::kBatterySaverEnabled;
case PrerenderFinalStatus::kActivatedDuringMainFrameNavigation:
NOTREACHED();
case PrerenderFinalStatus::kPreloadingUnsupportedByWebContents:
return PreloadingEligibility::kPreloadingUnsupportedByWebContents;
case PrerenderFinalStatus::kCrossSiteRedirectInMainFrameNavigation:
case PrerenderFinalStatus::kCrossSiteNavigationInMainFrameNavigation:
case PrerenderFinalStatus::
kSameSiteCrossOriginRedirectNotOptInInMainFrameNavigation:
case PrerenderFinalStatus::
kSameSiteCrossOriginNavigationNotOptInInMainFrameNavigation:
NOTREACHED();
case PrerenderFinalStatus::kMemoryPressureOnTrigger:
return PreloadingEligibility::kMemoryPressure;
case PrerenderFinalStatus::kMemoryPressureAfterTriggered:
NOTREACHED();
case PrerenderFinalStatus::kPrerenderingDisabledByDevTools:
return PreloadingEligibility::kPreloadingDisabledByDevTools;
case PrerenderFinalStatus::kSpeculationRuleRemoved:
case PrerenderFinalStatus::kActivatedWithAuxiliaryBrowsingContexts:
case PrerenderFinalStatus::kMaxNumOfRunningImmediatePrerendersExceeded:
case PrerenderFinalStatus::kMaxNumOfRunningNonImmediatePrerendersExceeded:
case PrerenderFinalStatus::kMaxNumOfRunningEmbedderPrerendersExceeded:
NOTREACHED();
case PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl:
case PrerenderFinalStatus::kRedirectedPrerenderingUrlHasEffectiveUrl:
case PrerenderFinalStatus::kActivationUrlHasEffectiveUrl:
return PreloadingEligibility::kHasEffectiveUrl;
case PrerenderFinalStatus::kJavaScriptInterfaceAdded:
case PrerenderFinalStatus::kJavaScriptInterfaceRemoved:
case PrerenderFinalStatus::kAllPrerenderingCanceled:
case PrerenderFinalStatus::kWindowClosed:
case PrerenderFinalStatus::kOtherPrerenderedPageActivated:
NOTREACHED();
case PrerenderFinalStatus::kSlowNetwork:
return PreloadingEligibility::kSlowNetwork;
case PrerenderFinalStatus::kPrerenderFailedDuringPrefetch:
case PrerenderFinalStatus::kBrowsingDataRemoved:
NOTREACHED();
case PrerenderFinalStatus::kPrerenderHostReused:
NOTREACHED();
}
NOTREACHED();
}
template <class ContainerType>
void DestructPrerenderHosts(ContainerType& hosts) {
// Swap the container and let it scope out instead of directly destructing the
// hosts in the container, for example, by `hosts.clear()`. This avoids
// potential cases where a host being deleted indirectly modifies the
// container while the container is being cleared up.
// See https://crbug.com/40263658 for contexts.
ContainerType temp;
hosts.swap(temp);
}
// Represents a contract and ensures that the given prerender attempt is started
// as a PrerenderHost or rejected with a reason. It is allowed to use it only in
// PrerenderHostRegistry::CreateAndStartHost.
//
// TODO(kenoss): Add emits of Preload.prerenderStatusUpdated.
class PrerenderHostBuilder {
public:
explicit PrerenderHostBuilder(PreloadingAttempt* attempt);
~PrerenderHostBuilder();
PrerenderHostBuilder(const PrerenderHostBuilder&) = delete;
PrerenderHostBuilder& operator=(const PrerenderHostBuilder&) = delete;
PrerenderHostBuilder(PrerenderHostBuilder&&) = delete;
PrerenderHostBuilder& operator=(PrerenderHostBuilder&&) = delete;
// The following methods consumes this class.
std::unique_ptr<PrerenderHost> Build(
std::unique_ptr<PrerenderHost> reuse_host,
const PrerenderAttributes& attributes,
WebContentsImpl& prerender_web_contents);
void RejectAsNotEligible(const PrerenderAttributes& attributes,
PrerenderFinalStatus status);
void RejectAsDuplicate();
void RejectAsFailure(const PrerenderAttributes& attributes,
PrerenderFinalStatus status);
void RejectDueToHoldback();
void SetHoldbackOverride(PreloadingHoldbackStatus status);
bool CheckIfShouldHoldback();
// Public only for exceptional case.
// TODO(crbug.com/40904828): Make this private again.
void Drop();
private:
bool IsDropped() const;
// Use raw pointer as PrerenderHostBuilder is alive only during
// PrerenderHostRegistry::CreateAndStartHost(), and PreloadingAttempt should
// outlive the function.
raw_ptr<PreloadingAttempt> attempt_;
std::unique_ptr<DevToolsPrerenderAttempt> devtools_attempt_;
};
PrerenderHostBuilder::PrerenderHostBuilder(PreloadingAttempt* attempt)
: attempt_(attempt),
devtools_attempt_(std::make_unique<DevToolsPrerenderAttempt>()) {}
PrerenderHostBuilder::~PrerenderHostBuilder() {
CHECK(IsDropped());
}
void PrerenderHostBuilder::Drop() {
attempt_ = nullptr;
devtools_attempt_.reset();
}
bool PrerenderHostBuilder::IsDropped() const {
return devtools_attempt_ == nullptr;
}
std::unique_ptr<PrerenderHost> PrerenderHostBuilder::Build(
std::unique_ptr<PrerenderHost> reuse_host,
const PrerenderAttributes& attributes,
WebContentsImpl& prerender_web_contents) {
CHECK(!IsDropped());
std::unique_ptr<PrerenderHost> prerender_host;
prerender_host = std::make_unique<PrerenderHost>(
std::move(reuse_host), attributes, prerender_web_contents,
attempt_ ? attempt_->GetWeakPtr() : nullptr,
std::move(devtools_attempt_));
Drop();
return prerender_host;
}
void PrerenderHostBuilder::RejectAsNotEligible(
const PrerenderAttributes& attributes,
PrerenderFinalStatus status) {
CHECK(!IsDropped());
if (attempt_) {
attempt_->SetEligibility(ToEligibility(status));
}
devtools_attempt_->SetFailureReason(attributes, status);
RecordFailedPrerenderFinalStatus(PrerenderCancellationReason(status),
attributes);
Drop();
}
bool PrerenderHostBuilder::CheckIfShouldHoldback() {
CHECK(!IsDropped());
// Assigns the holdback status in the attempt it was not overridden earlier.
return attempt_ && attempt_->ShouldHoldback();
}
void PrerenderHostBuilder::RejectDueToHoldback() {
CHECK(!IsDropped());
// If DevTools is opened, holdbacks are force-disabled. So, we don't need to
// report this case to DevTools.
Drop();
}
void PrerenderHostBuilder::RejectAsDuplicate() {
CHECK(!IsDropped());
if (attempt_) {
attempt_->SetTriggeringOutcome(PreloadingTriggeringOutcome::kDuplicate);
}
// No need to report DevTools nor UMA; just removing duplicates.
Drop();
}
void PrerenderHostBuilder::SetHoldbackOverride(
PreloadingHoldbackStatus status) {
if (!attempt_) {
return;
}
attempt_->SetHoldbackStatus(status);
}
void PrerenderHostBuilder::RejectAsFailure(
const PrerenderAttributes& attributes,
PrerenderFinalStatus status) {
CHECK(!IsDropped());
if (attempt_) {
attempt_->SetFailureReason(ToPreloadingFailureReason(status));
}
devtools_attempt_->SetFailureReason(attributes, status);
RecordFailedPrerenderFinalStatus(PrerenderCancellationReason(status),
attributes);
Drop();
}
bool IsSlowNetwork(WebContents* web_contents) {
static const base::TimeDelta kSlowNetworkThreshold =
features::kSuppressesPrerenderingOnSlowNetworkThreshold.Get();
return web_contents && web_contents->GetBrowserContext() &&
web_contents->GetBrowserContext()
->GetClientHintsControllerDelegate() &&
web_contents->GetBrowserContext()
->GetClientHintsControllerDelegate()
->GetNetworkQualityTracker() &&
web_contents->GetBrowserContext()
->GetClientHintsControllerDelegate()
->GetNetworkQualityTracker()
->GetHttpRTT() > kSlowNetworkThreshold;
}
} // namespace
PrerenderHostRegistry::PrerenderHostRegistry(WebContents& web_contents)
: memory_pressure_listener_(
FROM_HERE,
base::MemoryPressureListenerTag::kPrerenderHostRegistry,
base::BindRepeating(&PrerenderHostRegistry::OnMemoryPressure,
base::Unretained(this))) {
Observe(&web_contents);
}
PrerenderHostRegistry::~PrerenderHostRegistry() {
// This function is called by WebContentsImpl's dtor, so web_contents() should
// not be a null ptr at this moment.
CHECK(web_contents());
PrerenderFinalStatus final_status =
web_contents()->GetClosedByUserGesture()
? PrerenderFinalStatus::kTabClosedByUserGesture
: PrerenderFinalStatus::kTabClosedWithoutUserGesture;
// Here we have to delete the prerender hosts synchronously, to ensure the
// FrameTrees would not access the WebContents.
CancelAllHosts(final_status);
DestructPrerenderHosts(to_be_deleted_hosts_);
DestructPrerenderHosts(pending_deletion_hosts_);
Observe(nullptr);
for (Observer& obs : observers_)
obs.OnRegistryDestroyed();
}
void PrerenderHostRegistry::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void PrerenderHostRegistry::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
FrameTreeNodeId PrerenderHostRegistry::CreateAndStartHost(
const PrerenderAttributes& attributes,
PreloadingAttempt* attempt) {
std::string recorded_url =
attributes.initiator_origin.has_value()
? attributes.initiator_origin.value().GetURL().spec()
: "(empty_url)";
TRACE_EVENT2("navigation", "PrerenderHostRegistry::CreateAndStartHost",
"attributes", attributes, "initiator_origin", recorded_url);
// The initiator WebContents can be different from the WebContents that will
// host a prerendered page only when the prerender-in-new-tab runs.
CHECK(attributes.initiator_web_contents);
auto& initiator_web_contents =
static_cast<WebContentsImpl&>(*attributes.initiator_web_contents);
auto& prerender_web_contents = static_cast<WebContentsImpl&>(*web_contents());
CHECK(&initiator_web_contents == &prerender_web_contents ||
base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab));
FrameTreeNodeId frame_tree_node_id;
std::optional<blink::mojom::SpeculationEagerness> eagerness =
attributes.GetEagerness();
{
RenderFrameHostImpl* initiator_rfh =
attributes.IsBrowserInitiated()
? nullptr
: RenderFrameHostImpl::FromFrameToken(
attributes.initiator_process_id,
attributes.initiator_frame_token.value());
// Ensure observers are notified that a trigger occurred.
absl::Cleanup notify_trigger = [this, &attributes] {
NotifyTrigger(attributes.prerendering_url);
};
auto builder = PrerenderHostBuilder(attempt);
// We don't know the root cause, but there is a case this is null.
//
// TODO(crbug.com/40904828): Continue investigation and fix the root
// cause.
if (initiator_web_contents.GetDelegate() == nullptr) {
// Note that return without consuming `builder` is exceptional.
builder.Drop();
return FrameTreeNodeId();
}
// Check the about://flags toggle.
if (!base::FeatureList::IsEnabled(blink::features::kPrerender2)) {
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kPreloadingDisabled);
return FrameTreeNodeId();
}
// Check whether preloading is enabled. If it is not enabled, report the
// reason.
switch (initiator_web_contents.GetDelegate()->IsPrerender2Supported(
initiator_web_contents, attributes.trigger_type)) {
case PreloadingEligibility::kEligible:
// nop
break;
case PreloadingEligibility::kPreloadingDisabled:
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kPreloadingDisabled);
return FrameTreeNodeId();
case PreloadingEligibility::kDataSaverEnabled:
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kDataSaverEnabled);
return FrameTreeNodeId();
case PreloadingEligibility::kBatterySaverEnabled:
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kBatterySaverEnabled);
return FrameTreeNodeId();
case PreloadingEligibility::kPreloadingUnsupportedByWebContents:
builder.RejectAsNotEligible(
attributes,
PrerenderFinalStatus::kPreloadingUnsupportedByWebContents);
return FrameTreeNodeId();
default:
NOTREACHED();
}
// Don't prerender when the initiator is in the background and its type is
// `kEmbedder`, as current implementation doesn't use `pending_prerenders_`
// when kEmbedder.
// If the trigger type is speculation rules, nothing should be done here and
// then prerender host will be created and its id will be enqueued to
// `pending_prerenders_`. The visibility of the initiator will be considered
// when trying to pop from `pending_prerenders_` on `StartPrerendering()`.
if (attributes.trigger_type == PreloadingTriggerType::kEmbedder &&
initiator_web_contents.GetVisibility() == Visibility::HIDDEN) {
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kTriggerBackgrounded);
return FrameTreeNodeId();
}
// Don't prerender on low-end devices.
if (!DeviceHasEnoughMemoryForPrerender()) {
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kLowEndDevice);
return FrameTreeNodeId();
}
// Don't prerender under critical memory pressure.
switch (GetCurrentMemoryPressureLevel()) {
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE:
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE:
break;
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL:
builder.RejectAsNotEligible(
attributes, PrerenderFinalStatus::kMemoryPressureOnTrigger);
return FrameTreeNodeId();
}
// Disable prerendering on slow network.
static const bool kSuppressesPrerenderingOnSlowNetworkIsEnabled =
base::FeatureList::IsEnabled(
features::kSuppressesPrerenderingOnSlowNetwork);
if (kSuppressesPrerenderingOnSlowNetworkIsEnabled &&
IsSlowNetwork(web_contents())) {
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kSlowNetwork);
return FrameTreeNodeId();
}
// Allow prerendering only for same-site. The initiator origin is nullopt
// when prerendering is initiated by the browser (not by a renderer using
// Speculation Rules API). In that case, skip this same-site check.
// TODO(crbug.com/40168192): Support cross-site prerendering.
if (!attributes.IsBrowserInitiated() &&
!prerender_navigation_utils::IsSameSite(
attributes.prerendering_url, attributes.initiator_origin.value())) {
builder.RejectAsNotEligible(
attributes,
PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation);
return FrameTreeNodeId();
}
// Allow prerendering only HTTP(S) scheme URLs. For redirection, this will
// be checked in PrerenderNavigationThrottle::WillStartOrRedirectRequest().
if (!attributes.prerendering_url.SchemeIsHTTPOrHTTPS()) {
builder.RejectAsNotEligible(
attributes, PrerenderFinalStatus::kInvalidSchemeNavigation);
return FrameTreeNodeId();
}
// Disallow all pages that have an effective URL like hosted apps and NTP.
auto* browser_context = prerender_web_contents.GetBrowserContext();
if (SiteInstanceImpl::HasEffectiveURL(browser_context,
initiator_web_contents.GetURL())) {
builder.RejectAsNotEligible(
attributes, PrerenderFinalStatus::kTriggerUrlHasEffectiveUrl);
return FrameTreeNodeId();
}
if (SiteInstanceImpl::HasEffectiveURL(browser_context,
attributes.prerendering_url)) {
builder.RejectAsNotEligible(
attributes, PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl);
return FrameTreeNodeId();
}
if (initiator_rfh && initiator_rfh->frame_tree() &&
!devtools_instrumentation::IsPrerenderAllowed(
*initiator_rfh->frame_tree())) {
builder.RejectAsNotEligible(
attributes, PrerenderFinalStatus::kPrerenderingDisabledByDevTools);
return FrameTreeNodeId();
}
// Once all eligibility checks are completed, set the status to kEligible.
if (attempt)
attempt->SetEligibility(PreloadingEligibility::kEligible);
// Normally CheckIfShouldHoldback() computes the holdback status based on
// PreloadingConfig. In special cases, we call SetHoldbackOverride() to
// override that processing.
bool has_devtools_open =
initiator_rfh &&
RenderFrameDevToolsAgentHost::GetFor(initiator_rfh) != nullptr;
if (has_devtools_open) {
// Never holdback when DevTools is opened, to avoid web developer
// frustration.
builder.SetHoldbackOverride(PreloadingHoldbackStatus::kAllowed);
} else if (attributes.holdback_status_override !=
PreloadingHoldbackStatus::kUnspecified) {
// The caller (e.g. from chrome/) is allowed to specify a holdback that
// overrides the default logic.
builder.SetHoldbackOverride(attributes.holdback_status_override);
}
// Check if the attempt is held back either due to the check above or via
// PreloadingConfig.
if (builder.CheckIfShouldHoldback()) {
builder.RejectDueToHoldback();
return FrameTreeNodeId();
}
// Ignore prerendering requests for the same URL.
for (auto& iter : prerender_host_by_frame_tree_node_id_) {
if (iter.second->GetInitialUrl() == attributes.prerendering_url) {
builder.RejectAsDuplicate();
return FrameTreeNodeId();
}
}
// Under kPrerender2InNewTab, CreateAndStartHost will be called in
// the newly created WebContents’s PrerenderHostRegistry for new tab
// triggers, rather than in initiator WebContents’s registry, while
// it is called in initiator ones for normal triggers. In either
// case, we want to control the limit based on the initiator
// WebContents.
//
// TODO(crbug.com/40235847): Enqueue the request exceeding the number limit
// until the forerunners are cancelled, and suspend starting a new prerender
// when the number reaches the limit.
if (!initiator_web_contents.GetPrerenderHostRegistry()
->IsAllowedToStartPrerenderingForTrigger(attributes.trigger_type,
eagerness)) {
// The reason we don't consider limit exceeded as an ineligibility
// reason is because we can't replicate the behavior in our other
// experiment groups for analysis. To prevent this we set
// TriggeringOutcome to kFailure and look into the failure reason to
// learn more.
PrerenderFinalStatus final_status;
switch (GetPrerenderLimitGroup(attributes.trigger_type, eagerness)) {
case PrerenderLimitGroup::kSpeculationRulesImmediate:
final_status =
PrerenderFinalStatus::kMaxNumOfRunningImmediatePrerendersExceeded;
break;
case PrerenderLimitGroup::kSpeculationRulesNonImmediate:
final_status = PrerenderFinalStatus::
kMaxNumOfRunningNonImmediatePrerendersExceeded;
break;
case PrerenderLimitGroup::kEmbedder:
final_status =
PrerenderFinalStatus::kMaxNumOfRunningEmbedderPrerendersExceeded;
break;
}
builder.RejectAsFailure(attributes, final_status);
return FrameTreeNodeId();
}
std::unique_ptr<PrerenderHost> reuse_host;
std::unique_ptr<PrerenderHost> prerender_host;
if (base::FeatureList::IsEnabled(features::kPrerender2ReuseHost)) {
reuse_host = FindAndTakePrerenderHostToReuse(attributes);
}
base::UmaHistogramBoolean("Prerender.Experimental.FoundReusePrerenderHost",
reuse_host != nullptr);
if (!reuse_host) {
base::UmaHistogramCounts100(
"Prerender.Experimental.ReusePrerenderHost.PrerenderHostCount.Failed",
prerender_host_by_frame_tree_node_id_.size());
}
// If we find a reusable prerender host under the same site. We will
// take over its frame tree and initiate a new navigation to the new
// prerender URL.
prerender_host = builder.Build(std::move(reuse_host), attributes,
prerender_web_contents);
frame_tree_node_id = prerender_host->frame_tree_node_id();
CHECK(!base::Contains(prerender_host_by_frame_tree_node_id_,
frame_tree_node_id));
prerender_host_by_frame_tree_node_id_[frame_tree_node_id] =
std::move(prerender_host);
if (GetPrerenderLimitGroup(attributes.trigger_type, eagerness) ==
PrerenderLimitGroup::kSpeculationRulesNonImmediate) {
non_immediate_prerender_host_id_by_arrival_order_.push_back(
frame_tree_node_id);
}
}
// Now start prerender the new page. If the PrerenderHost is reusing a frame
// tree, the previous page will be unloaded after initiating a new navigation.
switch (attributes.trigger_type) {
case PreloadingTriggerType::kSpeculationRule:
case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld:
case PreloadingTriggerType::kSpeculationRuleFromAutoSpeculationRules:
pending_prerenders_.push_back(frame_tree_node_id);
if (running_prerender_host_id_.is_null()) {
// Start the initial prerendering navigation of the pending request in
// the head of the queue if there's no running prerender and the
// initiator is in the foreground. If the initiator page is in the
// background, `StartPrerendering` will return a corresponding
// frame_tree_node_id if allowed by
// `PrerenderCanBeStartedWhenInitiatorIsInBackground`.
if (IsBackground(initiator_web_contents.GetVisibility()) &&
!initiator_web_contents.GetPrerenderHostRegistry()
->PrerenderCanBeStartedWhenInitiatorIsInBackground()) {
// Cancel if it is prerender-into-new-tab.
// TODO(crbug.com/350785853): Add queue
// mechanism and update test expectation.
if (web_contents() != &initiator_web_contents) {
return FrameTreeNodeId();
}
break;
}
FrameTreeNodeId started_frame_tree_node_id =
StartPrerendering(FrameTreeNodeId());
CHECK(started_frame_tree_node_id == frame_tree_node_id ||
started_frame_tree_node_id.is_null());
frame_tree_node_id = started_frame_tree_node_id;
}
break;
case PreloadingTriggerType::kEmbedder:
// The prerendering request from embedder should have high-priority
// because embedder prediction is more likely for the user to visit. Hold
// the return value of `StartPrerendering` because the requested prerender
// might be cancelled due to some restrictions and a null FrameTreeNodeId
// should be returned in that case.
frame_tree_node_id = StartPrerendering(frame_tree_node_id);
break;
}
return frame_tree_node_id;
}
FrameTreeNodeId PrerenderHostRegistry::CreateAndStartHostForNewTab(
const PrerenderAttributes& attributes,
const PreloadingPredictor& creating_predictor,
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence) {
CHECK(base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab));
CHECK(IsSpeculationRuleType(attributes.trigger_type));
std::string recorded_url =
attributes.initiator_origin.has_value()
? attributes.initiator_origin.value().GetURL().spec()
: "(empty_url)";
TRACE_EVENT2("navigation",
"PrerenderHostRegistry::CreateAndStartHostForNewTab",
"attributes", attributes, "initiator_origin", recorded_url);
auto handle = std::make_unique<PrerenderNewTabHandle>(
attributes, *web_contents()->GetBrowserContext());
FrameTreeNodeId prerender_host_id = handle->StartPrerendering(
creating_predictor, enacting_predictor, confidence);
if (prerender_host_id.is_null()) {
return FrameTreeNodeId();
}
prerender_new_tab_handle_by_frame_tree_node_id_[prerender_host_id] =
std::move(handle);
if (GetPrerenderLimitGroup(attributes.trigger_type,
attributes.GetEagerness()) ==
PrerenderLimitGroup::kSpeculationRulesNonImmediate) {
non_immediate_prerender_host_id_by_arrival_order_.push_back(
prerender_host_id);
}
return prerender_host_id;
}
FrameTreeNodeId PrerenderHostRegistry::StartPrerendering(
FrameTreeNodeId frame_tree_node_id) {
// TODO(crbug.com/40260412): Don't start prerendering if the current
// memory pressure level is critical, and then retry prerendering when the
// memory pressure level goes down.
if (frame_tree_node_id.is_null()) {
CHECK(running_prerender_host_id_.is_null());
while (!pending_prerenders_.empty()) {
FrameTreeNodeId host_id = pending_prerenders_.front();
// Skip a cancelled request.
auto found = prerender_host_by_frame_tree_node_id_.find(host_id);
if (found == prerender_host_by_frame_tree_node_id_.end()) {
// Remove the cancelled request from the pending queue.
pending_prerenders_.pop_front();
continue;
}
PrerenderHost* prerender_host = found->second.get();
// The initiator WebContents should be alive as it cancels all the
// prerendering requests during destruction.
CHECK(prerender_host->initiator_web_contents());
WebContentsImpl* initiator_web_contents = static_cast<WebContentsImpl*>(
prerender_host->initiator_web_contents().get());
if (IsBackground(initiator_web_contents->GetVisibility())) {
// The pending prerender triggered by the background tab will be started
// according to the conditions in
// `PrerenderCanBeStartedWhenInitiatorIsInBackground`.
if (!initiator_web_contents->GetPrerenderHostRegistry()
->PrerenderCanBeStartedWhenInitiatorIsInBackground()) {
return FrameTreeNodeId();
}
}
// Found the request to run.
pending_prerenders_.pop_front();
frame_tree_node_id = host_id;
break;
}
if (frame_tree_node_id.is_null()) {
return FrameTreeNodeId();
}
}
auto prerender_host_it =
prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id);
CHECK(prerender_host_it != prerender_host_by_frame_tree_node_id_.end());
PrerenderHost& prerender_host = *prerender_host_it->second;
devtools_instrumentation::WillInitiatePrerender(
prerender_host.GetPrerenderFrameTree());
if (!prerender_host.StartPrerendering()) {
CancelHost(frame_tree_node_id, PrerenderFinalStatus::kStartFailed);
return FrameTreeNodeId();
}
switch (prerender_host_by_frame_tree_node_id_[frame_tree_node_id]
->trigger_type()) {
case PreloadingTriggerType::kSpeculationRule:
case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld:
case PreloadingTriggerType::kSpeculationRuleFromAutoSpeculationRules:
// Update the `running_prerender_host_id` to the starting prerender's id.
running_prerender_host_id_ = frame_tree_node_id;
break;
case PreloadingTriggerType::kEmbedder:
// `running_prerender_host_id` only tracks the id for speculation rules
// trigger, so we also don't update it in the case of embedder.
break;
}
RecordPrerenderTriggered(
prerender_host_by_frame_tree_node_id_[frame_tree_node_id]
->initiator_ukm_id());
return frame_tree_node_id;
}
std::set<FrameTreeNodeId> PrerenderHostRegistry::CancelHosts(
const std::vector<FrameTreeNodeId>& frame_tree_node_ids,
const PrerenderCancellationReason& reason) {
TRACE_EVENT1("navigation", "PrerenderHostRegistry::CancelHosts",
"frame_tree_node_ids", frame_tree_node_ids);
// Cancel must not be requested during activation.
CHECK(!reserved_prerender_host_);
std::set<FrameTreeNodeId> cancelled_ids;
for (FrameTreeNodeId host_id : frame_tree_node_ids) {
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
if (CancelHostInternal(host_id, reason) ||
CancelNewTabHostInternal(host_id, reason)) {
cancelled_ids.insert(host_id);
}
} else {
CHECK(prerender_new_tab_handle_by_frame_tree_node_id_.empty());
if (CancelHostInternal(host_id, reason)) {
cancelled_ids.insert(host_id);
}
}
}
// Start another prerender if the running prerender is cancelled.
if (running_prerender_host_id_.is_null()) {
StartPrerendering(FrameTreeNodeId());
}
return cancelled_ids;
}
bool PrerenderHostRegistry::CancelHost(FrameTreeNodeId frame_tree_node_id,
PrerenderFinalStatus final_status) {
return CancelHost(frame_tree_node_id,
PrerenderCancellationReason(final_status));
}
bool PrerenderHostRegistry::CancelHost(
FrameTreeNodeId frame_tree_node_id,
const PrerenderCancellationReason& reason) {
TRACE_EVENT1("navigation", "PrerenderHostRegistry::CancelHost",
"frame_tree_node_id", frame_tree_node_id);
std::set<FrameTreeNodeId> cancelled_ids =
CancelHosts({frame_tree_node_id}, reason);
return !cancelled_ids.empty();
}
void PrerenderHostRegistry::CancelHostsForTriggers(
std::vector<PreloadingTriggerType> trigger_types,
const PrerenderCancellationReason& reason) {
TRACE_EVENT1("navigation", "PrerenderHostRegistry::CancelHostsForTrigger",
"trigger_type", trigger_types[0]);
std::vector<FrameTreeNodeId> ids_to_be_deleted;
for (auto& iter : prerender_host_by_frame_tree_node_id_) {
if (base::Contains(trigger_types, iter.second->trigger_type())) {
ids_to_be_deleted.push_back(iter.first);
}
}
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
for (auto& iter : prerender_new_tab_handle_by_frame_tree_node_id_) {
if (base::Contains(trigger_types, iter.second->trigger_type())) {
// Prerendering into a new tab can be triggered by speculation rules
// only.
CHECK(IsSpeculationRuleType(iter.second->trigger_type()));
ids_to_be_deleted.push_back(iter.first);
}
}
} else {
CHECK(prerender_new_tab_handle_by_frame_tree_node_id_.empty());
}
CancelHosts(ids_to_be_deleted, reason);
}
void PrerenderHostRegistry::CancelAllHosts(PrerenderFinalStatus final_status) {
// Cancel must not be requested during activation.
CHECK(!reserved_prerender_host_);
PrerenderCancellationReason reason(final_status);
while (!prerender_host_by_frame_tree_node_id_.empty()) {
CancelHostInternal(prerender_host_by_frame_tree_node_id_.begin()->first,
reason);
}
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
while (!prerender_new_tab_handle_by_frame_tree_node_id_.empty()) {
CancelNewTabHostInternal(
prerender_new_tab_handle_by_frame_tree_node_id_.begin()->first,
reason);
}
} else {
CHECK(prerender_new_tab_handle_by_frame_tree_node_id_.empty());
}
pending_prerenders_.clear();
}
bool PrerenderHostRegistry::CancelHostInternal(
FrameTreeNodeId frame_tree_node_id,
const PrerenderCancellationReason& reason) {
// Look up the id in the non-reserved host map.
auto iter = prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id);
if (iter == prerender_host_by_frame_tree_node_id_.end()) {
return false;
}
if (running_prerender_host_id_ == frame_tree_node_id) {
running_prerender_host_id_ = FrameTreeNodeId();
}
// Remove the prerender host from the host map so that it's not used for
// activation during asynchronous deletion.
std::unique_ptr<PrerenderHost> prerender_host = std::move(iter->second);
prerender_host_by_frame_tree_node_id_.erase(iter);
reason.ReportMetrics(prerender_host->GetHistogramSuffix());
NotifyCancel(prerender_host->frame_tree_node_id(), reason);
// Under kPrerender2InNewTab, if the host we are attempting to cancel is the
// new-tab host and initiator WebContents's PrerenderHostRegistry for this
// host is still alive, invoke the initiator WebContents's
// CancelNewTabHostInternal to destroy PrerenderNewTabHandle and WebContents
// that this new-tab host belongs to. This will eventually destroy `this`, so
// it should be performed asynchronously.
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
WebContentsImpl* initiator_web_contents = static_cast<WebContentsImpl*>(
prerender_host->initiator_web_contents().get());
// The initiator WebContents may not be alive.
// See crrev.com/c/6286546/comment/1adfe28c_4f769aa7 for more details.
if (initiator_web_contents && web_contents() != initiator_web_contents &&
!initiator_web_contents->IsBeingDestroyed()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
base::IgnoreResult(
&PrerenderHostRegistry::CancelNewTabHostInternal),
initiator_web_contents->GetPrerenderHostRegistry()->GetWeakPtr(),
frame_tree_node_id,
PrerenderCancellationReason(reason.final_status())));
}
}
// Asynchronously delete the prerender host.
ScheduleToDeleteAbandonedHost(std::move(prerender_host), reason);
return true;
}
bool PrerenderHostRegistry::CancelNewTabHostInternal(
FrameTreeNodeId frame_tree_node_id,
const PrerenderCancellationReason& reason) {
CHECK(base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab));
// Look up the id in the prerender-in-new-tab handle map.
auto iter =
prerender_new_tab_handle_by_frame_tree_node_id_.find(frame_tree_node_id);
if (iter == prerender_new_tab_handle_by_frame_tree_node_id_.end()) {
return false;
}
// The host should be driven by PrerenderHostRegistry associated with
// the new tab.
CHECK_NE(running_prerender_host_id_, frame_tree_node_id);
std::unique_ptr<PrerenderNewTabHandle> handle = std::move(iter->second);
prerender_new_tab_handle_by_frame_tree_node_id_.erase(iter);
NotifyCancel(frame_tree_node_id, reason);
if (reason.final_status() == PrerenderFinalStatus::kSpeculationRuleRemoved) {
auto& new_tab_registry = handle->GetPrerenderHostRegistry();
new_tab_registry.SchedulePendingDeletionPrerenderNewTabHandle(
std::move(handle));
new_tab_registry.CancelHost(frame_tree_node_id, reason);
} else {
handle->CancelPrerendering(reason);
}
return true;
}
FrameTreeNodeId PrerenderHostRegistry::FindPotentialHostToActivate(
NavigationRequest& navigation_request) {
TRACE_EVENT2(
"navigation", "PrerenderHostRegistry::FindPotentialHostToActivate",
"navigation_url", navigation_request.GetURL().spec(), "render_frame_host",
navigation_request.frame_tree_node()->current_frame_host());
// Disallow activation when the navigation is for a nested browsing context
// (e.g., iframes, fenced frames). This is because nested browsing contexts
// such as iframes are supposed to be created in the parent's browsing context
// group and can script with the parent, but prerendered pages are created in
// new browsing context groups.
//
// Also, disallow activation when the navigation happens in the prerendering
// frame tree.
if (!navigation_request.IsInPrimaryMainFrame()) {
return FrameTreeNodeId();
}
// Collect hosts that can match the navigation request.
std::vector<PrerenderHost*> matchable_hosts;
// First, collect hosts that can exactly match or match with the
// No-Vary-Search header.
for (const auto& [host_id, host] : prerender_host_by_frame_tree_node_id_) {
if (host->IsUrlMatch(navigation_request.GetURL())) {
matchable_hosts.push_back(host.get());
}
}
// Then, collect hosts that can match with the No-Vary-Search hint.
for (const auto& [host_id, host] : prerender_host_by_frame_tree_node_id_) {
if (host->IsNoVarySearchHintUrlMatch(navigation_request.GetURL())) {
matchable_hosts.push_back(host.get());
}
}
RecordPotentialPrerenderProcessReuse(!matchable_hosts.empty(),
navigation_request.GetURL());
if (matchable_hosts.empty()) {
return FrameTreeNodeId();
}
// Use the first match. This prioritizes the exact match or No-Vary-Search
// header match than No-Vary-Search hint match.
PrerenderHost* host = *matchable_hosts.begin();
base::UmaHistogramCounts100(
"Prerender.Experimental.MatchableHostCountOnActivation",
matchable_hosts.size());
// Cannot activate if prerendering navigation has not started yet.
if (!host->GetInitialNavigationId().has_value()) {
CancelHost(host->frame_tree_node_id(),
PrerenderFinalStatus::kActivatedBeforeStarted);
return FrameTreeNodeId();
}
return CanNavigationActivateHost(navigation_request, *host)
? host->frame_tree_node_id()
: FrameTreeNodeId();
}
std::optional<ReservedPrerenderHostInfo>
PrerenderHostRegistry::ReserveHostToActivate(
NavigationRequest& navigation_request,
FrameTreeNodeId expected_host_id) {
RenderFrameHostImpl* render_frame_host =
navigation_request.frame_tree_node()->current_frame_host();
TRACE_EVENT2("navigation", "PrerenderHostRegistry::ReserveHostToActivate",
"navigation_url", navigation_request.GetURL().spec(),
"render_frame_host", render_frame_host);
CHECK(navigation_request.IsInPrimaryMainFrame());
// Choose the host that NavigationRequest expects.
//
// Note that other prerendered pages may match this NavigationRequest but
// we shouldn't activate them. NavigationRequest makes sure that the
// expected prerendered page is ready for activation by waiting for
// PrerenderCommitDeferringCondition before this point, while the new
// matched pages may not be ready for activation yet.
auto it = prerender_host_by_frame_tree_node_id_.find(expected_host_id);
if (it == prerender_host_by_frame_tree_node_id_.end()) {
return std::nullopt;
}
PrerenderHost& host_ref = *it->second;
// The expected host does not match. This can happen when the host matches
// based on the No-Vary-Search hint but actually it does not match based on
// the No-Vary-Search header.
std::optional<UrlMatchType> match_type =
host_ref.IsUrlMatch(navigation_request.GetURL());
if (!match_type.has_value()) {
return std::nullopt;
}
if (!CanNavigationActivateHost(navigation_request, host_ref)) {
return std::nullopt;
}
FrameTreeNodeId host_id = host_ref.frame_tree_node_id();
// Disallow activation when ongoing navigations exist. It can happen when the
// main frame navigation starts after PrerenderCommitDeferringCondition posts
// a task to resume activation and before the activation is completed.
auto& prerender_frame_tree = host_ref.GetPrerenderFrameTree();
if (prerender_frame_tree.root()->HasNavigation()) {
CancelHost(host_id,
PrerenderFinalStatus::kActivatedDuringMainFrameNavigation);
return std::nullopt;
}
// Remove the host from the map of non-reserved hosts.
std::unique_ptr<PrerenderHost> host =
std::move(prerender_host_by_frame_tree_node_id_[host_id]);
prerender_host_by_frame_tree_node_id_.erase(host_id);
CHECK_EQ(host_id, host->frame_tree_node_id());
CHECK(host->IsUrlMatch(navigation_request.GetURL()));
if (match_type.value() == UrlMatchType::kNoVarySearch) {
// Count use of No-Vary-Search header in prerender.
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
web_contents()->GetPrimaryMainFrame(),
blink::mojom::WebFeature::kNoVarySearchPrerender);
}
// Reserve the host for activation.
CHECK(!reserved_prerender_host_);
reserved_prerender_host_ = std::move(host);
return ReservedPrerenderHostInfo(
host_id, reserved_prerender_host_->trigger_type(),
reserved_prerender_host_->embedder_histogram_suffix(),
reserved_prerender_host_->host_reused());
}
RenderFrameHostImpl* PrerenderHostRegistry::GetRenderFrameHostForReservedHost(
FrameTreeNodeId frame_tree_node_id) {
if (!reserved_prerender_host_)
return nullptr;
CHECK_EQ(frame_tree_node_id, reserved_prerender_host_->frame_tree_node_id());
return reserved_prerender_host_->GetPrerenderedMainFrameHost();
}
std::unique_ptr<StoredPage> PrerenderHostRegistry::ActivateReservedHost(
FrameTreeNodeId frame_tree_node_id,
NavigationRequest& navigation_request) {
CHECK(reserved_prerender_host_);
CHECK_EQ(frame_tree_node_id, reserved_prerender_host_->frame_tree_node_id());
std::unique_ptr<PrerenderHost> prerender_host =
std::move(reserved_prerender_host_);
return prerender_host->Activate(navigation_request);
}
void PrerenderHostRegistry::OnActivationFinished(
FrameTreeNodeId frame_tree_node_id) {
// OnActivationFinished() should not be called for non-reserved hosts.
CHECK(!base::Contains(prerender_host_by_frame_tree_node_id_,
frame_tree_node_id));
if (!reserved_prerender_host_) {
// The activation finished successfully and has already activated the
// reserved host.
return;
}
// The activation navigation is cancelled before activating the prerendered
// page, which means the activation failed.
CHECK_EQ(frame_tree_node_id, reserved_prerender_host_->frame_tree_node_id());
// TODO(crbug.com/40243805): Monitor the final status metric and see
// whether it could be possible.
ScheduleToDeleteAbandonedHost(
std::move(reserved_prerender_host_),
PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationDestroyedBeforeSuccess));
}
PrerenderHost* PrerenderHostRegistry::FindNonReservedHostById(
FrameTreeNodeId frame_tree_node_id) {
auto id_iter = prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id);
if (id_iter == prerender_host_by_frame_tree_node_id_.end())
return nullptr;
return id_iter->second.get();
}
bool PrerenderHostRegistry::HasReservedHost() const {
return !!reserved_prerender_host_;
}
std::unique_ptr<WebContentsImpl>
PrerenderHostRegistry::TakePreCreatedWebContentsForNewTabIfExists(
const mojom::CreateNewWindowParams& create_new_window_params,
const WebContents::CreateParams& web_contents_create_params) {
CHECK(base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab));
// Don't serve a prerendered page if the window needs the opener or is created
// for non-regular navigations.
if (!create_new_window_params.opener_suppressed ||
create_new_window_params.is_form_submission ||
create_new_window_params.pip_options) {
return nullptr;
}
for (auto& iter : prerender_new_tab_handle_by_frame_tree_node_id_) {
std::unique_ptr<WebContentsImpl> web_contents =
iter.second->TakeWebContentsIfAvailable(create_new_window_params,
web_contents_create_params);
if (web_contents) {
prerender_new_tab_handle_by_frame_tree_node_id_.erase(iter);
return web_contents;
}
}
return nullptr;
}
std::vector<FrameTree*> PrerenderHostRegistry::GetPrerenderFrameTrees() {
std::vector<FrameTree*> result;
for (auto& i : prerender_host_by_frame_tree_node_id_) {
result.push_back(&i.second->GetPrerenderFrameTree());
}
if (reserved_prerender_host_)
result.push_back(&reserved_prerender_host_->GetPrerenderFrameTree());
return result;
}
PrerenderHost* PrerenderHostRegistry::FindHostByUrlForTesting(
const GURL& prerendering_url) {
for (auto& iter : prerender_host_by_frame_tree_node_id_) {
if (iter.second->IsUrlMatch(prerendering_url)) {
return iter.second.get();
}
}
return nullptr;
}
PrerenderHost* PrerenderHostRegistry::FindPrewarmSearchResultHostForTesting(
const GURL& search_prewarm_url) {
for (auto& iter : prerender_host_by_frame_tree_node_id_) {
if (iter.second->GetInitialUrl() == search_prewarm_url) {
return iter.second.get();
}
}
return nullptr;
}
bool PrerenderHostRegistry::HasNewTabHandleByIdForTesting(
FrameTreeNodeId frame_tree_node_id) {
return prerender_new_tab_handle_by_frame_tree_node_id_.contains(
frame_tree_node_id);
}
void PrerenderHostRegistry::CancelAllHostsForTesting() {
CHECK(!reserved_prerender_host_)
<< "It is not possible to cancel a reserved host, so they must not exist "
"when trying to cancel all hosts";
for (auto& iter : prerender_host_by_frame_tree_node_id_) {
// Asynchronously delete the prerender host.
ScheduleToDeleteAbandonedHost(
std::move(iter.second),
PrerenderCancellationReason(
PrerenderFinalStatus::kCancelAllHostsForTesting));
}
// After we're done scheduling deletion, clear the map and the pending queue.
prerender_host_by_frame_tree_node_id_.clear();
pending_prerenders_.clear();
}
void PrerenderHostRegistry::BackNavigationLikely(
PreloadingPredictor predictor) {
if (http_cache_query_loader_) {
return;
}
PreloadingDataImpl* preloading_data =
PreloadingDataImpl::GetOrCreateForWebContents(web_contents());
preloading_data->SetIsNavigationInDomainCallback(
predictor,
base::BindRepeating(IsNavigationInSessionHistoryPredictorDomain));
WebContentsImpl* contents = static_cast<WebContentsImpl*>(web_contents());
NavigationControllerImpl& controller = contents->GetController();
const std::optional<int> target_index = controller.GetIndexForGoBack();
if (!target_index.has_value()) {
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kNoBackEntry, nullptr);
return;
}
NavigationEntryImpl* back_entry = controller.GetEntryAtIndex(*target_index);
CHECK(back_entry);
const GURL& back_url = back_entry->GetURL();
if (controller.GetBackForwardCache()
.GetOrEvictEntry(back_entry->GetUniqueID())
.has_value()) {
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kBfcacheEntryExists,
nullptr);
return;
}
PreloadingURLMatchCallback same_url_matcher =
PreloadingData::GetSameURLMatcher(back_url);
ukm::SourceId triggered_primary_page_source_id =
web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId();
preloading_data->AddPreloadingPrediction(predictor, PreloadingConfidence{100},
same_url_matcher,
triggered_primary_page_source_id);
PreloadingAttempt* attempt = preloading_data->AddPreloadingAttempt(
predictor, PreloadingType::kPrerender, same_url_matcher,
triggered_primary_page_source_id);
if (back_entry->GetMainFrameDocumentSequenceNumber() ==
controller.GetLastCommittedEntry()
->GetMainFrameDocumentSequenceNumber()) {
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kTargetIsSameDocument,
attempt);
return;
}
if (back_entry->root_node()->frame_entry->method() != "GET") {
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kMethodNotGet, attempt);
return;
}
if (prerender_navigation_utils::IsDisallowedHttpResponseCode(
back_entry->GetHttpStatusCode())) {
RecordPrerenderBackNavigationEligibility(
predictor,
PrerenderBackNavigationEligibility::kTargetIsFailedNavigation, attempt);
return;
}
if (!back_url.SchemeIsHTTPOrHTTPS()) {
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kTargetIsNonHttp,
attempt);
return;
}
// While same site back navigations could potentially be prerendered, doing so
// would involve more significant compat risk. For now, we consider them
// ineligible. See https://crbug.com/1422266 .
if (prerender_navigation_utils::IsSameSite(
back_url,
contents->GetPrimaryMainFrame()->GetLastCommittedOrigin())) {
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kTargetIsSameSite,
attempt);
return;
}
// Session history prerendering will reuse the back navigation entry's
// existing SiteInstance. We can't have a prerendered document share a
// SiteInstance with related active content (i.e. an active document with the
// same BrowsingInstance) as that would risk having scripting connections to
// prerendered documents. So this case is not eligible for prerendering.
SiteInstanceImpl* entry_site_instance = back_entry->site_instance();
// `entry_site_instance` could be null in cases such as session restore.
if (entry_site_instance) {
const bool current_and_target_related =
contents->GetSiteInstance()->IsRelatedSiteInstance(entry_site_instance);
const size_t allowable_related_count = current_and_target_related ? 1u : 0u;
if (entry_site_instance->GetRelatedActiveContentsCount() >
allowable_related_count) {
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kRelatedActiveContents,
attempt);
return;
}
}
// To determine whether the resource for the target entry is in the HTTP
// cache, we send a "fake" ResourceRequest which only loads from the cache.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
contents->GetPrimaryMainFrame()
->GetStoragePartition()
->GetURLLoaderFactoryForBrowserProcess();
http_cache_query_loader_ = CreateHttpCacheQueryingResourceLoad(back_url);
http_cache_query_loader_->DownloadHeadersOnly(
url_loader_factory.get(),
base::BindOnce(&PrerenderHostRegistry::OnBackResourceCacheResult,
base::Unretained(this), predictor, attempt->GetWeakPtr(),
back_url));
}
void PrerenderHostRegistry::OnBackResourceCacheResult(
PreloadingPredictor predictor,
base::WeakPtr<PreloadingAttempt> attempt,
GURL back_url,
scoped_refptr<net::HttpResponseHeaders> headers) {
// It's safe to delete the SimpleURLLoader while running the callback that was
// passed to it. We do so once we're done with it in this method.
std::unique_ptr<network::SimpleURLLoader> http_cache_query_loader =
std::move(http_cache_query_loader_);
if (!http_cache_query_loader->LoadedFromCache()) {
// If not in the cache, then this cache-only request must have failed.
CHECK_NE(http_cache_query_loader->NetError(), net::OK);
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kNoHttpCacheEntry,
attempt.get());
return;
}
RecordPrerenderBackNavigationEligibility(
predictor, PrerenderBackNavigationEligibility::kEligible, attempt.get());
if (attempt) {
attempt->SetHoldbackStatus(PreloadingHoldbackStatus::kAllowed);
// At this point, we are only collecting metrics and not actually
// prerendering anything.
attempt->SetTriggeringOutcome(PreloadingTriggeringOutcome::kNoOp);
}
}
base::WeakPtr<PrerenderHostRegistry> PrerenderHostRegistry::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
PrerenderHostId PrerenderHostRegistry::GetPrerenderHostIdForNavigation(
NavigationRequest* navigation_request) {
PrerenderHost* prerender_host = nullptr;
if (navigation_request->IsInPrerenderedMainFrame()) {
// This navigation is running on the main frame in the prerendered page, so
// its FrameTree::Delegate should be PrerenderHost.
prerender_host = &PrerenderHost::GetFromFrameTreeNode(
*navigation_request->frame_tree_node());
} else {
// Since the navigation in the fenced frames are deferred until the
// activation, we do not need to check the outermost main frame for
// navigation requests in prerendered pages.
FrameTreeNodeId main_frame_host_id = navigation_request->frame_tree_node()
->frame_tree()
.root()
->frame_tree_node_id();
prerender_host = FindNonReservedHostById(main_frame_host_id);
}
if (prerender_host) {
return prerender_host->prerender_host_id();
}
return PrerenderHostId();
}
void PrerenderHostRegistry::DidStartNavigation(
NavigationHandle* navigation_handle) {
// DidStartNavigation is used for monitoring the main frame navigation in a
// prerendered page so do nothing for other navigations.
auto* navigation_request = NavigationRequest::From(navigation_handle);
if (!navigation_request->IsInPrerenderedMainFrame() ||
navigation_request->IsSameDocument()) {
return;
}
// This navigation is running on the main frame in the prerendered page, so
// its FrameTree::Delegate should be PrerenderHost.
auto& prerender_host = PrerenderHost::GetFromFrameTreeNode(
*navigation_request->frame_tree_node());
prerender_host.DidStartNavigation(navigation_handle);
}
void PrerenderHostRegistry::ReadyToCommitNavigation(
NavigationHandle* navigation_handle) {
// ReadyToCommitNavigation is used for monitoring the main frame navigation in
// a prerendered page so do nothing for other navigations.
auto* navigation_request = NavigationRequest::From(navigation_handle);
if (!navigation_request->IsInPrerenderedMainFrame() ||
navigation_request->IsSameDocument()) {
return;
}
// This navigation is running on the main frame in the prerendered page, so
// its FrameTree::Delegate should be PrerenderHost.
auto& prerender_host = PrerenderHost::GetFromFrameTreeNode(
*navigation_request->frame_tree_node());
prerender_host.ReadyToCommitNavigation(navigation_handle);
}
void PrerenderHostRegistry::DidFinishNavigation(
NavigationHandle* navigation_handle) {
auto* navigation_request = NavigationRequest::From(navigation_handle);
if (navigation_request->IsSameDocument())
return;
FrameTreeNodeId main_frame_host_id = navigation_request->frame_tree_node()
->frame_tree()
.root()
->frame_tree_node_id();
PrerenderHost* prerender_host = FindNonReservedHostById(main_frame_host_id);
if (!prerender_host)
return;
prerender_host->DidFinishNavigation(navigation_handle);
if (running_prerender_host_id_ == main_frame_host_id) {
running_prerender_host_id_ = FrameTreeNodeId();
StartPrerendering(FrameTreeNodeId());
}
}
void PrerenderHostRegistry::OnVisibilityChanged(Visibility visibility) {
// Update the timer for prerendering timeout in the background.
if (IsBackground(visibility)) {
if (timeout_timer_for_embedder_.IsRunning() ||
timeout_timer_for_speculation_rules_.IsRunning()) {
// Keep the timers which started on a previous visibility change.
return;
}
// Keep a prerendered page alive in the background when its visibility
// state changes to HIDDEN or OCCLUDED.
timeout_timer_for_embedder_.SetTaskRunner(GetTimerTaskRunner());
timeout_timer_for_speculation_rules_.SetTaskRunner(GetTimerTaskRunner());
// Cancel PrerenderHost in the background when it exceeds a certain
// amount of time. The timeout differs depending on the trigger type.
timeout_timer_for_embedder_.Start(
FROM_HERE, kTimeToLiveInBackgroundForEmbedder,
base::BindOnce(&PrerenderHostRegistry::CancelHostsForTriggers,
base::Unretained(this),
std::vector({PreloadingTriggerType::kEmbedder}),
PrerenderCancellationReason(
PrerenderFinalStatus::kTimeoutBackgrounded)));
timeout_timer_for_speculation_rules_.Start(
FROM_HERE, kTimeToLiveInBackgroundForSpeculationRules,
base::BindOnce(
&PrerenderHostRegistry::CancelHostsForTriggers,
base::Unretained(this),
std::vector(
{PreloadingTriggerType::kSpeculationRule,
PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld}),
PrerenderCancellationReason(
PrerenderFinalStatus::kTimeoutBackgrounded)));
} else {
// Stop the timer when a prerendered page gets visible to users.
timeout_timer_for_embedder_.Stop();
timeout_timer_for_speculation_rules_.Stop();
// Start the next prerender if needed.
if (running_prerender_host_id_.is_null()) {
StartPrerendering(FrameTreeNodeId());
}
}
}
void PrerenderHostRegistry::PrimaryMainFrameRenderProcessGone(
base::TerminationStatus status) {
CancelAllHosts(
status == base::TERMINATION_STATUS_PROCESS_CRASHED
? PrerenderFinalStatus::kPrimaryMainFrameRendererProcessCrashed
: PrerenderFinalStatus::kPrimaryMainFrameRendererProcessKilled);
}
bool PrerenderHostRegistry::CanNavigationActivateHost(
NavigationRequest& navigation_request,
PrerenderHost& host) {
RenderFrameHostImpl* render_frame_host =
navigation_request.frame_tree_node()->current_frame_host();
TRACE_EVENT2("navigation", "PrerenderHostRegistry::CanNavigationActivateHost",
"navigation_url", navigation_request.GetURL().spec(),
"render_frame_host", render_frame_host);
// Disallow activation when the navigation URL has an effective URL like
// hosted apps and NTP.
if (SiteInstanceImpl::HasEffectiveURL(web_contents()->GetBrowserContext(),
navigation_request.GetURL())) {
CancelHost(host.frame_tree_node_id(),
PrerenderFinalStatus::kActivationUrlHasEffectiveUrl);
return false;
}
// Disallow activation when other auxiliary browsing contexts (e.g., pop-up
// windows) exist in the same browsing context group. This is because these
// browsing contexts should be able to script each other, but prerendered
// pages are created in new browsing context groups.
SiteInstance* site_instance = render_frame_host->GetSiteInstance();
if (site_instance->GetRelatedActiveContentsCount() != 1u) {
CancelHost(host.frame_tree_node_id(),
PrerenderFinalStatus::kActivatedWithAuxiliaryBrowsingContexts);
return false;
}
// TODO(crbug.com/40249964): Remove the restriction after further
// investigation and discussion. Disallow activation when the navigation
// happens in the hidden tab.
if (web_contents()->GetVisibility() == Visibility::HIDDEN &&
!IsAllowedToActivateInBackgroundForTesting()) {
CancelHost(host.frame_tree_node_id(),
PrerenderFinalStatus::kActivatedInBackground);
return false;
}
{
PrerenderCancellationReason reason = PrerenderCancellationReason::
CreateCandidateReasonForActivationParameterMismatch();
// Compare navigation params from activation with the navigation params
// from the initial prerender navigation. If they don't match, the
// navigation should not activate the prerendered page.
if (!host.AreInitialPrerenderNavigationParamsCompatibleWithNavigation(
navigation_request, reason)) {
// TODO(lingqi): We'd better cancel all hosts.
CancelHost(host.frame_tree_node_id(), reason);
return false;
}
}
if (!host.IsFramePolicyCompatibleWithPrimaryFrameTree()) {
CancelHost(host.frame_tree_node_id(),
PrerenderFinalStatus::kActivationFramePolicyNotCompatible);
return false;
}
// Cancel all the other prerender hosts because we no longer need the other
// hosts after we determine the host to be activated.
std::vector<FrameTreeNodeId> cancelled_prerenders;
for (const auto& [host_id, _] : prerender_host_by_frame_tree_node_id_) {
if (host_id != host.frame_tree_node_id()) {
cancelled_prerenders.push_back(host_id);
}
}
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
for (const auto& [host_id, _] :
prerender_new_tab_handle_by_frame_tree_node_id_) {
cancelled_prerenders.push_back(host_id);
}
} else {
CHECK(prerender_new_tab_handle_by_frame_tree_node_id_.empty());
}
CancelHosts(cancelled_prerenders,
PrerenderCancellationReason(
PrerenderFinalStatus::kOtherPrerenderedPageActivated));
pending_prerenders_.clear();
return true;
}
void PrerenderHostRegistry::DeletePendingDeletionHosts(
FrameTreeNodeId prerender_host_id) {
// Avoid directly destructing the host in the map. See the comments in
// `DestructPrerenderHosts()` for details.
std::unique_ptr<PrerenderHost> prerender_host =
std::move(pending_deletion_hosts_[prerender_host_id]);
pending_deletion_hosts_.erase(prerender_host_id);
DestructPrerenderHosts(prerender_host);
if (pending_deletion_new_tab_prerender_handle_) {
// Delete the handle asynchronously to avoid delete `this`, as the handle
// owns the prerender WebContents, which indirectly owns this
// PrerenderHostRegistry.
base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon(
FROM_HERE, std::move(pending_deletion_new_tab_prerender_handle_));
}
}
void PrerenderHostRegistry::SchedulePendingDeletionPrerenderNewTabHandle(
std::unique_ptr<PrerenderNewTabHandle> handle) {
CHECK(!pending_deletion_new_tab_prerender_handle_);
pending_deletion_new_tab_prerender_handle_ = std::move(handle);
}
void PrerenderHostRegistry::ScheduleToDeleteAbandonedHost(
std::unique_ptr<PrerenderHost> prerender_host,
const PrerenderCancellationReason& cancellation_reason) {
prerender_host->RecordFailedFinalStatus(PassKey(), cancellation_reason);
if (base::FeatureList::IsEnabled(
blink::features::kPageHideEventForPrerender2)) {
// TODO(crbug.com/353628449): Support pagehide event dispatch for
// PrerenderFinalStatus::kWindowClosed.
if (cancellation_reason.final_status() ==
PrerenderFinalStatus::kSpeculationRuleRemoved) {
// Fire unload related events upon intended prerender cancellation.
RenderFrameHostImpl* rfhi = prerender_host->GetPrerenderedMainFrameHost();
FrameTreeNodeId prerender_host_id = prerender_host->frame_tree_node_id();
pending_deletion_hosts_[prerender_host_id] = std::move(prerender_host);
rfhi->ClosePage(RenderFrameHostImpl::ClosePageSource::kPrerenderDiscard,
base::BindRepeating(
&PrerenderHostRegistry::DeletePendingDeletionHosts,
weak_factory_.GetWeakPtr(), prerender_host_id));
return;
}
}
// Asynchronously delete the prerender host.
to_be_deleted_hosts_.push_back(std::move(prerender_host));
// A task has already been scheduled to delete the abandoned hosts.
if (to_be_deleted_hosts_.size() > 1) {
return;
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&PrerenderHostRegistry::DeleteAbandonedHosts,
weak_factory_.GetWeakPtr()));
}
void PrerenderHostRegistry::DeleteAbandonedHosts() {
DestructPrerenderHosts(to_be_deleted_hosts_);
}
void PrerenderHostRegistry::NotifyTrigger(const GURL& url) {
for (Observer& obs : observers_) {
obs.OnTrigger(url);
}
}
void PrerenderHostRegistry::NotifyCancel(
FrameTreeNodeId host_frame_tree_node_id,
const PrerenderCancellationReason& reason) {
for (Observer& obs : observers_) {
obs.OnCancel(host_frame_tree_node_id, reason);
}
}
PrerenderHostRegistry::PrerenderLimitGroup
PrerenderHostRegistry::GetPrerenderLimitGroup(
PreloadingTriggerType trigger_type,
std::optional<blink::mojom::SpeculationEagerness> eagerness) {
switch (trigger_type) {
case PreloadingTriggerType::kSpeculationRule:
case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld:
case PreloadingTriggerType::kSpeculationRuleFromAutoSpeculationRules:
CHECK(eagerness.has_value());
return IsImmediateSpeculationEagerness(eagerness.value())
? PrerenderLimitGroup::kSpeculationRulesImmediate
: PrerenderLimitGroup::kSpeculationRulesNonImmediate;
case PreloadingTriggerType::kEmbedder:
return PrerenderLimitGroup::kEmbedder;
}
}
int PrerenderHostRegistry::GetHostCountByLimitGroup(
PrerenderLimitGroup limit_group) {
int host_count = 0;
for (const auto& [_, host] : prerender_host_by_frame_tree_node_id_) {
if (GetPrerenderLimitGroup(host->trigger_type(), host->eagerness()) ==
limit_group) {
++host_count;
}
}
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
for (const auto& [_, handle] :
prerender_new_tab_handle_by_frame_tree_node_id_) {
if (GetPrerenderLimitGroup(handle->trigger_type(), handle->eagerness()) ==
limit_group) {
++host_count;
}
}
}
return host_count;
}
bool PrerenderHostRegistry::IsAllowedToStartPrerenderingForTrigger(
PreloadingTriggerType trigger_type,
std::optional<blink::mojom::SpeculationEagerness> eagerness) {
PrerenderLimitGroup limit_group =
GetPrerenderLimitGroup(trigger_type, eagerness);
// Apply the limit of maximum number of running prerenders per
// PrerenderLimitGroup.
switch (limit_group) {
case PrerenderLimitGroup::kSpeculationRulesImmediate: {
int host_count = GetHostCountByLimitGroup(limit_group);
return host_count < kMaxRunningSpeculationRulesImmediatePrerenders;
}
case PrerenderLimitGroup::kSpeculationRulesNonImmediate: {
int host_count = GetHostCountByLimitGroup(limit_group);
// When the limit on non-immediate speculation rules is reached, cancel
// the oldest host to allow a newly incoming trigger to start.
if (host_count >= kMaxRunningSpeculationRulesNonImmediatePrerenders) {
FrameTreeNodeId oldest_prerender_host_id;
// Find the oldest non-immediate prerender that has not been canceled
// yet.
do {
oldest_prerender_host_id =
non_immediate_prerender_host_id_by_arrival_order_.front();
non_immediate_prerender_host_id_by_arrival_order_.pop_front();
} while (
base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)
? !prerender_host_by_frame_tree_node_id_.contains(
oldest_prerender_host_id) &&
!prerender_new_tab_handle_by_frame_tree_node_id_.contains(
oldest_prerender_host_id)
: !prerender_host_by_frame_tree_node_id_.contains(
oldest_prerender_host_id));
CHECK(CancelHost(oldest_prerender_host_id,
PrerenderFinalStatus::
kMaxNumOfRunningNonImmediatePrerendersExceeded));
CHECK_LT(GetHostCountByLimitGroup(limit_group),
kMaxRunningSpeculationRulesNonImmediatePrerenders);
}
return true;
}
case PrerenderLimitGroup::kEmbedder:
return IsAllowedToStartPrerenderingForEmbedder();
}
}
void PrerenderHostRegistry::OnMemoryPressure(
base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level) {
// Ignore the memory pressure event if the memory control is disabled.
if (!base::FeatureList::IsEnabled(
blink::features::kPrerender2MemoryControls)) {
return;
}
switch (memory_pressure_level) {
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE:
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE:
break;
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL:
CancelAllHosts(PrerenderFinalStatus::kMemoryPressureAfterTriggered);
break;
}
}
scoped_refptr<base::SingleThreadTaskRunner>
PrerenderHostRegistry::GetTimerTaskRunner() {
return timer_task_runner_for_testing_
? timer_task_runner_for_testing_
: base::SingleThreadTaskRunner::GetCurrentDefault();
}
void PrerenderHostRegistry::SetTaskRunnerForTesting(
scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
timer_task_runner_for_testing_ = std::move(task_runner);
}
bool PrerenderHostRegistry::PrerenderCanBeStartedWhenInitiatorIsInBackground() {
// Allow at most 1 prerendering to be started if the initiator page is
// still in the background.
// There is a running prerender, so no extra prerender is allowed before
// this one is finished.
if (running_prerender_host_id_) {
return false;
}
// There are non-pending prerenders, which have finished the initial
// navigation and been waiting for activation. Don't start a new prerender.
if (prerender_host_by_frame_tree_node_id_.size() -
pending_prerenders_.size() >=
1) {
return false;
}
// One or more than prerenders for new tab finished or are running. Don't
// start a new prerender.
if (prerender_new_tab_handle_by_frame_tree_node_id_.size() >= 1) {
return false;
}
return true;
}
bool PrerenderHostRegistry::IsAllowedToStartPrerenderingForEmbedder() {
int host_count = GetHostCountByLimitGroup(PrerenderLimitGroup::kEmbedder);
return web_contents()->GetDelegate()->AllowedPrerenderingCount(
*web_contents()) > host_count;
}
void PrerenderHostRegistry::CancelHostsByOriginFilter(
const StoragePartition::StorageKeyMatcherFunction& storage_key_filter,
PrerenderFinalStatus final_status) {
std::vector<FrameTreeNodeId> ids_to_be_deleted;
for (const auto& [host_id, host] : prerender_host_by_frame_tree_node_id_) {
if (host->initiator_origin().has_value()) {
if (storage_key_filter.Run(blink::StorageKey::CreateFirstParty(
host->initiator_origin().value()))) {
ids_to_be_deleted.push_back(host_id);
}
}
}
for (const auto& [host_id, handle] :
prerender_new_tab_handle_by_frame_tree_node_id_) {
if (handle->initiator_origin().has_value()) {
if (storage_key_filter.Run(blink::StorageKey::CreateFirstParty(
handle->initiator_origin().value()))) {
ids_to_be_deleted.push_back(host_id);
}
} else {
CHECK(prerender_new_tab_handle_by_frame_tree_node_id_.empty());
}
}
if (!ids_to_be_deleted.empty()) {
CancelHosts(ids_to_be_deleted, PrerenderCancellationReason(final_status));
}
}
void PrerenderHostRegistry::RecordPotentialPrerenderProcessReuse(
bool kHasMatchableHosts,
const GURL& navigation_url) {
static constexpr char kPrerenderProcessReuseUMAName[] =
"Prerender.Experimental.PrerenderProcessReuseAvailability";
if (kHasMatchableHosts) {
base::UmaHistogramEnumeration(
kPrerenderProcessReuseUMAName,
PrerenderProcessReuseAvailability::kHasMatchableHosts);
return;
}
bool has_same_origin_host =
std::find_if(prerender_host_by_frame_tree_node_id_.begin(),
prerender_host_by_frame_tree_node_id_.end(),
[&navigation_url](const auto& pair) {
return pair.second->IsUrlSameOrigin(navigation_url);
}) != prerender_host_by_frame_tree_node_id_.end();
bool has_same_site_host =
std::find_if(prerender_host_by_frame_tree_node_id_.begin(),
prerender_host_by_frame_tree_node_id_.end(),
[&navigation_url](const auto& pair) {
return pair.second->IsUrlSameSite(navigation_url);
}) != prerender_host_by_frame_tree_node_id_.end();
PrerenderProcessReuseAvailability availability =
PrerenderProcessReuseAvailability::kNoSameOriginOrSiteHosts;
if (has_same_origin_host) {
availability = PrerenderProcessReuseAvailability::kHasSameOriginHosts;
} else if (has_same_site_host) {
availability = PrerenderProcessReuseAvailability::kHasSameSiteHosts;
}
base::UmaHistogramEnumeration(kPrerenderProcessReuseUMAName, availability);
}
std::unique_ptr<PrerenderHost>
PrerenderHostRegistry::FindAndTakePrerenderHostToReuse(
const PrerenderAttributes& attributes) {
const GURL prerender_url = attributes.prerendering_url;
auto iter = std::find_if(prerender_host_by_frame_tree_node_id_.begin(),
prerender_host_by_frame_tree_node_id_.end(),
[&prerender_url](const auto& pair) {
return pair.second->IsUrlSameSite(prerender_url) &&
pair.second->IsReusable();
});
if (iter != prerender_host_by_frame_tree_node_id_.end()) {
std::unique_ptr<PrerenderHost> reuse_host = std::move(iter->second);
prerender_host_by_frame_tree_node_id_.erase(iter);
reuse_host->NotifyReused();
return reuse_host;
}
return nullptr;
}
} // namespace content