| // Copyright 2022 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/prerenderer_impl.h" |
| |
| #include "content/browser/preloading/preloading.h" |
| #include "content/browser/preloading/prerender/prerender_attributes.h" |
| #include "content/browser/preloading/prerender/prerender_final_status.h" |
| #include "content/browser/preloading/prerender/prerender_host_registry.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/render_frame_host_delegate.h" |
| #include "content/public/browser/web_contents.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| PrerenderTriggerType GetTriggerType( |
| blink::mojom::SpeculationInjectionWorld world) { |
| switch (world) { |
| case blink::mojom::SpeculationInjectionWorld::kNone: |
| [[fallthrough]]; |
| case blink::mojom::SpeculationInjectionWorld::kMain: |
| return PrerenderTriggerType::kSpeculationRule; |
| case blink::mojom::SpeculationInjectionWorld::kIsolated: |
| return PrerenderTriggerType::kSpeculationRuleFromIsolatedWorld; |
| } |
| } |
| |
| } // namespace |
| |
| struct PrerendererImpl::PrerenderInfo { |
| GURL url; |
| Referrer referrer; |
| int prerender_host_id; |
| }; |
| |
| PrerendererImpl::PrerendererImpl(RenderFrameHost& render_frame_host) |
| : WebContentsObserver(WebContents::FromRenderFrameHost(&render_frame_host)), |
| render_frame_host_(render_frame_host) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto& rfhi = static_cast<RenderFrameHostImpl&>(render_frame_host); |
| registry_ = rfhi.delegate()->GetPrerenderHostRegistry()->GetWeakPtr(); |
| } |
| |
| PrerendererImpl::~PrerendererImpl() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CancelStartedPrerenders(); |
| } |
| |
| void PrerendererImpl::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_CURRENTLY_ON(BrowserThread::UI); |
| CancelStartedPrerenders(); |
| } |
| |
| // TODO(isaboori) Part of the logic in |ProcessCandidatesForPrerender| method is |
| // about making preloading decisions and could be moved to PreloadingDecider |
| // class. |
| void PrerendererImpl::ProcessCandidatesForPrerender( |
| const std::vector<blink::mojom::SpeculationCandidatePtr>& candidates) { |
| if (!registry_) |
| return; |
| |
| // 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; |
| |
| // Collects the host ids corresponding to the URLs that are removed from the |
| // speculation rules. These hosts are cancelled later. |
| std::vector<int> removed_prerender_rules; |
| |
| // 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()) { |
| removed_prerender_rules.push_back(prerender.prerender_host_id); |
| } |
| } |
| |
| // 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; |
| } |
| |
| registry_->CancelHosts( |
| removed_prerender_rules, |
| PrerenderCancellationReason(PrerenderFinalStatus::kTriggerDestroyed)); |
| { |
| base::flat_set<int> removed_prerender_rules_set( |
| removed_prerender_rules.begin(), removed_prerender_rules.end()); |
| |
| // Remove the canceled entries so that the page can re-trigger prerendering. |
| // Here are two options: to remove the entries whose prerender_host_id is |
| // invalid, or to remove the entries whose prerender_host_id is in the |
| // removed list. Here we go with the latter, to ensure the prerender |
| // requests rejected by PrerenderHostRegistry can be filtered out. But |
| // ideally PrerenderHostRegistry should implement the history management |
| // mechanism by itself. |
| started_prerenders_.erase( |
| std::remove_if(started_prerenders_.begin(), started_prerenders_.end(), |
| [&](const PrerenderInfo& x) { |
| return base::Contains(removed_prerender_rules_set, |
| x.prerender_host_id); |
| }), |
| started_prerenders_.end()); |
| } |
| |
| // Actually start the candidates once the diffing is done. |
| for (const auto& candidate : candidates_to_start) { |
| MaybePrerender(candidate); |
| } |
| } |
| |
| bool PrerendererImpl::MaybePrerender( |
| const blink::mojom::SpeculationCandidatePtr& candidate) { |
| DCHECK_EQ(candidate->action, blink::mojom::SpeculationAction::kPrerender); |
| |
| if (!registry_) |
| return false; |
| |
| auto& rfhi = static_cast<RenderFrameHostImpl&>(render_frame_host_.get()); |
| WebContents* web_contents = |
| WebContents::FromRenderFrameHost(&render_frame_host_.get()); |
| |
| 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(candidate->url); |
| PreloadingAttempt* preloading_attempt = preloading_data->AddPreloadingAttempt( |
| GetPredictorForSpeculationRules(candidate->injection_world), |
| PreloadingType::kPrerender, std::move(same_url_matcher)); |
| |
| auto [begin, end] = base::ranges::equal_range( |
| started_prerenders_.begin(), started_prerenders_.end(), candidate->url, |
| std::less<>(), &PrerenderInfo::url); |
| // cannot currently start a second prerender with the same URL |
| if (begin != end) { |
| return false; |
| } |
| |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &rfhi, blink::mojom::WebFeature::kSpeculationRulesPrerender); |
| |
| // TODO(crbug.com/1176054): Remove it after supporting cross-site |
| // prerender. |
| if (!prerender_navigation_utils::IsSameSite(candidate->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(candidate->url).Serialize().c_str())); |
| } |
| |
| Referrer referrer(*(candidate->referrer)); |
| PrerenderAttributes attributes( |
| candidate->url, GetTriggerType(candidate->injection_world), |
| /*embedder_histogram_suffix=*/"", referrer, rfhi.GetLastCommittedOrigin(), |
| rfhi.GetProcess()->GetID(), web_contents->GetWeakPtr(), |
| rfhi.GetFrameToken(), rfhi.GetFrameTreeNodeId(), |
| rfhi.GetPageUkmSourceId(), ui::PAGE_TRANSITION_LINK, |
| /*url_match_predicate=*/absl::nullopt, rfhi.GetDevToolsNavigationToken()); |
| |
| // 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. |
| switch (candidate->target_browsing_context_name_hint) { |
| case blink::mojom::SpeculationTargetHint::kBlank: { |
| if (base::FeatureList::IsEnabled(blink::features::kPrerender2InNewTab)) { |
| // `preloading_attempt` is not available for prerendering in a new tab |
| // as it's associated with the current WebContents. |
| // TODO(crbug.com/1350676): Create new PreloadAttempt associated with |
| // WebContents for prerendering. |
| int prerender_host_id = |
| registry_->CreateAndStartHostForNewTab(attributes); |
| started_prerenders_.insert(end, |
| {.url = candidate->url, |
| .referrer = referrer, |
| .prerender_host_id = prerender_host_id}); |
| break; |
| } |
| // Handle the rule as kNoHint if the prerender-in-new-tab is not |
| // enabled. |
| [[fallthrough]]; |
| } |
| case blink::mojom::SpeculationTargetHint::kNoHint: |
| case blink::mojom::SpeculationTargetHint::kSelf: { |
| int prerender_host_id = registry_->CreateAndStartHost( |
| attributes, /*preloading_attempt=*/preloading_attempt); |
| started_prerenders_.insert(end, {.url = candidate->url, |
| .referrer = referrer, |
| .prerender_host_id = prerender_host_id}); |
| break; |
| } |
| } |
| return true; |
| } |
| |
| bool PrerendererImpl::ShouldWaitForPrerenderResult(const GURL& url) { |
| auto [begin, end] = base::ranges::equal_range( |
| started_prerenders_.begin(), started_prerenders_.end(), url, |
| std::less<>(), &PrerenderInfo::url); |
| for (auto it = begin; it != end; ++it) { |
| if (it->prerender_host_id == RenderFrameHost::kNoFrameTreeNodeId) { |
| return false; |
| } |
| } |
| return begin != end; |
| } |
| |
| void PrerendererImpl::CancelStartedPrerenders() { |
| if (registry_) { |
| std::vector<int> started_prerender_ids; |
| for (auto& prerender_info : started_prerenders_) { |
| started_prerender_ids.push_back(prerender_info.prerender_host_id); |
| } |
| registry_->CancelHosts( |
| started_prerender_ids, |
| PrerenderCancellationReason(PrerenderFinalStatus::kTriggerDestroyed)); |
| } |
| |
| started_prerenders_.clear(); |
| } |
| |
| } // namespace content |