| // 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/prefetch/prefetch_document_manager.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <tuple> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "content/browser/browser_context_impl.h" |
| #include "content/browser/preloading/prefetch/no_vary_search_helper.h" |
| #include "content/browser/preloading/prefetch/prefetch_container.h" |
| #include "content/browser/preloading/prefetch/prefetch_handle_impl.h" |
| #include "content/browser/preloading/prefetch/prefetch_params.h" |
| #include "content/browser/preloading/prefetch/prefetch_service.h" |
| #include "content/browser/preloading/preload_pipeline_info_impl.h" |
| #include "content/browser/preloading/preloading.h" |
| #include "content/browser/preloading/preloading_attempt_impl.h" |
| #include "content/browser/preloading/preloading_data_impl.h" |
| #include "content/browser/preloading/preloading_trigger_type_impl.h" |
| #include "content/browser/preloading/speculation_rules/speculation_rules_tags.h" |
| #include "content/browser/preloading/speculation_rules/speculation_rules_util.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/prefetch_metrics.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_client.h" |
| #include "net/http/http_no_vary_search_data.h" |
| #include "services/network/public/mojom/no_vary_search.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| static PrefetchService* g_prefetch_service_for_testing = nullptr; |
| |
| struct PrefetchUrlParams { |
| explicit PrefetchUrlParams( |
| const blink::mojom::SpeculationCandidatePtr& candidate) |
| : prefetch_url(candidate->url), |
| prefetch_type(PreloadingTriggerTypeFromSpeculationInjectionType( |
| candidate->injection_type), |
| /*use_prefetch_proxy=*/ |
| candidate->requires_anonymous_client_ip_when_cross_origin, |
| candidate->eagerness), |
| referrer(*candidate->referrer), |
| no_vary_search_hint(candidate->no_vary_search_hint.Clone()), |
| tags(candidate->tags.empty() ? std::nullopt |
| : std::make_optional(candidate->tags)) { |
| if (prefetch_type.IsProxyRequiredWhenCrossOrigin() && |
| ShouldPrefetchBypassProxyForTestHost(prefetch_url.host())) { |
| // TODO(crbug.com/40942006): Remove SetProxyBypassedForTest, since it is |
| // the only mutator of the PrefetchType. |
| prefetch_type.SetProxyBypassedForTest(); // IN-TEST |
| } |
| } |
| |
| GURL prefetch_url; |
| PrefetchType prefetch_type; |
| blink::mojom::Referrer referrer; |
| network::mojom::NoVarySearchPtr no_vary_search_hint; |
| std::optional<SpeculationRulesTags> tags; |
| }; |
| |
| } // namespace |
| |
| PrefetchDocumentManager::PrefetchDocumentManager(RenderFrameHost* rfh) |
| : DocumentUserData(rfh), |
| document_token_( |
| static_cast<RenderFrameHostImpl*>(rfh)->GetDocumentToken()), |
| prefetch_destruction_callback_(base::DoNothing()) {} |
| |
| PrefetchDocumentManager::~PrefetchDocumentManager() { |
| PrefetchService* prefetch_service = GetPrefetchService(); |
| if (!prefetch_service) |
| return; |
| |
| // Invalidate weak pointers to `this` a little earlier to avoid callbacks to |
| // `this` (especially `PrefetchWillBeDestroyed()`) during |
| // `MayReleasePrefetch()` below. |
| weak_method_factory_.InvalidateWeakPtrs(); |
| } |
| |
| // static |
| PrefetchDocumentManager* PrefetchDocumentManager::FromDocumentToken( |
| int process_id, |
| const blink::DocumentToken& document_token) { |
| if (auto* rfh = |
| RenderFrameHostImpl::FromDocumentToken(process_id, document_token)) { |
| if (auto* prefetch_document_manager = GetForCurrentDocument(rfh)) { |
| // A RenderFrameHost can have multiple Documents/PrefetchDocumentManagers |
| // and the Document of `document_token` might be pending deletion or |
| // bfcached, so check `document_token_` to confirm we get the correct |
| // `PrefetchDocumentManager`. |
| // TODO(crbug.com/40615943): clean this up once RenderDocument ships. |
| if (prefetch_document_manager->document_token_ == document_token) { |
| return prefetch_document_manager; |
| } |
| } |
| } |
| return nullptr; |
| } |
| |
| void PrefetchDocumentManager::ProcessCandidates( |
| std::vector<blink::mojom::SpeculationCandidatePtr>& candidates) { |
| // Filter out candidates that can be handled by |PrefetchService| and |
| // determine the type of prefetch required. |
| // TODO(crbug.com/40215782): Once this code becomes enabled by default |
| // to handle all prefetches and the prefetch proxy code in chrome/browser/ is |
| // removed, then we can move the logic of which speculation candidates this |
| // code can handle up a layer to |SpeculationHostImpl|. |
| std::vector<PrefetchUrlParams> prefetches; |
| |
| // Evicts an existing prefetch if there is no longer a matching speculation |
| // candidate for it. Note: A matching candidate is not necessarily the |
| // candidate that originally triggered the prefetch, but is any prefetch |
| // candidate that has the same URL. |
| std::vector<GURL> urls_from_candidates; |
| urls_from_candidates.reserve(candidates.size()); |
| for (const auto& candidate_ptr : candidates) { |
| if (candidate_ptr->action == blink::mojom::SpeculationAction::kPrefetch) { |
| urls_from_candidates.push_back(candidate_ptr->url); |
| } |
| } |
| base::flat_set<GURL> url_set(std::move(urls_from_candidates)); |
| std::vector<std::pair<GURL, PreloadingType>> prefetches_to_evict; |
| for (const auto& [all_prefetches_key, prefetch] : all_prefetches_) { |
| const auto& [url, planned_max_preloading_type] = all_prefetches_key; |
| |
| // Don't evict prefetch ahead of prerender, which is initiated by |
| // `PrerenderImpl`, as `PrefetchDocumentManager::ProcessCandidates()` is |
| // only called for prefeches managed by `Prefetcher`. |
| if (planned_max_preloading_type != PreloadingType::kPrefetch) { |
| continue; |
| } |
| |
| if (!base::Contains(url_set, url)) { |
| static_cast<PrefetchHandleImpl*>(prefetch.get()) |
| ->SetPrefetchStatusOnReleaseStartedPrefetch( |
| PrefetchStatus::kPrefetchEvictedAfterCandidateRemoved); |
| prefetches_to_evict.push_back(all_prefetches_key); |
| } |
| } |
| for (const auto& all_prefetches_key : prefetches_to_evict) { |
| all_prefetches_.erase(all_prefetches_key); |
| } |
| |
| auto should_process_entry = |
| [&](const blink::mojom::SpeculationCandidatePtr& candidate) { |
| // This code doesn't not support speculation candidates with the action |
| // of |blink::mojom::SpeculationAction::kPrefetchWithSubresources|. See |
| // https://crbug.com/1296309. |
| if (candidate->action != blink::mojom::SpeculationAction::kPrefetch) { |
| return false; |
| } |
| |
| prefetches.emplace_back(candidate); |
| return true; |
| }; |
| |
| std::erase_if(candidates, should_process_entry); |
| |
| for (auto& [prefetch_url, prefetch_type, referrer, no_vary_search_hint, |
| tags] : prefetches) { |
| // Immediate candidates are enacted by the same predictor that creates them. |
| const PreloadingPredictor enacting_predictor = |
| GetPredictorForPreloadingTriggerType(prefetch_type.trigger_type()); |
| PrefetchUrl(prefetch_url, prefetch_type, enacting_predictor, referrer, |
| std::move(tags), no_vary_search_hint, |
| PreloadPipelineInfo::Create( |
| /*planned_max_preloading_type=*/PreloadingType::kPrefetch)); |
| } |
| |
| if (PrefetchService* prefetch_service = GetPrefetchService()) { |
| prefetch_service->OnCandidatesUpdated(); |
| } |
| } |
| |
| bool PrefetchDocumentManager::MaybePrefetch( |
| blink::mojom::SpeculationCandidatePtr candidate, |
| const PreloadingPredictor& enacting_predictor) { |
| if (candidate->action != blink::mojom::SpeculationAction::kPrefetch) { |
| return false; |
| } |
| |
| PrefetchUrlParams params(candidate); |
| PrefetchUrl(params.prefetch_url, params.prefetch_type, enacting_predictor, |
| params.referrer, std::move(params.tags), |
| params.no_vary_search_hint, |
| PreloadPipelineInfo::Create( |
| /*planned_max_preloading_type=*/PreloadingType::kPrefetch)); |
| return true; |
| } |
| |
| void PrefetchDocumentManager::PrefetchAheadOfPrerender( |
| scoped_refptr<PreloadPipelineInfo> preload_pipeline_info, |
| blink::mojom::SpeculationCandidatePtr candidate, |
| const PreloadingPredictor& enacting_predictor) { |
| PrefetchUrlParams params(candidate); |
| PrefetchUrl(params.prefetch_url, params.prefetch_type, enacting_predictor, |
| params.referrer, std::move(params.tags), |
| params.no_vary_search_hint, std::move(preload_pipeline_info)); |
| } |
| |
| void PrefetchDocumentManager::PrefetchUrl( |
| const GURL& url, |
| const PrefetchType& prefetch_type, |
| const PreloadingPredictor& enacting_predictor, |
| const blink::mojom::Referrer& referrer, |
| std::optional<SpeculationRulesTags> speculation_rules_tags, |
| const network::mojom::NoVarySearchPtr& mojo_no_vary_search_hint, |
| scoped_refptr<PreloadPipelineInfo> preload_pipeline_info) { |
| const std::pair<GURL, PreloadingType> all_prefetches_key = |
| std::make_pair(url, PreloadPipelineInfoImpl::From(*preload_pipeline_info) |
| .planned_max_preloading_type()); |
| |
| // Skip prefetches that have already been requested. |
| auto prefetch_container_iter = all_prefetches_.find(all_prefetches_key); |
| if (prefetch_container_iter != all_prefetches_.end() && |
| static_cast<PrefetchHandleImpl*>(prefetch_container_iter->second.get()) |
| ->IsAlive()) { |
| return; |
| } |
| |
| // Log that a prefetch is occurring. Paths that reach this point go through |
| // speculation rules in some form or another. |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), |
| blink::mojom::WebFeature::kSpeculationRulesPrefetch); |
| |
| std::optional<net::HttpNoVarySearchData> no_vary_search_hint; |
| if (mojo_no_vary_search_hint) { |
| no_vary_search_hint = no_vary_search::ParseHttpNoVarySearchDataFromMojom( |
| mojo_no_vary_search_hint); |
| } |
| PrefetchService* prefetch_service = GetPrefetchService(); |
| if (!prefetch_service) { |
| return; |
| } |
| |
| auto* web_contents = WebContents::FromRenderFrameHost(&render_frame_host()); |
| auto* preloading_data = |
| PreloadingDataImpl::GetOrCreateForWebContents(web_contents); |
| |
| const PreloadingPredictor creating_predictor = |
| GetPredictorForPreloadingTriggerType(prefetch_type.trigger_type()); |
| PreloadingURLMatchCallback matcher = |
| PreloadingDataImpl::GetPrefetchServiceMatcher( |
| *prefetch_service, PrefetchContainer::Key(document_token_, url)); |
| |
| auto* attempt = |
| static_cast<PreloadingAttemptImpl*>(preloading_data->AddPreloadingAttempt( |
| creating_predictor, enacting_predictor, PreloadingType::kPrefetch, |
| std::move(matcher), |
| web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId())); |
| |
| attempt->SetSpeculationEagerness(prefetch_type.GetEagerness()); |
| CHECK(!IsImmediateSpeculationEagerness(prefetch_type.GetEagerness()) || |
| creating_predictor == enacting_predictor); |
| |
| // `PreloadingPrediction` is added in `PreloadingDecider`. |
| |
| auto container = std::make_unique<PrefetchContainer>( |
| static_cast<RenderFrameHostImpl&>(render_frame_host()), document_token_, |
| url, prefetch_type, referrer, std::move(speculation_rules_tags), |
| std::move(no_vary_search_hint), /*priority=*/std::nullopt, |
| weak_method_factory_.GetWeakPtr(), std::move(preload_pipeline_info), |
| attempt->GetWeakPtr()); |
| DVLOG(1) << *container << ": created"; |
| |
| referring_page_metrics_.prefetch_attempted_count++; |
| |
| all_prefetches_[all_prefetches_key] = |
| prefetch_service->AddPrefetchContainerWithHandle(std::move(container)); |
| } |
| |
| bool PrefetchDocumentManager::IsPrefetchAttemptFailedOrDiscarded( |
| const GURL& url) { |
| PrefetchService* prefetch_service = GetPrefetchService(); |
| if (!prefetch_service) { |
| return true; |
| } |
| |
| return prefetch_service->IsPrefetchAttemptFailedOrDiscardedInternal( |
| base::PassKey<PrefetchDocumentManager>(), |
| PrefetchContainer::Key(document_token_, url)); |
| } |
| |
| // static |
| void PrefetchDocumentManager::SetPrefetchServiceForTesting( |
| PrefetchService* prefetch_service) { |
| g_prefetch_service_for_testing = prefetch_service; |
| } |
| |
| void PrefetchDocumentManager::ResetPrefetchAheadOfPrerenderIfExist( |
| const GURL& url) { |
| // TODO(https://crbug.com/428500219): Update the logic for |
| // prerender-until-script. |
| auto it = |
| all_prefetches_.find(std::make_pair(url, PreloadingType::kPrerender)); |
| if (it == all_prefetches_.end()) { |
| return; |
| } |
| |
| static_cast<PrefetchHandleImpl*>(it->second.get()) |
| ->SetPrefetchStatusOnReleaseStartedPrefetch( |
| PrefetchStatus::kPrefetchEvictedAfterCandidateRemoved); |
| all_prefetches_.erase(it); |
| } |
| |
| PrefetchService* PrefetchDocumentManager::GetPrefetchService() const { |
| if (g_prefetch_service_for_testing) { |
| return g_prefetch_service_for_testing; |
| } |
| |
| DCHECK(BrowserContextImpl::From(render_frame_host().GetBrowserContext()) |
| ->GetPrefetchService()); |
| return BrowserContextImpl::From(render_frame_host().GetBrowserContext()) |
| ->GetPrefetchService(); |
| } |
| |
| void PrefetchDocumentManager::OnEligibilityCheckComplete(bool is_eligible) { |
| if (is_eligible) |
| referring_page_metrics_.prefetch_eligible_count++; |
| } |
| |
| void PrefetchDocumentManager::OnPrefetchSuccessful( |
| PrefetchContainer* prefetch) { |
| referring_page_metrics_.prefetch_successful_count++; |
| if (IsImmediateSpeculationEagerness( |
| prefetch->GetPrefetchType().GetEagerness())) { |
| completed_immediate_prefetches_.push_back(prefetch->GetWeakPtr()); |
| } else { |
| completed_non_immediate_prefetches_.push_back(prefetch->GetWeakPtr()); |
| } |
| } |
| |
| std::tuple<bool, base::WeakPtr<PrefetchContainer>> |
| PrefetchDocumentManager::CanPrefetchNow(PrefetchContainer* prefetch) { |
| RenderFrameHost* rfh = &render_frame_host(); |
| // The document needs to be active, primary and in a visible WebContents for |
| // the prefetch to be eligible. |
| if (!rfh->IsActive() || !rfh->GetPage().IsPrimary() || |
| WebContents::FromRenderFrameHost(rfh)->GetVisibility() != |
| Visibility::VISIBLE) { |
| return std::make_tuple(false, nullptr); |
| } |
| if (IsImmediateSpeculationEagerness( |
| prefetch->GetPrefetchType().GetEagerness())) { |
| return std::make_tuple(completed_immediate_prefetches_.size() < |
| kMaxNumberOfImmediatePrefetchesPerPage, |
| nullptr); |
| } else { |
| if (completed_non_immediate_prefetches_.size() < |
| kMaxNumberOfNonImmediatePrefetchesPerPage) { |
| return std::make_tuple(true, nullptr); |
| } |
| // We are at capacity, and now need to evict the oldest non-immediate |
| // prefetch to make space for a new one. |
| DCHECK(GetPrefetchService()); |
| base::WeakPtr<PrefetchContainer> oldest_prefetch = |
| completed_non_immediate_prefetches_.front(); |
| // TODO(crbug.com/40064525): We should also be checking if the prefetch is |
| // currently being used to serve a navigation. In that scenario, evicting |
| // doesn't make sense. |
| return std::make_tuple(true, oldest_prefetch); |
| } |
| } |
| |
| void PrefetchDocumentManager::SetPrefetchDestructionCallback( |
| PrefetchDestructionCallback callback) { |
| prefetch_destruction_callback_ = std::move(callback); |
| } |
| |
| void PrefetchDocumentManager::PrefetchWillBeDestroyed( |
| PrefetchContainer* prefetch) { |
| prefetch_destruction_callback_.Run(prefetch->GetURL()); |
| |
| std::vector<base::WeakPtr<PrefetchContainer>>& completed_prefetches = |
| IsImmediateSpeculationEagerness( |
| prefetch->GetPrefetchType().GetEagerness()) |
| ? completed_immediate_prefetches_ |
| : completed_non_immediate_prefetches_; |
| auto it = std::ranges::find(completed_prefetches, prefetch->key(), |
| [&](const auto& p) { return p->key(); }); |
| if (it != completed_prefetches.end()) { |
| completed_prefetches.erase(it); |
| } |
| } |
| |
| DOCUMENT_USER_DATA_KEY_IMPL(PrefetchDocumentManager); |
| |
| } // namespace content |