blob: 0c2b75bdf384ae22394e1e5fe3f99d989576d500 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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/callback_helpers.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/observer_list.h"
#include "base/system/sys_info.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/trace_event/common/trace_event_common.h"
#include "base/trace_event/trace_conversion_helper.h"
#include "build/build_config.h"
#include "content/browser/preloading/preloading_attempt_impl.h"
#include "content/browser/preloading/prerender/prerender_metrics.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/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "third_party/blink/public/common/features.h"
namespace content {
namespace {
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;
// Use the same default threshold as the back/forward cache. See comments in
// DeviceHasEnoughMemoryForBackForwardCache().
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;
}
} // namespace
PrerenderHostRegistry::PrerenderHostRegistry() {
DCHECK(blink::features::IsPrerender2Enabled());
}
PrerenderHostRegistry::~PrerenderHostRegistry() {
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,
WebContents& web_contents,
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);
int frame_tree_node_id = RenderFrameHost::kNoFrameTreeNodeId;
{
// Ensure observers are notified that a trigger occurred.
base::ScopedClosureRunner notify_trigger(
base::BindOnce(&PrerenderHostRegistry::NotifyTrigger,
base::Unretained(this), attributes.prerendering_url));
// Check whether preloading is enabled. If users disable this
// setting, it means users do not want to preload pages.
WebContentsDelegate* web_contents_delegate = web_contents.GetDelegate();
if (!web_contents_delegate ||
!web_contents_delegate->IsPrerender2Supported(web_contents)) {
if (attempt)
attempt->SetEligibility(PreloadingEligibility::kPreloadingDisabled);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// Don't prerender when the trigger is in the background.
if (web_contents.GetVisibility() == Visibility::HIDDEN) {
RecordPrerenderHostFinalStatus(
PrerenderHost::FinalStatus::kTriggerBackgrounded, attributes,
ukm::kInvalidSourceId);
if (attempt)
attempt->SetEligibility(PreloadingEligibility::kHidden);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// Don't prerender on low-end devices.
// TODO(https://crbug.com/1176120): Fallback to NoStatePrefetch
// since the memory requirements are different.
if (!DeviceHasEnoughMemoryForPrerender()) {
RecordPrerenderHostFinalStatus(PrerenderHost::FinalStatus::kLowEndDevice,
attributes, ukm::kInvalidSourceId);
if (attempt)
attempt->SetEligibility(PreloadingEligibility::kLowMemory);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// TODO(crbug.com/1176054): Support cross-origin prerendering.
// The initiator origin is nullopt when prerendering is initiated by the
// browser (not by a renderer using Speculation Rules API). In that case,
// skip the same-origin check.
if (!attributes.IsBrowserInitiated() &&
!attributes.initiator_origin.value().IsSameOriginWith(
attributes.prerendering_url)) {
RecordPrerenderHostFinalStatus(
PrerenderHost::FinalStatus::kCrossOriginNavigation, attributes,
ukm::kInvalidSourceId);
if (attempt)
attempt->SetEligibility(PreloadingEligibility::kCrossOrigin);
return RenderFrameHost::kNoFrameTreeNodeId;
}
// Once all eligibility checks are completed, set the status to kEligible.
if (attempt)
attempt->SetEligibility(PreloadingEligibility::kEligible);
// Check for the HoldbackStatus after checking the eligibility.
if (base::GetFieldTrialParamByFeatureAsBool(blink::features::kPrerender2,
"prerender_holdback", false)) {
if (attempt)
attempt->SetHoldbackStatus(PreloadingHoldbackStatus::kHoldback);
return RenderFrameHost::kNoFrameTreeNodeId;
}
if (attempt)
attempt->SetHoldbackStatus(PreloadingHoldbackStatus::kAllowed);
// Ignore prerendering requests for the same URL.
for (auto& iter : prerender_host_by_frame_tree_node_id_) {
if (iter.second->GetInitialUrl() == attributes.prerendering_url) {
if (attempt) {
attempt->SetTriggeringOutcome(
PreloadingTriggeringOutcome::kDuplicate);
}
return RenderFrameHost::kNoFrameTreeNodeId;
}
}
// TODO(crbug.com/1197133): Cancel the started prerender and start a new
// one if the score of the new candidate is higher than the started one's.
if (!IsAllowedToStartPrerenderingForTrigger(attributes.trigger_type)) {
if (attempt) {
// 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.
attempt->SetTriggeringOutcome(PreloadingTriggeringOutcome::kFailure);
}
RecordPrerenderHostFinalStatus(
PrerenderHost::FinalStatus::kMaxNumOfRunningPrerendersExceeded,
attributes, ukm::kInvalidSourceId);
return RenderFrameHost::kNoFrameTreeNodeId;
}
auto* attempt_impl = static_cast<PreloadingAttemptImpl*>(attempt);
auto prerender_host =
std::make_unique<PrerenderHost>(attributes, web_contents, attempt_impl);
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);
}
// Outside the NotifyTrigger ScopedClosureRunner since tests use the trigger
// to register observers. Observers may be notified about start in the call
// below.
if (!prerender_host_by_frame_tree_node_id_[frame_tree_node_id]
->StartPrerendering()) {
// TODO(nhiroki): Pass a more suitable cancellation reason like
// kStartFailed.
CancelHost(frame_tree_node_id, PrerenderHost::FinalStatus::kDestroyed);
return RenderFrameHost::kNoFrameTreeNodeId;
}
RecordPrerenderTriggered(attributes.initiator_ukm_id);
return frame_tree_node_id;
}
bool PrerenderHostRegistry::CancelHost(
int frame_tree_node_id,
PrerenderHost::FinalStatus final_status) {
TRACE_EVENT1("navigation", "PrerenderHostRegistry::CancelHost",
"frame_tree_node_id", frame_tree_node_id);
// Cancel must not be requested during activation.
// TODO(https://crbug.com/1195751): This is the key assumption of the
// synchronous prerender activation, so now this is CHECK. Change this to
// DCHECK once the assumption is ensured in the real world.
CHECK(!base::Contains(reserved_prerender_host_by_frame_tree_node_id_,
frame_tree_node_id));
// Look up the id in the non-reserved host map, remove it from the map, and
// record the cancellation reason.
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;
// 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);
// Asynchronously delete the prerender host.
ScheduleToDeleteAbandonedHost(std::move(prerender_host), final_status);
return true;
}
void PrerenderHostRegistry::CancelAllHosts(
PrerenderHost::FinalStatus final_status) {
// Should not have an activating host. See comments in CancelHost.
CHECK(reserved_prerender_host_by_frame_tree_node_id_.empty());
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), final_status);
}
}
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);
// 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;
// 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);
DCHECK_EQ(host_id, host->frame_tree_node_id());
// Reserve the host for activation.
auto result = reserved_prerender_host_by_frame_tree_node_id_.emplace(
host_id, std::move(host));
DCHECK(result.second);
return host_id;
}
RenderFrameHostImpl* PrerenderHostRegistry::GetRenderFrameHostForReservedHost(
int frame_tree_node_id) {
auto iter =
reserved_prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id);
if (iter == reserved_prerender_host_by_frame_tree_node_id_.end()) {
return nullptr;
}
return iter->second->GetPrerenderedMainFrameHost();
}
std::unique_ptr<StoredPage> PrerenderHostRegistry::ActivateReservedHost(
int frame_tree_node_id,
NavigationRequest& navigation_request) {
auto iter =
reserved_prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id);
CHECK(iter != reserved_prerender_host_by_frame_tree_node_id_.end());
std::unique_ptr<PrerenderHost> prerender_host = std::move(iter->second);
reserved_prerender_host_by_frame_tree_node_id_.erase(iter);
return prerender_host->Activate(navigation_request);
}
void PrerenderHostRegistry::OnTriggerDestroyed(int frame_tree_node_id) {
// TODO(https://crbug.com/1169594): Since one prerender may have several
// triggers, PrerenderHostRegistry should not destroy a PrerenderHost instance
// if one of the triggers is still alive.
// Look up the id in the non-reserved host map and remove it from the map if
// it's found.
auto found = prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id);
if (found != prerender_host_by_frame_tree_node_id_.end()) {
DCHECK(!base::Contains(reserved_prerender_host_by_frame_tree_node_id_,
frame_tree_node_id));
// Remove the prerender host from the host maps so that it's not used for
// activation during asynchronous deletion.
std::unique_ptr<PrerenderHost> prerender_host = std::move(found->second);
prerender_host_by_frame_tree_node_id_.erase(found);
// Asynchronously delete the prerender host.
ScheduleToDeleteAbandonedHost(
std::move(prerender_host),
PrerenderHost::FinalStatus::kTriggerDestroyed);
}
// Don't remove the host from the reserved host map. Unlike use of the
// disallowed features in prerendered pages, the destruction of the trigger
// doesn't spoil prerendering, so let it keep ongoing.
}
void PrerenderHostRegistry::OnActivationFinished(int frame_tree_node_id) {
// OnActivationFinished() should not be called for non-reserved hosts.
DCHECK(!base::Contains(prerender_host_by_frame_tree_node_id_,
frame_tree_node_id));
reserved_prerender_host_by_frame_tree_node_id_.erase(frame_tree_node_id);
}
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();
}
PrerenderHost* PrerenderHostRegistry::FindReservedHostById(
int frame_tree_node_id) {
auto iter =
reserved_prerender_host_by_frame_tree_node_id_.find(frame_tree_node_id);
if (iter == reserved_prerender_host_by_frame_tree_node_id_.end())
return nullptr;
return iter->second.get();
}
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());
}
for (auto& i : reserved_prerender_host_by_frame_tree_node_id_) {
result.push_back(&i.second->GetPrerenderFrameTree());
}
return result;
}
PrerenderHost* PrerenderHostRegistry::FindHostByUrlForTesting(
const GURL& prerendering_url) {
for (auto& iter : prerender_host_by_frame_tree_node_id_) {
if (iter.second->GetInitialUrl() == prerendering_url)
return iter.second.get();
}
return nullptr;
}
void PrerenderHostRegistry::CancelAllHostsForTesting() {
DCHECK(reserved_prerender_host_by_frame_tree_node_id_.empty())
<< "It is not possible to cancel reserved hosts, 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),
PrerenderHost::FinalStatus::kCancelAllHostsForTesting);
}
// After we're done scheduling deletion, clear the map.
prerender_host_by_frame_tree_node_id_.clear();
}
base::WeakPtr<PrerenderHostRegistry> PrerenderHostRegistry::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void PrerenderHostRegistry::ForEachPrerenderHost(
base::RepeatingCallback<void(PrerenderHost&)> callback) {
for (auto& iter : prerender_host_by_frame_tree_node_id_) {
callback.Run(*iter.second);
}
for (auto& iter : reserved_prerender_host_by_frame_tree_node_id_) {
callback.Run(*iter.second);
}
}
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;
// 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)
return RenderFrameHost::kNoFrameTreeNodeId;
// Find an available host for the navigation URL.
PrerenderHost* host = nullptr;
for (const auto& [_, 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;
// 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)) {
return RenderFrameHost::kNoFrameTreeNodeId;
}
if (!host->IsFramePolicyCompatibleWithPrimaryFrameTree()) {
return RenderFrameHost::kNoFrameTreeNodeId;
}
return host->frame_tree_node_id();
}
void PrerenderHostRegistry::ScheduleToDeleteAbandonedHost(
std::unique_ptr<PrerenderHost> prerender_host,
PrerenderHost::FinalStatus final_status) {
prerender_host->RecordFinalStatus(PassKey(), final_status);
// Asynchronously delete the prerender host.
to_be_deleted_hosts_.push_back(std::move(prerender_host));
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&PrerenderHostRegistry::DeleteAbandonedHosts,
weak_factory_.GetWeakPtr()));
}
void PrerenderHostRegistry::DeleteAbandonedHosts() {
to_be_deleted_hosts_.clear();
}
void PrerenderHostRegistry::NotifyTrigger(const GURL& url) {
for (Observer& obs : observers_)
obs.OnTrigger(url);
}
PrerenderTriggerType PrerenderHostRegistry::GetPrerenderTriggerType(
int frame_tree_node_id) {
PrerenderHost* prerender_host = FindReservedHostById(frame_tree_node_id);
DCHECK(prerender_host);
return prerender_host->trigger_type();
}
const std::string& PrerenderHostRegistry::GetPrerenderEmbedderHistogramSuffix(
int frame_tree_node_id) {
PrerenderHost* prerender_host = FindReservedHostById(frame_tree_node_id);
DCHECK(prerender_host);
return prerender_host->embedder_histogram_suffix();
}
bool PrerenderHostRegistry::IsAllowedToStartPrerenderingForTrigger(
PrerenderTriggerType trigger_type) {
// Currently the number prerenders is limited to two per WebContentsImpl.
const size_t kMaxNumOfRunningPrerenders = 2;
if (prerender_host_by_frame_tree_node_id_.size() ==
kMaxNumOfRunningPrerenders)
return false;
int trigger_type_count = 0;
DCHECK_LT(prerender_host_by_frame_tree_node_id_.size(),
kMaxNumOfRunningPrerenders);
for (const auto& host_by_id : prerender_host_by_frame_tree_node_id_) {
if (host_by_id.second->trigger_type() == trigger_type)
++trigger_type_count;
}
switch (trigger_type) {
case PrerenderTriggerType::kSpeculationRule:
// Speculation Rules trigger is allowed to start only one prerender.
return trigger_type_count < 1;
case PrerenderTriggerType::kEmbedder:
// Embedder triggers are allowed to start at most 2 concurrent
// prerenders.
return trigger_type_count < 2;
}
}
} // namespace content