blob: 538e30f435b7458d12d73db61c657531b93ba161 [file] [log] [blame]
// Copyright 2021 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/speculation_rules/speculation_host_impl.h"
#include <functional>
#include "base/containers/span.h"
#include "base/ranges/algorithm.h"
#include "base/scoped_observation.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/devtools/network_service_devtools_observer.h"
#include "content/browser/preloading//preloading.h"
#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include "content/browser/preloading/prefetch/prefetch_features.h"
#include "content/browser/preloading/preloading_data_impl.h"
#include "content/browser/preloading/prerender/prerender_attributes.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/browser/preloading/prerender/prerender_navigation_utils.h"
#include "content/browser/renderer_host/render_frame_host_delegate.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/common/content_client.h"
#include "content/public/common/referrer.h"
#include "mojo/public/cpp/bindings/message.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h"
namespace content {
namespace {
bool CandidatesAreValid(
std::vector<blink::mojom::SpeculationCandidatePtr>& candidates) {
for (const auto& candidate : candidates) {
// These non-http candidates should be filtered out in Blink and
// SpeculationHostImpl should not see them. If SpeculationHostImpl receives
// non-http candidates, it may mean the renderer process has a bug
// or is compromised.
if (!candidate->url.SchemeIsHTTPOrHTTPS()) {
mojo::ReportBadMessage("SH_NON_HTTP");
return false;
}
// `target_browsing_context_name_hint` on non-prerender actions should be
// filtered out in Blink.
if (candidate->action != blink::mojom::SpeculationAction::kPrerender &&
candidate->target_browsing_context_name_hint !=
blink::mojom::SpeculationTargetHint::kNoHint) {
mojo::ReportBadMessage("SH_TARGET_HINT_ON_PREFETCH");
return false;
}
}
return true;
}
} // namespace
class SpeculationHostImpl::PrerenderHostObserver
: public PrerenderHost::Observer {
public:
explicit PrerenderHostObserver(PrerenderHost* prerender_host);
~PrerenderHostObserver() override;
// PrerenderHost::Observer implementation:
void OnActivated() override;
void OnHostDestroyed(PrerenderHost::FinalStatus final_status) override;
bool destroyed_by_memory_limit_exceeded() const {
return destroyed_by_memory_limit_exceeded_;
}
private:
bool destroyed_by_memory_limit_exceeded_ = false;
base::ScopedObservation<PrerenderHost, PrerenderHost::Observer> observation_{
this};
};
SpeculationHostImpl::PrerenderHostObserver::PrerenderHostObserver(
PrerenderHost* prerender_host) {
if (prerender_host)
observation_.Observe(prerender_host);
}
SpeculationHostImpl::PrerenderHostObserver::~PrerenderHostObserver() = default;
void SpeculationHostImpl::PrerenderHostObserver::OnActivated() {}
void SpeculationHostImpl::PrerenderHostObserver::OnHostDestroyed(
PrerenderHost::FinalStatus final_status) {
observation_.Reset();
if (final_status == PrerenderHost::FinalStatus::kMemoryLimitExceeded)
destroyed_by_memory_limit_exceeded_ = true;
}
struct SpeculationHostImpl::PrerenderInfo {
GURL url;
Referrer referrer;
int prerender_host_id;
};
// static
void SpeculationHostImpl::Bind(
RenderFrameHost* frame_host,
mojo::PendingReceiver<blink::mojom::SpeculationHost> receiver) {
CHECK(frame_host);
// TODO(crbug.com/1190338): Allow SpeculationHostDelegate to participate in
// this feature check.
if (!base::FeatureList::IsEnabled(
blink::features::kSpeculationRulesPrefetchProxy) &&
!blink::features::IsPrerender2Enabled()) {
mojo::ReportBadMessage(
"Speculation rules must be enabled to bind to "
"blink.mojom.SpeculationHost in the browser.");
return;
}
// DocumentService will destroy this on pipe closure or frame destruction.
new SpeculationHostImpl(*frame_host, std::move(receiver));
}
SpeculationHostImpl::SpeculationHostImpl(
RenderFrameHost& frame_host,
mojo::PendingReceiver<blink::mojom::SpeculationHost> receiver)
: DocumentService(frame_host, std::move(receiver)),
WebContentsObserver(WebContents::FromRenderFrameHost(&frame_host)) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
delegate_ = GetContentClient()->browser()->CreateSpeculationHostDelegate(
render_frame_host());
if (blink::features::IsPrerender2Enabled()) {
auto& rfhi = static_cast<RenderFrameHostImpl&>(frame_host);
registry_ = rfhi.delegate()->GetPrerenderHostRegistry()->GetWeakPtr();
}
}
SpeculationHostImpl::~SpeculationHostImpl() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
CancelStartedPrerenders();
}
void SpeculationHostImpl::PrimaryPageChanged(Page& page) {
// Listen to the change of the primary page. Since only the primary page can
// trigger speculationrules, the change of the primary page indicates that the
// trigger associated with this host is destroyed, so the browser should
// cancel the prerenders that are initiated by it.
// We cannot do it in the destructor only, because DocumentService can be
// deleted asynchronously, but we want to make sure to cancel prerendering
// before the next primary page swaps in so that the next page can trigger a
// new prerender without hitting the max number of running prerenders.
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
CancelStartedPrerenders();
}
void SpeculationHostImpl::UpdateSpeculationCandidates(
std::vector<blink::mojom::SpeculationCandidatePtr> candidates) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (!CandidatesAreValid(candidates))
return;
// Only handle messages from an active main frame.
if (!render_frame_host().IsActive())
return;
if (render_frame_host().GetParent())
return;
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
for (const auto& candidate : candidates) {
// Create new PreloadingPrediction class and pass all fields for all
// candidates.
// In case of speculation rules, the confidence is set as 100 as the URL
// was not predicted and confidence in this case is not defined.
int64_t confidence = 100;
PreloadingURLMatchCallback same_url_matcher =
PreloadingData::GetSameURLMatcher(candidate->url);
auto* preloading_data =
PreloadingData::GetOrCreateForWebContents(web_contents);
// TODO(crbug.com/1341019): Pass the action requested by speculation rules
// to PreloadingPrediction.
preloading_data->AddPreloadingPrediction(
ToPreloadingPredictor(ContentPreloadingPredictor::kSpeculationRules),
confidence, std::move(same_url_matcher));
}
if (base::FeatureList::IsEnabled(features::kPrefetchUseContentRefactor)) {
PrefetchDocumentManager* prefetch_document_manager =
PrefetchDocumentManager::GetOrCreateForCurrentDocument(
&render_frame_host());
prefetch_document_manager->ProcessCandidates(
candidates, weak_ptr_factory_.GetWeakPtr());
}
// Let `delegate_` process the candidates that it is interested in.
if (delegate_)
delegate_->ProcessCandidates(candidates, weak_ptr_factory_.GetWeakPtr());
ProcessCandidatesForPrerender(candidates);
}
void SpeculationHostImpl::ProcessCandidatesForPrerender(
const std::vector<blink::mojom::SpeculationCandidatePtr>& candidates) {
if (!registry_)
return;
DCHECK(blink::features::IsPrerender2Enabled());
// Extract only the candidates which apply to prerender, and sort them by URL
// so we can efficiently compare them to `started_prerenders_`.
std::vector<blink::mojom::SpeculationCandidatePtr> prerender_candidates;
for (const auto& candidate : candidates) {
if (candidate->action == blink::mojom::SpeculationAction::kPrerender)
prerender_candidates.push_back(candidate.Clone());
}
base::ranges::sort(prerender_candidates, std::less<>(),
&blink::mojom::SpeculationCandidate::url);
std::vector<blink::mojom::SpeculationCandidatePtr> candidates_to_start;
// Compare the sorted candidate and started prerender lists to one another.
// Since they are sorted, we process the lexicographically earlier of the two
// URLs pointed at by the iterators, and compare the range of entries in each
// that match that URL.
//
// URLs which are present in the prerender list but not the candidate list can
// no longer proceed and are cancelled.
//
// URLs which are present in the candidate list but not the prerender list
// could be started and are gathered in `candidates_to_start`.
auto candidate_it = prerender_candidates.begin();
auto started_it = started_prerenders_.begin();
while (candidate_it != prerender_candidates.end() ||
started_it != started_prerenders_.end()) {
// Select the lesser of the two URLs to diff.
GURL url;
if (started_it == started_prerenders_.end())
url = (*candidate_it)->url;
else if (candidate_it == prerender_candidates.end())
url = started_it->url;
else
url = std::min((*candidate_it)->url, started_it->url);
// Select the ranges from both that match the URL in question.
auto equal_prerender_end = base::ranges::find_if(
started_it, started_prerenders_.end(),
[&](const auto& started) { return started.url != url; });
base::span<PrerenderInfo> matching_prerenders(started_it,
equal_prerender_end);
auto equal_candidate_end = base::ranges::find_if(
candidate_it, prerender_candidates.end(),
[&](const auto& candidate) { return candidate->url != url; });
base::span<blink::mojom::SpeculationCandidatePtr> matching_candidates(
candidate_it, equal_candidate_end);
// Decide what started prerenders to cancel.
for (PrerenderInfo& prerender : matching_prerenders) {
if (prerender.prerender_host_id == RenderFrameHost::kNoFrameTreeNodeId)
continue;
// TODO(jbroman): This doesn't currently care about other aspects, like
// the referrer. This doesn't presently matter, but in the future we might
// want to cancel if there are candidates which match by URL but none of
// which permit this prerender.
if (matching_candidates.empty()) {
registry_->OnTriggerDestroyed(prerender.prerender_host_id);
prerender.prerender_host_id = RenderFrameHost::kNoFrameTreeNodeId;
}
}
// Decide what new candidates to start.
// For now, start the first candidate for a URL only if there are no
// matching prerenders. We could be cleverer in the future.
if (matching_prerenders.empty()) {
DCHECK_GT(matching_candidates.size(), 0u);
candidates_to_start.push_back(std::move(matching_candidates[0]));
}
// Advance the iterators past all matching entries.
candidate_it = equal_candidate_end;
started_it = equal_prerender_end;
}
// Actually start the candidates once the diffing is done.
auto& rfhi = static_cast<RenderFrameHostImpl&>(render_frame_host());
WebContents* web_contents =
WebContents::FromRenderFrameHost(&render_frame_host());
for (const auto& it : candidates_to_start) {
DCHECK_EQ(it->action, blink::mojom::SpeculationAction::kPrerender);
auto* preloading_data =
PreloadingData::GetOrCreateForWebContents(web_contents);
// Create new PreloadingAttempt and pass all the values corresponding to
// this prerendering attempt.
PreloadingURLMatchCallback same_url_matcher =
PreloadingData::GetSameURLMatcher(it->url);
PreloadingAttempt* preloading_attempt =
preloading_data->AddPreloadingAttempt(
ToPreloadingPredictor(
ContentPreloadingPredictor::kSpeculationRules),
PreloadingType::kPrerender, std::move(same_url_matcher));
auto [begin, end] = base::ranges::equal_range(
started_prerenders_.begin(), started_prerenders_.end(), it->url,
std::less<>(), &PrerenderInfo::url);
DCHECK(begin == end)
<< "cannot currently start a second prerender with the same URL";
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
&rfhi, blink::mojom::WebFeature::kSpeculationRulesPrerender);
// TODO(crbug.com/1176054): Remove it after supporting cross-site
// prerender.
if (blink::features::
IsSameSiteCrossOriginForSpeculationRulesPrerender2Enabled()) {
if (!prerender_navigation_utils::IsSameSite(
it->url, rfhi.GetLastCommittedOrigin())) {
rfhi.AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kWarning,
base::StringPrintf(
"The SpeculationRules API does not support cross-site "
"prerender yet "
"(kSameSiteCrossOriginForSpeculationRulesPrerender2 is "
"enabled). (initiator origin: %s, prerender origin: %s). "
"https://crbug.com/1176054 tracks cross-site support.",
rfhi.GetLastCommittedOrigin().Serialize().c_str(),
url::Origin::Create(it->url).Serialize().c_str()));
}
} else {
if (!rfhi.GetLastCommittedOrigin().IsSameOriginWith(it->url)) {
rfhi.AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kWarning,
base::StringPrintf(
"The SpeculationRules API does not support cross-origin "
"prerender yet. (initiator origin: %s, prerender origin: %s). "
"https://crbug.com/1176054 tracks cross-origin support.",
rfhi.GetLastCommittedOrigin().Serialize().c_str(),
url::Origin::Create(it->url).Serialize().c_str()));
}
}
// TODO(crbug.com/1354049): Pass `target_browsing_context_name_hint` to
// start prerendering in a new tab.
Referrer referrer(*(it->referrer));
int prerender_host_id = registry_->CreateAndStartHost(
PrerenderAttributes(it->url, PrerenderTriggerType::kSpeculationRule,
/*embedder_histogram_suffix=*/"", referrer,
rfhi.GetLastCommittedOrigin(),
rfhi.GetLastCommittedURL(),
rfhi.GetProcess()->GetID(), rfhi.GetFrameToken(),
rfhi.GetFrameTreeNodeId(),
rfhi.GetPageUkmSourceId(), ui::PAGE_TRANSITION_LINK,
/*url_match_predicate=*/absl::nullopt),
*web_contents, /*preloading_attempt=*/preloading_attempt);
// TODO(crbug.com/1354049): Handle the case where multiple speculation rules
// have the same URL but its `target_browsing_context_name_hint` is
// different. In the current implementation, only the first rule is
// triggered.
started_prerenders_.insert(end, {.url = it->url,
.referrer = referrer,
.prerender_host_id = prerender_host_id});
// Start to observe PrerenderHost to get the information about FinalStatus.
observers_.push_back(std::make_unique<PrerenderHostObserver>(
registry_->FindNonReservedHostById(prerender_host_id)));
}
}
void SpeculationHostImpl::CancelStartedPrerenders() {
// This function can be called twice and the histogram should be recorded in
// the first call. Also, skip recording the histogram when no prerendering
// starts.
if (started_prerenders_.empty()) {
DCHECK(observers_.empty());
return;
}
// Record the percentage of destroyed prerenders due to the excessive memory
// usage. `started_prerenders_` can include destroyed prerenders by other
// reasons.
// The closer the value is to 0, the less prerenders are cancelled by
// FinalStatus::kMemoryLimitExceeded. The result depends on Finch params
// `max_num_of_running_speculation_rules` and
// `acceptable_percent_of_system_memory`.
base::UmaHistogramPercentage(
"Prerender.Experimental.CancellationPercentageByExcessiveMemoryUsage."
"SpeculationRule",
GetNumberOfDestroyedByMemoryExceeded() * 100 /
started_prerenders_.size());
if (registry_) {
for (const auto& prerender : started_prerenders_) {
int host_id = prerender.prerender_host_id;
if (host_id != RenderFrameHost::kNoFrameTreeNodeId)
registry_->OnTriggerDestroyed(host_id);
}
started_prerenders_.clear();
observers_.clear();
}
}
void SpeculationHostImpl::OnStartSinglePrefetch(
const std::string& request_id,
const network::ResourceRequest& request) {
auto* ftn = static_cast<RenderFrameHostImpl*>(&render_frame_host())
->frame_tree_node();
devtools_instrumentation::OnPrefetchRequestWillBeSent(
ftn, request_id, render_frame_host().GetLastCommittedURL(), request);
}
void SpeculationHostImpl::OnPrefetchResponseReceived(
const GURL& url,
const std::string& request_id,
const network::mojom::URLResponseHead& response) {
auto* ftn = static_cast<RenderFrameHostImpl*>(&render_frame_host())
->frame_tree_node();
devtools_instrumentation::OnPrefetchResponseReceived(ftn, request_id, url,
response);
}
void SpeculationHostImpl::OnPrefetchRequestComplete(
const std::string& request_id,
const network::URLLoaderCompletionStatus& status) {
auto* ftn = static_cast<RenderFrameHostImpl*>(&render_frame_host())
->frame_tree_node();
devtools_instrumentation::OnPrefetchRequestComplete(ftn, request_id, status);
}
void SpeculationHostImpl::OnPrefetchBodyDataReceived(
const std::string& request_id,
const std::string& body,
bool is_base64_encoded) {
auto* ftn = static_cast<RenderFrameHostImpl*>(&render_frame_host())
->frame_tree_node();
devtools_instrumentation::OnPrefetchBodyDataReceived(ftn, request_id, body,
is_base64_encoded);
}
mojo::PendingRemote<network::mojom::DevToolsObserver>
SpeculationHostImpl::MakeSelfOwnedNetworkServiceDevToolsObserver() {
auto* ftn = static_cast<RenderFrameHostImpl*>(&render_frame_host())
->frame_tree_node();
return NetworkServiceDevToolsObserver::MakeSelfOwned(ftn);
}
int SpeculationHostImpl::GetNumberOfDestroyedByMemoryExceeded() {
int destroyed_prerenders_by_memory_limit_exceeded = 0;
for (auto& observer : observers_) {
if (observer->destroyed_by_memory_limit_exceeded())
destroyed_prerenders_by_memory_limit_exceeded++;
}
return destroyed_prerenders_by_memory_limit_exceeded;
}
} // namespace content