blob: 46507bec7dca0998259636c78006097927129c7e [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/functional/callback_helpers.h"
#include "base/memory/memory_pressure_monitor.h"
#include "base/metrics/field_trial_params.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 "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_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/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/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/simple_url_loader.h"
#include "services/resource_coordinator/public/cpp/memory_instrumentation/global_memory_dump.h"
#include "services/resource_coordinator/public/cpp/memory_instrumentation/memory_instrumentation.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;
}
}
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(https://crbug.com/1470820): 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::AmountOfPhysicalMemoryMB() > 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) {
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;
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;
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;
}
bool IsDevToolsOpen(WebContents& web_contents) {
return DevToolsAgentHost::HasFor(&web_contents);
}
PreloadingEligibility ToEligibility(PrerenderFinalStatus status) {
switch (status) {
case PrerenderFinalStatus::kActivated:
case PrerenderFinalStatus::kDestroyed:
NOTREACHED_NORETURN();
case PrerenderFinalStatus::kLowEndDevice:
return PreloadingEligibility::kLowMemory;
case PrerenderFinalStatus::kInvalidSchemeRedirect:
NOTREACHED_NORETURN();
case PrerenderFinalStatus::kInvalidSchemeNavigation:
return PreloadingEligibility::kHttpOrHttpsOnly;
case PrerenderFinalStatus::kNavigationRequestBlockedByCsp:
case PrerenderFinalStatus::kMainFrameNavigation:
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_NORETURN();
case PrerenderFinalStatus::kTriggerBackgrounded:
return PreloadingEligibility::kHidden;
case PrerenderFinalStatus::kMemoryLimitExceeded:
NOTREACHED_NORETURN();
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_NORETURN();
case PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation:
return PreloadingEligibility::kCrossOrigin;
case PrerenderFinalStatus::
kSameSiteCrossOriginRedirectNotOptInInInitialNavigation:
case PrerenderFinalStatus::
kSameSiteCrossOriginNavigationNotOptInInInitialNavigation:
case PrerenderFinalStatus::kActivationNavigationParameterMismatch:
case PrerenderFinalStatus::kActivatedInBackground:
case PrerenderFinalStatus::kEmbedderHostDisallowed:
case PrerenderFinalStatus::kActivationNavigationDestroyedBeforeSuccess:
case PrerenderFinalStatus::kTabClosedByUserGesture:
case PrerenderFinalStatus::kTabClosedWithoutUserGesture:
case PrerenderFinalStatus::kPrimaryMainFrameRendererProcessCrashed:
case PrerenderFinalStatus::kPrimaryMainFrameRendererProcessKilled:
case PrerenderFinalStatus::kActivationFramePolicyNotCompatible:
NOTREACHED_NORETURN();
case PrerenderFinalStatus::kPreloadingDisabled:
return PreloadingEligibility::kPreloadingDisabled;
case PrerenderFinalStatus::kBatterySaverEnabled:
return PreloadingEligibility::kBatterySaverEnabled;
case PrerenderFinalStatus::kActivatedDuringMainFrameNavigation:
NOTREACHED_NORETURN();
case PrerenderFinalStatus::kPreloadingUnsupportedByWebContents:
return PreloadingEligibility::kPreloadingUnsupportedByWebContents;
case PrerenderFinalStatus::kCrossSiteRedirectInMainFrameNavigation:
case PrerenderFinalStatus::kCrossSiteNavigationInMainFrameNavigation:
case PrerenderFinalStatus::
kSameSiteCrossOriginRedirectNotOptInInMainFrameNavigation:
case PrerenderFinalStatus::
kSameSiteCrossOriginNavigationNotOptInInMainFrameNavigation:
NOTREACHED_NORETURN();
case PrerenderFinalStatus::kMemoryPressureOnTrigger:
return PreloadingEligibility::kMemoryPressure;
case PrerenderFinalStatus::kMemoryPressureAfterTriggered:
NOTREACHED_NORETURN();
case PrerenderFinalStatus::kPrerenderingDisabledByDevTools:
return PreloadingEligibility::kPreloadingDisabledByDevTools;
case PrerenderFinalStatus::kSpeculationRuleRemoved:
case PrerenderFinalStatus::kActivatedWithAuxiliaryBrowsingContexts:
case PrerenderFinalStatus::kMaxNumOfRunningEagerPrerendersExceeded:
case PrerenderFinalStatus::kMaxNumOfRunningNonEagerPrerendersExceeded:
case PrerenderFinalStatus::kMaxNumOfRunningEmbedderPrerendersExceeded:
NOTREACHED_NORETURN();
case PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl:
case PrerenderFinalStatus::kRedirectedPrerenderingUrlHasEffectiveUrl:
case PrerenderFinalStatus::kActivationUrlHasEffectiveUrl:
return PreloadingEligibility::kHasEffectiveUrl;
}
NOTREACHED_NORETURN();
}
// 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(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(https://crbug.com/1435376): Make this private again.
void Drop();
bool IsDropped();
private:
// 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() {
return devtools_attempt_ == nullptr;
}
std::unique_ptr<PrerenderHost> PrerenderHostBuilder::Build(
const PrerenderAttributes& attributes,
WebContentsImpl& prerender_web_contents) {
CHECK(!IsDropped());
auto prerender_host = std::make_unique<PrerenderHost>(
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();
}
} // namespace
const char kMaxNumOfRunningSpeculationRulesEagerPrerenders[] =
"max_num_of_running_speculation_rules_eager_prerenders";
const char kMaxNumOfRunningSpeculationRulesNonEagerPrerenders[] =
"max_num_of_running_speculation_rules_non_eager_prerenders";
const char kMaxNumOfRunningEmbedderPrerenders[] =
"max_num_of_running_embedder_prerenders";
PrerenderHostRegistry::PrerenderHostRegistry(WebContents& web_contents)
: memory_pressure_listener_(
FROM_HERE,
base::BindRepeating(&PrerenderHostRegistry::OnMemoryPressure,
base::Unretained(this))) {
Observe(&web_contents);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
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());
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
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);
Observe(nullptr);
for (Observer& obs : observers_)
obs.OnRegistryDestroyed();
}
void PrerenderHostRegistry::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void PrerenderHostRegistry::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
int PrerenderHostRegistry::CreateAndStartHost(
const PrerenderAttributes& attributes,
PreloadingAttempt* attempt) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
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));
int frame_tree_node_id = RenderFrameHost::kNoFrameTreeNodeId;
{
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.
base::ScopedClosureRunner notify_trigger(
base::BindOnce(&PrerenderHostRegistry::NotifyTrigger,
base::Unretained(this), attributes.prerendering_url));
auto builder = PrerenderHostBuilder(attempt);
// We don't know the root cause, but there is a case this is null.
//
// TODO(https://crbug.com/1435376): Continue investigation and fix the root
// cause.
if (initiator_web_contents.GetDelegate() == nullptr) {
// Note that return without consuming `builder` is exceptional.
builder.Drop();
return RenderFrameHost::kNoFrameTreeNodeId;
}
// Check whether preloading is enabled. If it is not enabled, report the
// reason.
switch (initiator_web_contents.GetDelegate()->IsPrerender2Supported(
initiator_web_contents)) {
case PreloadingEligibility::kEligible:
// nop
break;
case PreloadingEligibility::kPreloadingDisabled:
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kPreloadingDisabled);
return RenderFrameHost::kNoFrameTreeNodeId;
case PreloadingEligibility::kDataSaverEnabled:
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kDataSaverEnabled);
return RenderFrameHost::kNoFrameTreeNodeId;
case PreloadingEligibility::kBatterySaverEnabled:
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kBatterySaverEnabled);
return RenderFrameHost::kNoFrameTreeNodeId;
case PreloadingEligibility::kPreloadingUnsupportedByWebContents:
builder.RejectAsNotEligible(
attributes,
PrerenderFinalStatus::kPreloadingUnsupportedByWebContents);
return RenderFrameHost::kNoFrameTreeNodeId;
default:
NOTREACHED_NORETURN();
}
// 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 RenderFrameHost::kNoFrameTreeNodeId;
}
// Don't prerender on low-end devices.
if (!DeviceHasEnoughMemoryForPrerender()) {
builder.RejectAsNotEligible(attributes,
PrerenderFinalStatus::kLowEndDevice);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// 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 RenderFrameHost::kNoFrameTreeNodeId;
}
// 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/1176054): Support cross-site prerendering.
if (!attributes.IsBrowserInitiated() &&
!prerender_navigation_utils::IsSameSite(
attributes.prerendering_url, attributes.initiator_origin.value())) {
builder.RejectAsNotEligible(
attributes,
PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// 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 RenderFrameHost::kNoFrameTreeNodeId;
}
// 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 RenderFrameHost::kNoFrameTreeNodeId;
}
if (SiteInstanceImpl::HasEffectiveURL(browser_context,
attributes.prerendering_url)) {
builder.RejectAsNotEligible(
attributes, PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl);
return RenderFrameHost::kNoFrameTreeNodeId;
}
if (initiator_rfh && initiator_rfh->frame_tree() &&
!devtools_instrumentation::IsPrerenderAllowed(
*initiator_rfh->frame_tree())) {
builder.RejectAsNotEligible(
attributes, PrerenderFinalStatus::kPrerenderingDisabledByDevTools);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// 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 RenderFrameHost::kNoFrameTreeNodeId;
}
// 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 RenderFrameHost::kNoFrameTreeNodeId;
}
}
// 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/1355151): 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,
attributes.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,
attributes.eagerness)) {
case PrerenderLimitGroup::kSpeculationRulesEager:
final_status =
PrerenderFinalStatus::kMaxNumOfRunningEagerPrerendersExceeded;
break;
case PrerenderLimitGroup::kSpeculationRulesNonEager:
final_status =
PrerenderFinalStatus::kMaxNumOfRunningNonEagerPrerendersExceeded;
break;
case PrerenderLimitGroup::kEmbedder:
final_status =
PrerenderFinalStatus::kMaxNumOfRunningEmbedderPrerendersExceeded;
break;
}
builder.RejectAsFailure(attributes, final_status);
return RenderFrameHost::kNoFrameTreeNodeId;
}
auto prerender_host = builder.Build(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 (base::FeatureList::IsEnabled(
features::kPrerender2NewLimitAndScheduler)) {
if (GetPrerenderLimitGroup(attributes.trigger_type,
attributes.eagerness) ==
PrerenderLimitGroup::kSpeculationRulesNonEager) {
non_eager_prerender_host_id_by_arrival_order_.push_back(
frame_tree_node_id);
}
}
}
switch (attributes.trigger_type) {
case PreloadingTriggerType::kSpeculationRule:
case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld:
pending_prerenders_.push_back(frame_tree_node_id);
// 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 (running_prerender_host_id_ == RenderFrameHost::kNoFrameTreeNodeId) {
// No running prerender means that either no other prerenders are in the
// pending queue or the initiator continues to be in the background.
// Skip starting prerendering in the latter case.
if (IsBackground(initiator_web_contents.GetVisibility())) {
break;
}
CHECK_EQ(pending_prerenders_.size(), 1u);
int started_frame_tree_node_id =
StartPrerendering(RenderFrameHost::kNoFrameTreeNodeId);
CHECK(started_frame_tree_node_id == frame_tree_node_id ||
started_frame_tree_node_id ==
RenderFrameHost::kNoFrameTreeNodeId);
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
// `kNoFrameTreeNodeId` should be returned in that case.
frame_tree_node_id = StartPrerendering(frame_tree_node_id);
break;
}
return frame_tree_node_id;
}
int PrerenderHostRegistry::CreateAndStartHostForNewTab(
const PrerenderAttributes& attributes,
PreloadingPredictor preloading_predictor) {
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());
int prerender_host_id = handle->StartPrerendering(preloading_predictor);
if (prerender_host_id == RenderFrameHost::kNoFrameTreeNodeId)
return RenderFrameHost::kNoFrameTreeNodeId;
prerender_new_tab_handle_by_frame_tree_node_id_[prerender_host_id] =
std::move(handle);
if (base::FeatureList::IsEnabled(features::kPrerender2NewLimitAndScheduler)) {
if (GetPrerenderLimitGroup(attributes.trigger_type, attributes.eagerness) ==
PrerenderLimitGroup::kSpeculationRulesNonEager) {
non_eager_prerender_host_id_by_arrival_order_.push_back(
prerender_host_id);
}
}
return prerender_host_id;
}
int PrerenderHostRegistry::StartPrerendering(int frame_tree_node_id) {
// TODO(https://crbug.com/1424425): 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 == RenderFrameHost::kNoFrameTreeNodeId) {
CHECK_EQ(running_prerender_host_id_, RenderFrameHost::kNoFrameTreeNodeId);
while (!pending_prerenders_.empty()) {
int 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());
// Don't start the pending prerender triggered by the background tab.
if (IsBackground(
prerender_host->initiator_web_contents()->GetVisibility())) {
return RenderFrameHost::kNoFrameTreeNodeId;
}
// Found the request to run.
pending_prerenders_.pop_front();
frame_tree_node_id = host_id;
break;
}
if (frame_tree_node_id == RenderFrameHost::kNoFrameTreeNodeId) {
return RenderFrameHost::kNoFrameTreeNodeId;
}
}
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 RenderFrameHost::kNoFrameTreeNodeId;
}
switch (prerender_host_by_frame_tree_node_id_[frame_tree_node_id]
->trigger_type()) {
case PreloadingTriggerType::kSpeculationRule:
case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld:
// Check the current memory usage and destroy a prerendering if the entire
// browser uses excessive memory. This occurs asynchronously.
DestroyWhenUsingExcessiveMemory(frame_tree_node_id);
// Update the `running_prerender_host_id` to the starting prerender's id.
running_prerender_host_id_ = frame_tree_node_id;
break;
case PreloadingTriggerType::kEmbedder:
// We don't check the memory usage for embedder triggered prerenderings
// for now.
// `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<int> PrerenderHostRegistry::CancelHosts(
const std::vector<int>& 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<int> cancelled_ids;
for (int host_id : frame_tree_node_ids) {
// Look up the id in the non-reserved host map.
if (auto iter = prerender_host_by_frame_tree_node_id_.find(host_id);
iter != prerender_host_by_frame_tree_node_id_.end()) {
if (running_prerender_host_id_ == host_id)
running_prerender_host_id_ = RenderFrameHost::kNoFrameTreeNodeId;
// 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->trigger_type(),
prerender_host->embedder_histogram_suffix());
NotifyCancel(prerender_host->frame_tree_node_id(), reason);
// Asynchronously delete the prerender host.
ScheduleToDeleteAbandonedHost(std::move(prerender_host), reason);
cancelled_ids.insert(host_id);
}
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
// Look up the id in the prerender-in-new-tab handle map.
if (auto iter =
prerender_new_tab_handle_by_frame_tree_node_id_.find(host_id);
iter != prerender_new_tab_handle_by_frame_tree_node_id_.end()) {
// The host should be driven by PrerenderHostRegistry associated with
// the new tab.
CHECK_NE(running_prerender_host_id_, host_id);
std::unique_ptr<PrerenderNewTabHandle> handle = std::move(iter->second);
prerender_new_tab_handle_by_frame_tree_node_id_.erase(iter);
NotifyCancel(host_id, reason);
handle->CancelPrerendering(reason);
cancelled_ids.insert(host_id);
}
} else {
CHECK(prerender_new_tab_handle_by_frame_tree_node_id_.empty());
}
}
// Start another prerender if the running prerender is cancelled.
if (running_prerender_host_id_ == RenderFrameHost::kNoFrameTreeNodeId) {
StartPrerendering(RenderFrameHost::kNoFrameTreeNodeId);
}
return cancelled_ids;
}
bool PrerenderHostRegistry::CancelHost(int frame_tree_node_id,
PrerenderFinalStatus final_status) {
return CancelHost(frame_tree_node_id,
PrerenderCancellationReason(final_status));
}
bool PrerenderHostRegistry::CancelHost(
int frame_tree_node_id,
const PrerenderCancellationReason& reason) {
TRACE_EVENT1("navigation", "PrerenderHostRegistry::CancelHost",
"frame_tree_node_id", frame_tree_node_id);
std::set<int> 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<int> 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);
auto prerender_host_map = std::move(prerender_host_by_frame_tree_node_id_);
for (auto& iter : prerender_host_map) {
std::unique_ptr<PrerenderHost> prerender_host = std::move(iter.second);
ScheduleToDeleteAbandonedHost(std::move(prerender_host), reason);
}
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
auto prerender_new_tab_handle_map =
std::move(prerender_new_tab_handle_by_frame_tree_node_id_);
for (auto& iter : prerender_new_tab_handle_map)
iter.second->CancelPrerendering(reason);
} else {
CHECK(prerender_new_tab_handle_by_frame_tree_node_id_.empty());
}
pending_prerenders_.clear();
}
int 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());
return FindHostToActivateInternal(navigation_request);
}
int PrerenderHostRegistry::ReserveHostToActivate(
NavigationRequest& navigation_request,
int 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);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Find an available host for the navigation request.
int host_id = FindHostToActivateInternal(navigation_request);
if (host_id == RenderFrameHost::kNoFrameTreeNodeId)
return RenderFrameHost::kNoFrameTreeNodeId;
// Check if the host is what the NavigationRequest expects. The host can be
// different when a trigger page removes the existing prerender and then
// re-adds a new prerender for the same URL.
//
// NavigationRequest makes sure that the prerender is ready for activation by
// waiting for PrerenderCommitDeferringCondition before this point. Without
// this check, if the prerender is changed during the period,
// NavigationRequest may attempt to activate the new prerender that is not
// ready.
if (host_id != expected_host_id)
return RenderFrameHost::kNoFrameTreeNodeId;
// 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 = prerender_host_by_frame_tree_node_id_[host_id]
.get()
->GetPrerenderFrameTree();
if (prerender_frame_tree.root()->HasNavigation()) {
CancelHost(host_id,
PrerenderFinalStatus::kActivatedDuringMainFrameNavigation);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// 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());
// Reserve the host for activation.
CHECK(!reserved_prerender_host_);
reserved_prerender_host_ = std::move(host);
return host_id;
}
RenderFrameHostImpl* PrerenderHostRegistry::GetRenderFrameHostForReservedHost(
int 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(
int frame_tree_node_id,
NavigationRequest& navigation_request) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
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(int 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(https://crbug.com/1378151): Monitor the final status metric and see
// whether it could be possible.
ScheduleToDeleteAbandonedHost(
std::move(reserved_prerender_host_),
PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationDestroyedBeforeSuccess));
}
PrerenderHost* PrerenderHostRegistry::FindNonReservedHostById(
int 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;
}
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;
}
PreloadingData* preloading_data =
PreloadingData::GetOrCreateForWebContents(web_contents());
preloading_data->SetIsNavigationInDomainCallback(
predictor,
base::BindRepeating(IsNavigationInSessionHistoryPredictorDomain));
WebContentsImpl* contents = static_cast<WebContentsImpl*>(web_contents());
NavigationControllerImpl& controller = contents->GetController();
const absl::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);
preloading_data->AddPreloadingPrediction(predictor, /*confidence=*/100,
same_url_matcher);
PreloadingAttempt* attempt = preloading_data->AddPreloadingAttempt(
predictor, PreloadingType::kPrerender, same_url_matcher,
web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId());
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();
}
void PrerenderHostRegistry::DidStartNavigation(
NavigationHandle* navigation_handle) {
if (!base::FeatureList::IsEnabled(
blink::features::kPrerender2MainFrameNavigation)) {
return;
}
// 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 = static_cast<PrerenderHost*>(
navigation_request->frame_tree_node()->frame_tree().delegate());
CHECK(prerender_host);
prerender_host->DidStartNavigation(navigation_handle);
}
void PrerenderHostRegistry::DidFinishNavigation(
NavigationHandle* navigation_handle) {
auto* navigation_request = NavigationRequest::From(navigation_handle);
if (navigation_request->IsSameDocument())
return;
int 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_ = RenderFrameHost::kNoFrameTreeNodeId;
StartPrerendering(RenderFrameHost::kNoFrameTreeNodeId);
}
}
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_ == RenderFrameHost::kNoFrameTreeNodeId) {
StartPrerendering(RenderFrameHost::kNoFrameTreeNodeId);
}
}
}
void PrerenderHostRegistry::PrimaryMainFrameRenderProcessGone(
base::TerminationStatus status) {
CancelAllHosts(
status == base::TERMINATION_STATUS_PROCESS_CRASHED
? PrerenderFinalStatus::kPrimaryMainFrameRendererProcessCrashed
: PrerenderFinalStatus::kPrimaryMainFrameRendererProcessKilled);
}
int PrerenderHostRegistry::FindHostToActivateInternal(
NavigationRequest& navigation_request) {
RenderFrameHostImpl* render_frame_host =
navigation_request.frame_tree_node()->current_frame_host();
TRACE_EVENT2("navigation",
"PrerenderHostRegistry::FindHostToActivateInternal",
"navigation_url", navigation_request.GetURL().spec(),
"render_frame_host", render_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. And also, we disallow activation when the
// navigation is for a fenced frame to prevent the communication path from the
// embedding page to the fenced frame.
if (!navigation_request.IsInPrimaryMainFrame())
return RenderFrameHost::kNoFrameTreeNodeId;
// Disallow activation when the navigation happens in the prerendering frame
// tree.
if (navigation_request.IsInPrerenderedMainFrame())
return RenderFrameHost::kNoFrameTreeNodeId;
// Find an available host for the navigation URL.
PrerenderHost* host = nullptr;
for (const auto& [host_id, it_prerender_host] :
prerender_host_by_frame_tree_node_id_) {
if (it_prerender_host->IsUrlMatch(navigation_request.GetURL())) {
host = it_prerender_host.get();
break;
}
}
if (!host)
return RenderFrameHost::kNoFrameTreeNodeId;
// 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 RenderFrameHost::kNoFrameTreeNodeId;
}
// 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 RenderFrameHost::kNoFrameTreeNodeId;
}
// TODO(crbug.com/1399709): Remove the restriction after further investigation
// and discussion.
// Disallow activation when the navigation happens in the hidden tab.
if (web_contents()->GetVisibility() == Visibility::HIDDEN) {
CancelHost(host->frame_tree_node_id(),
PrerenderFinalStatus::kActivatedInBackground);
return RenderFrameHost::kNoFrameTreeNodeId;
}
if (!host->GetInitialNavigationId().has_value()) {
CancelHost(host->frame_tree_node_id(),
PrerenderFinalStatus::kActivatedBeforeStarted);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// 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 (std::unique_ptr<PrerenderMismatchedHeaders> mismatched_headers =
host->CheckInitialPrerenderNavigationParamsCompatibleWithNavigation(
navigation_request)) {
// TODO(https://crbug.com/1328365): Report a detailed reason to devtools.
// Currently users have to check
// Prerender.Experimental.ActivationNavigationParamsMatch.
// TODO(lingqi): We'd better cancel all hosts.
PrerenderCancellationReason reason = PrerenderCancellationReason::
BuildForActivationNavigationParameterMismatch(
std::move(mismatched_headers));
CancelHost(host->frame_tree_node_id(), reason);
return RenderFrameHost::kNoFrameTreeNodeId;
}
if (!host->IsFramePolicyCompatibleWithPrimaryFrameTree()) {
CancelHost(host->frame_tree_node_id(),
PrerenderFinalStatus::kActivationFramePolicyNotCompatible);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// Cancel all the other prerender hosts because we no longer need the other
// hosts after we determine the host to be activated.
std::vector<int> 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::kTriggerDestroyed));
pending_prerenders_.clear();
return host->frame_tree_node_id();
}
void PrerenderHostRegistry::ScheduleToDeleteAbandonedHost(
std::unique_ptr<PrerenderHost> prerender_host,
const PrerenderCancellationReason& cancellation_reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI))
<< "Post tasks to destroy PrerenderHosts on non-ui threads "
"with reason of "
<< static_cast<int>(cancellation_reason.final_status());
prerender_host->RecordFailedFinalStatus(PassKey(), cancellation_reason);
// Asynchronously delete the prerender host.
to_be_deleted_hosts_.push_back(std::move(prerender_host));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&PrerenderHostRegistry::DeleteAbandonedHosts,
weak_factory_.GetWeakPtr()));
}
void PrerenderHostRegistry::DeleteAbandonedHosts() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Swap the vector and let it scope out instead of directly destructing the
// hosts in the vector, for example, by `to_be_deleted_hosts_.clear()`. This
// avoids potential cases where a host being deleted indirectly modifies
// `to_be_deleted_hosts_` while the vector is being cleared up. See
// https://crbug.com/1431744 for contexts.
std::vector<std::unique_ptr<PrerenderHost>> hosts;
to_be_deleted_hosts_.swap(hosts);
}
void PrerenderHostRegistry::NotifyTrigger(const GURL& url) {
for (Observer& obs : observers_) {
obs.OnTrigger(url);
}
}
void PrerenderHostRegistry::NotifyCancel(
int host_frame_tree_node_id,
const PrerenderCancellationReason& reason) {
for (Observer& obs : observers_) {
obs.OnCancel(host_frame_tree_node_id, reason);
}
}
PreloadingTriggerType PrerenderHostRegistry::GetPrerenderTriggerType(
int frame_tree_node_id) {
CHECK(reserved_prerender_host_);
CHECK_EQ(reserved_prerender_host_->frame_tree_node_id(), frame_tree_node_id);
return reserved_prerender_host_->trigger_type();
}
const std::string& PrerenderHostRegistry::GetPrerenderEmbedderHistogramSuffix(
int frame_tree_node_id) {
CHECK(reserved_prerender_host_);
CHECK_EQ(reserved_prerender_host_->frame_tree_node_id(), frame_tree_node_id);
return reserved_prerender_host_->embedder_histogram_suffix();
}
PrerenderHostRegistry::PrerenderLimitGroup
PrerenderHostRegistry::GetPrerenderLimitGroup(
PreloadingTriggerType trigger_type,
absl::optional<blink::mojom::SpeculationEagerness> eagerness) {
switch (trigger_type) {
case PreloadingTriggerType::kSpeculationRule:
case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld:
CHECK(eagerness.has_value());
switch (eagerness.value()) {
// Separate the limits of speculation rules into two categories: eager,
// which are triggered immediately after adding the rule, and
// non-eager(moderate, conservative), which wait for a specific user
// action to trigger, aiming to apply the appropriate corresponding
// limits for these attributes.
case blink::mojom::SpeculationEagerness::kEager:
return PrerenderLimitGroup::kSpeculationRulesEager;
case blink::mojom::SpeculationEagerness::kModerate:
case blink::mojom::SpeculationEagerness::kConservative:
return PrerenderLimitGroup::kSpeculationRulesNonEager;
}
case PreloadingTriggerType::kEmbedder:
return PrerenderLimitGroup::kEmbedder;
}
}
int PrerenderHostRegistry::GetHostCountByTriggerType(
PreloadingTriggerType trigger_type) {
int host_count = 0;
for (const auto& [_, host] : prerender_host_by_frame_tree_node_id_) {
if (host->trigger_type() == trigger_type) {
++host_count;
}
}
if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) {
for (const auto& [_, handle] :
prerender_new_tab_handle_by_frame_tree_node_id_) {
if (handle->trigger_type() == trigger_type) {
++host_count;
}
}
}
return host_count;
}
int PrerenderHostRegistry::GetHostCountByLimitGroup(
PrerenderLimitGroup limit_group) {
CHECK(
base::FeatureList::IsEnabled(features::kPrerender2NewLimitAndScheduler));
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,
absl::optional<blink::mojom::SpeculationEagerness> eagerness) {
PrerenderLimitGroup limit_group;
int host_count;
if (base::FeatureList::IsEnabled(features::kPrerender2NewLimitAndScheduler)) {
limit_group = GetPrerenderLimitGroup(trigger_type, eagerness);
host_count = GetHostCountByLimitGroup(limit_group);
} else {
host_count = GetHostCountByTriggerType(trigger_type);
}
if (base::FeatureList::IsEnabled(features::kPrerender2NewLimitAndScheduler)) {
// Apply the limit of maximum number of running prerenders per
// PrerenderLimitGroup.
switch (limit_group) {
case PrerenderLimitGroup::kSpeculationRulesEager:
return host_count < base::GetFieldTrialParamByFeatureAsInt(
features::kPrerender2NewLimitAndScheduler,
kMaxNumOfRunningSpeculationRulesEagerPrerenders,
10);
case PrerenderLimitGroup::kSpeculationRulesNonEager: {
int limit_non_eager = base::GetFieldTrialParamByFeatureAsInt(
features::kPrerender2NewLimitAndScheduler,
kMaxNumOfRunningSpeculationRulesNonEagerPrerenders, 2);
// When the limit on non-eager speculation rules is reached, cancel the
// oldest host to allow a newly incoming trigger to start.
if (host_count >= limit_non_eager) {
int oldest_prerender_host_id;
// Find the oldest non-eager prerender that has not been canceled yet.
do {
oldest_prerender_host_id =
non_eager_prerender_host_id_by_arrival_order_.front();
non_eager_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::
kMaxNumOfRunningNonEagerPrerendersExceeded));
CHECK_LT(GetHostCountByLimitGroup(limit_group), limit_non_eager);
}
return true;
}
case PrerenderLimitGroup::kEmbedder:
return host_count < base::GetFieldTrialParamByFeatureAsInt(
features::kPrerender2NewLimitAndScheduler,
kMaxNumOfRunningEmbedderPrerenders, 2);
}
}
switch (trigger_type) {
case PreloadingTriggerType::kSpeculationRule:
case PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld:
// The number of prerenders triggered by speculation rules is limited to
// a Finch config param.
return host_count <
base::GetFieldTrialParamByFeatureAsInt(
blink::features::kPrerender2,
blink::features::kPrerender2MaxNumOfRunningSpeculationRules,
10);
case PreloadingTriggerType::kEmbedder:
// Currently the number of prerenders triggered by an embedder is
// limited to two.
return host_count < 2;
}
}
void PrerenderHostRegistry::DestroyWhenUsingExcessiveMemory(
int frame_tree_node_id) {
if (!base::FeatureList::IsEnabled(
blink::features::kPrerender2MemoryControls)) {
return;
}
if (base::FeatureList::IsEnabled(
features::kPrerender2BypassMemoryLimitCheck)) {
return;
}
// Override the memory restriction when the DevTools is open.
if (IsDevToolsOpen(*web_contents())) {
return;
}
memory_instrumentation::MemoryInstrumentation::GetInstance()
->RequestPrivateMemoryFootprint(
base::kNullProcessId,
base::BindOnce(&PrerenderHostRegistry::DidReceiveMemoryDump,
weak_factory_.GetWeakPtr(), frame_tree_node_id));
}
void PrerenderHostRegistry::DidReceiveMemoryDump(
int frame_tree_node_id,
bool success,
std::unique_ptr<memory_instrumentation::GlobalMemoryDump> dump) {
CHECK(
base::FeatureList::IsEnabled(blink::features::kPrerender2MemoryControls));
// Override the memory restriction when the DevTools is open.
if (IsDevToolsOpen(*web_contents())) {
return;
}
if (!success) {
// Give up checking the memory consumption and continue prerendering. This
// case commonly happens due to lifecycle changes of renderer processes
// during the query. Skipping the check should be safe for other safety
// measures: the limit on the number of ongoing prerendering requests and
// memory pressure events should prevent excessive memory usage.
return;
}
int64_t private_footprint_total_kb = 0;
for (const auto& pmd : dump->process_dumps()) {
private_footprint_total_kb += pmd.os_dump().private_footprint_kb;
}
// The default acceptable percent is 60% of the system memory.
int acceptable_percent_of_system_memory =
base::GetFieldTrialParamByFeatureAsInt(
blink::features::kPrerender2MemoryControls,
blink::features::
kPrerender2MemoryAcceptablePercentOfSystemMemoryParamName,
60);
// When the current memory usage is higher than
// `acceptable_percent_of_system_memory` % of the system memory, cancel a
// prerendering with `frame_tree_node_id`.
if (private_footprint_total_kb * 1024 >=
acceptable_percent_of_system_memory * 0.01 *
base::SysInfo::AmountOfPhysicalMemory()) {
CancelHost(frame_tree_node_id, PrerenderFinalStatus::kMemoryLimitExceeded);
}
}
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);
}
} // namespace content