| // 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. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/341324165): Fix and remove. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #include "content/browser/preloading/prerenderer_impl.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "base/feature_list.h" |
| #include "base/strings/stringprintf.h" |
| #include "content/browser/preloading/prefetch/no_vary_search_helper.h" |
| #include "content/browser/preloading/prefetch/prefetch_document_manager.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/prerender/prerender_attributes.h" |
| #include "content/browser/preloading/prerender/prerender_features.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/preloading/speculation_rules/speculation_rules_util.h" |
| #include "content/browser/renderer_host/render_frame_host_delegate.h" |
| #include "content/public/browser/preloading_trigger_type.h" |
| #include "content/public/browser/web_contents.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| namespace content { |
| |
| struct PrerendererImpl::PrerenderInfo { |
| blink::mojom::SpeculationInjectionType injection_type; |
| blink::mojom::SpeculationEagerness eagerness; |
| bool is_target_blank; |
| FrameTreeNodeId prerender_host_id; |
| GURL url; |
| |
| PrerenderInfo() = default; |
| explicit PrerenderInfo( |
| const blink::mojom::SpeculationCandidatePtr& candidate); |
| |
| static bool PrerenderInfoComparator(const PrerenderInfo& p1, |
| const PrerenderInfo& p2); |
| |
| bool operator<(const PrerenderInfo& p) const { |
| return PrerenderInfoComparator(*this, p); |
| } |
| |
| bool operator==(const PrerenderInfo& p) const { |
| return !PrerenderInfoComparator(*this, p) && |
| !PrerenderInfoComparator(p, *this); |
| } |
| }; |
| |
| bool PrerendererImpl::PrerenderInfo::PrerenderInfoComparator( |
| const PrerenderInfo& p1, |
| const PrerenderInfo& p2) { |
| if (p1.url != p2.url) { |
| return p1.url < p2.url; |
| } |
| |
| return p1.is_target_blank < p2.is_target_blank; |
| } |
| |
| // `prerender_host_id` is not provided by `SpeculationCandidatePtr`, so |
| // FrameTreeNodeId() is assigned instead. The value should be updated once it is |
| // available. |
| PrerendererImpl::PrerenderInfo::PrerenderInfo( |
| const blink::mojom::SpeculationCandidatePtr& candidate) |
| : injection_type(candidate->injection_type), |
| eagerness(candidate->eagerness), |
| is_target_blank(candidate->target_browsing_context_name_hint == |
| blink::mojom::SpeculationTargetHint::kBlank), |
| url(candidate->url) {} |
| |
| 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(); |
| if (registry_) { |
| observation_.Observe(registry_.get()); |
| } |
| ResetReceivedPrerendersCountForMetrics(); |
| if (base::FeatureList::IsEnabled( |
| blink::features::kLCPTimingPredictorPrerender2)) { |
| blocked_ = true; |
| } |
| } |
| |
| PrerendererImpl::~PrerendererImpl() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CancelStartedPrerenders(); |
| RecordReceivedPrerendersCountToMetrics(); |
| ResetReceivedPrerendersCountForMetrics(); |
| } |
| |
| 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(); |
| RecordReceivedPrerendersCountToMetrics(); |
| ResetReceivedPrerendersCountForMetrics(); |
| } |
| |
| // 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<std::pair<size_t, blink::mojom::SpeculationCandidatePtr>> |
| prerender_candidates; |
| for (const auto& candidate : candidates) { |
| if (candidate->action == blink::mojom::SpeculationAction::kPrerender) { |
| prerender_candidates.emplace_back(prerender_candidates.size(), |
| candidate.Clone()); |
| } |
| } |
| |
| std::ranges::stable_sort( |
| prerender_candidates, std::less<>(), |
| [](const auto& p) { return PrerenderInfo(p.second); }); |
| std::vector<std::pair<size_t, 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<FrameTreeNodeId> 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 |
| // PrerenderInfos pointed at by the iterators, and compare the range of |
| // entries in each that match that PrerenderInfo. |
| // |
| // PrerenderInfos which are present in the prerender list but not the |
| // candidate list can no longer proceed and are cancelled. |
| // |
| // PrerenderInfos 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 PrerenderInfos to diff. |
| PrerenderInfo prerender_info; |
| if (started_it == started_prerenders_.end()) { |
| prerender_info = PrerenderInfo(candidate_it->second); |
| } else if (candidate_it == prerender_candidates.end()) { |
| prerender_info = *started_it; |
| } else { |
| prerender_info = |
| std::min(PrerenderInfo(candidate_it->second), *started_it); |
| } |
| |
| // Select the ranges from both that match the PrerenderInfo in question. |
| auto equal_prerender_end = std::ranges::find_if( |
| started_it, started_prerenders_.end(), |
| [&](const auto& started) { return started != prerender_info; }); |
| base::span<PrerenderInfo> matching_prerenders(started_it, |
| equal_prerender_end); |
| auto equal_candidate_end = std::ranges::find_if( |
| candidate_it, prerender_candidates.end(), [&](const auto& candidate) { |
| return PrerenderInfo(candidate.second) != prerender_info; |
| }); |
| base::span<std::pair<size_t, 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.is_null()) { |
| 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 PrerenderInfo 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 one candidate per target hint for a URL only if there are |
| // no matching prerenders. We could be cleverer in the future. |
| if (matching_prerenders.empty()) { |
| CHECK(!matching_candidates.empty()); |
| |
| std::set<PrerenderInfo> processed_prerender_info; |
| |
| for (auto& matching_candidate : matching_candidates) { |
| PrerenderInfo matching_candidate_prerender_info = |
| PrerenderInfo(matching_candidate.second); |
| if (processed_prerender_info |
| .insert(std::move(matching_candidate_prerender_info)) |
| .second) { |
| candidates_to_start.push_back(std::move(matching_candidate)); |
| } |
| } |
| } |
| |
| // Advance the iterators past all matching entries. |
| candidate_it = equal_candidate_end; |
| started_it = equal_prerender_end; |
| } |
| |
| std::vector<GURL> urls; |
| for (auto ftn_id : removed_prerender_rules) { |
| if (PrerenderHost* prerender_host = |
| registry_->FindNonReservedHostById(ftn_id)) { |
| urls.push_back(prerender_host->GetInitialUrl()); |
| } |
| } |
| std::set<FrameTreeNodeId> canceled_prerender_rules_set = |
| registry_->CancelHosts( |
| removed_prerender_rules, |
| PrerenderCancellationReason( |
| PrerenderFinalStatus::kSpeculationRuleRemoved)); |
| if (base::FeatureList::IsEnabled( |
| features::kPrerender2FallbackPrefetchSpecRules)) { |
| WebContents* web_contents = |
| WebContents::FromRenderFrameHost(&render_frame_host_.get()); |
| auto* prefetch_document_manager = |
| content::PrefetchDocumentManager::GetOrCreateForCurrentDocument( |
| web_contents->GetPrimaryMainFrame()); |
| for (const auto& url : urls) { |
| prefetch_document_manager->ResetPrefetchAheadOfPrerenderIfExist(url); |
| } |
| } |
| |
| // Canceled prerenders by kSpeculationRuleRemoved should have already been |
| // removed from `started_prerenders_` via `OnCancel`. |
| CHECK(std::find_if(started_prerenders_.begin(), started_prerenders_.end(), |
| [&](const PrerenderInfo& x) { |
| return base::Contains(canceled_prerender_rules_set, |
| x.prerender_host_id); |
| }) == started_prerenders_.end()); |
| |
| // Actually start the candidates in their original order once the diffing is |
| // done. |
| std::ranges::sort(candidates_to_start, std::less<>(), |
| [](const auto& p) { return p.first; }); |
| for (const auto& [_, candidate] : candidates_to_start) { |
| PreloadingTriggerType trigger_type = |
| PreloadingTriggerTypeFromSpeculationInjectionType( |
| candidate->injection_type); |
| // Immediate candidates are enacted by the same predictor that creates them. |
| PreloadingPredictor enacting_predictor = |
| GetPredictorForPreloadingTriggerType(trigger_type); |
| MaybePrerender(candidate, enacting_predictor, PreloadingConfidence{100}); |
| } |
| } |
| |
| void PrerendererImpl::OnLCPPredicted() { |
| blocked_ = false; |
| for (auto& [candidate, enacting_predictor, confidence] : |
| std::move(blocked_candidates_)) { |
| MaybePrerender(candidate, enacting_predictor, confidence); |
| } |
| } |
| |
| bool PrerendererImpl::MaybePrerender( |
| const blink::mojom::SpeculationCandidatePtr& candidate, |
| const PreloadingPredictor& enacting_predictor, |
| PreloadingConfidence confidence) { |
| CHECK_EQ(candidate->action, blink::mojom::SpeculationAction::kPrerender); |
| |
| // Prerendering is not allowed in fenced frames. |
| if (render_frame_host_->IsNestedWithinFencedFrame()) { |
| render_frame_host_->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, |
| "The SpeculationRules API does not support prerendering in fenced " |
| "frames."); |
| return false; |
| } |
| |
| WebContents* web_contents = |
| WebContents::FromRenderFrameHost(&render_frame_host_.get()); |
| static_cast<PreloadingDataImpl*>( |
| PreloadingData::GetOrCreateForWebContents(web_contents)) |
| ->SetHasSpeculationRulesPrerender(); |
| if (blocked_) { |
| blocked_candidates_.emplace_back(candidate->Clone(), enacting_predictor, |
| confidence); |
| return false; |
| } |
| |
| // Prerendering frames should not trigger any prerender request. |
| CHECK(!render_frame_host_->IsInLifecycleState( |
| RenderFrameHost::LifecycleState::kPrerendering)); |
| |
| if (!registry_) |
| return false; |
| |
| auto& rfhi = static_cast<RenderFrameHostImpl&>(render_frame_host_.get()); |
| |
| // `prerender_host_id` is not available yet. |
| PrerenderInfo prerender_info(candidate); |
| |
| auto [begin, end] = std::ranges::equal_range( |
| started_prerenders_.begin(), started_prerenders_.end(), prerender_info, |
| PrerenderInfo::PrerenderInfoComparator); |
| // cannot currently start a second prerender with the same URL and target_hint |
| if (begin != end) { |
| return false; |
| } |
| |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &rfhi, blink::mojom::WebFeature::kSpeculationRulesPrerender); |
| |
| IncrementReceivedPrerendersCountForMetrics( |
| PreloadingTriggerTypeFromSpeculationInjectionType( |
| candidate->injection_type), |
| candidate->eagerness); |
| |
| // TODO(crbug.com/40168192): 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 (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())); |
| } |
| |
| std::optional<net::HttpNoVarySearchData> no_vary_search_hint; |
| if (candidate->no_vary_search_hint) { |
| no_vary_search_hint = no_vary_search::ParseHttpNoVarySearchDataFromMojom( |
| candidate->no_vary_search_hint); |
| } |
| |
| const bool should_warm_up_compositor = base::FeatureList::IsEnabled( |
| IsImmediateSpeculationEagerness(candidate->eagerness) |
| ? features::kPrerender2WarmUpCompositorForImmediate |
| : features::kPrerender2WarmUpCompositorForNonImmediate); |
| |
| PrerenderAttributes attributes( |
| candidate->url, |
| PreloadingTriggerTypeFromSpeculationInjectionType( |
| candidate->injection_type), |
| /*embedder_histogram_suffix=*/"", |
| SpeculationRulesParams(candidate->target_browsing_context_name_hint, |
| candidate->eagerness, |
| SpeculationRulesTags(candidate->tags)), |
| Referrer{*candidate->referrer}, no_vary_search_hint, &rfhi, |
| web_contents->GetWeakPtr(), ui::PAGE_TRANSITION_LINK, |
| should_warm_up_compositor, |
| /*should_prepare_paint_tree=*/false, |
| /*url_match_predicate=*/{}, |
| /*prerender_navigation_handle_callback=*/{}, |
| PreloadPipelineInfoImpl::Create( |
| /*planned_max_preloading_type=*/PreloadingType::kPrerender), |
| /*allow_reuse=*/false); |
| |
| PreloadingTriggerType trigger_type = |
| PreloadingTriggerTypeFromSpeculationInjectionType( |
| candidate->injection_type); |
| PreloadingPredictor creating_predictor = |
| GetPredictorForPreloadingTriggerType(trigger_type); |
| prerender_info.prerender_host_id = [&] { |
| // TODO(crbug.com/40235424): 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)) { |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &rfhi, |
| blink::mojom::WebFeature::kSpeculationRulesTargetHintBlank); |
| // For the prerender-in-new-tab, PreloadingAttempt will be managed by |
| // a prerender WebContents to be created later. |
| return registry_->CreateAndStartHostForNewTab( |
| attributes, creating_predictor, enacting_predictor, confidence); |
| } |
| // 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: { |
| if (base::FeatureList::IsEnabled( |
| features::kPrerender2FallbackPrefetchSpecRules)) { |
| auto* prefetch_document_manager = |
| content::PrefetchDocumentManager::GetOrCreateForCurrentDocument( |
| web_contents->GetPrimaryMainFrame()); |
| prefetch_document_manager->PrefetchAheadOfPrerender( |
| attributes.preload_pipeline_info, candidate.Clone(), |
| enacting_predictor); |
| } |
| |
| // Create new PreloadingAttempt and pass all the values corresponding to |
| // this prerendering attempt. |
| auto* preloading_data = |
| PreloadingDataImpl::GetOrCreateForWebContents(web_contents); |
| PreloadingURLMatchCallback same_url_matcher = |
| PreloadingData::GetSameURLMatcher(candidate->url); |
| |
| auto* preloading_attempt = static_cast<PreloadingAttemptImpl*>( |
| preloading_data->AddPreloadingAttempt( |
| creating_predictor, enacting_predictor, |
| PreloadingType::kPrerender, std::move(same_url_matcher), |
| web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId())); |
| preloading_attempt->SetSpeculationEagerness(candidate->eagerness); |
| return registry_->CreateAndStartHost(attributes, preloading_attempt); |
| } |
| } |
| }(); |
| |
| // An existing prerender may be canceled to start a new prerender, and |
| // `started_prerenders_` may be modified through this cancellation. Therefore, |
| // it is needed to re-calculate the right place here on `started_prerenders_` |
| // for new candidates. |
| end = std::ranges::upper_bound(started_prerenders_.begin(), |
| started_prerenders_.end(), prerender_info, |
| PrerenderInfo::PrerenderInfoComparator); |
| |
| started_prerenders_.insert(end, std::move(prerender_info)); |
| |
| return true; |
| } |
| |
| bool PrerendererImpl::ShouldWaitForPrerenderResult(const GURL& url) { |
| // This function is used to check whetehr a prerender is started to avoid |
| // starting prefetch in OnPointerDown, OnPointerHover or other heuristic |
| // methods which don't take target_hint into consideration. So unlike other |
| // functions in this file, this part uses `url` only instead of |
| // `PrerenderInfo` which consists of target_hint information. |
| auto [begin, end] = std::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.is_null()) { |
| return false; |
| } |
| } |
| return begin != end; |
| } |
| |
| void PrerendererImpl::OnCancel(FrameTreeNodeId host_frame_tree_node_id, |
| const PrerenderCancellationReason& reason) { |
| switch (reason.final_status()) { |
| // TODO(crbug.com/40275452): Support other final status cases. |
| case PrerenderFinalStatus::kTimeoutBackgrounded: |
| case PrerenderFinalStatus::kMaxNumOfRunningNonImmediatePrerendersExceeded: |
| case PrerenderFinalStatus::kSpeculationRuleRemoved: { |
| auto erasing_prerender_it = std::find_if( |
| started_prerenders_.begin(), started_prerenders_.end(), |
| [&](const PrerenderInfo& prerender_info) { |
| return prerender_info.prerender_host_id == host_frame_tree_node_id; |
| }); |
| |
| if (erasing_prerender_it != started_prerenders_.end()) { |
| auto url = erasing_prerender_it->url; |
| started_prerenders_.erase(erasing_prerender_it); |
| |
| // Notify PreloadingDecider. |
| prerender_cancellation_callback_.Run(url); |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| |
| void PrerendererImpl::OnRegistryDestroyed() { |
| observation_.Reset(); |
| } |
| |
| void PrerendererImpl::SetPrerenderCancellationCallback( |
| PrerenderCancellationCallback callback) { |
| prerender_cancellation_callback_ = std::move(callback); |
| } |
| |
| void PrerendererImpl::CancelStartedPrerenders() { |
| if (registry_) { |
| std::vector<FrameTreeNodeId> 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(); |
| } |
| |
| void PrerendererImpl::CancelStartedPrerendersForTesting() { |
| CancelStartedPrerenders(); |
| } |
| |
| void PrerendererImpl::ResetReceivedPrerendersCountForMetrics() { |
| for (auto trigger_type : |
| {PreloadingTriggerType::kSpeculationRule, |
| PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld}) { |
| received_prerenders_by_eagerness_[trigger_type].fill({}); |
| } |
| } |
| |
| void PrerendererImpl::IncrementReceivedPrerendersCountForMetrics( |
| PreloadingTriggerType trigger_type, |
| blink::mojom::SpeculationEagerness eagerness) { |
| received_prerenders_by_eagerness_[trigger_type] |
| [static_cast<size_t>(eagerness)]++; |
| } |
| |
| void PrerendererImpl::RecordReceivedPrerendersCountToMetrics() { |
| for (auto trigger_type : |
| {PreloadingTriggerType::kSpeculationRule, |
| PreloadingTriggerType::kSpeculationRuleFromIsolatedWorld}) { |
| int conservative = |
| received_prerenders_by_eagerness_[trigger_type][static_cast<size_t>( |
| blink::mojom::SpeculationEagerness::kConservative)]; |
| int moderate = |
| received_prerenders_by_eagerness_[trigger_type][static_cast<size_t>( |
| blink::mojom::SpeculationEagerness::kModerate)]; |
| int eager = |
| received_prerenders_by_eagerness_[trigger_type][static_cast<size_t>( |
| blink::mojom::SpeculationEagerness::kEager)]; |
| int immediate = |
| received_prerenders_by_eagerness_[trigger_type][static_cast<size_t>( |
| blink::mojom::SpeculationEagerness::kImmediate)]; |
| |
| // This will record zero when |
| // 1) there are no started prerenders eventually. Also noted that if |
| // there is no rule set, PreloadingDecider won't be created (which means |
| // PrerenderImpl also won't be created), so it cannot be reached this |
| // code path at the first place. |
| // 2) when the corresponding RFH lives but is inactive (such as the case in |
| // BFCache) after once PrimaryPageChanged was called and the recorded |
| // number was reset (As long as PreloadingDecider (which has the same |
| // lifetime with a document) that owns this (PrerenderImpl) lives, this |
| // function will be called per PrimaryPageChanged). |
| // |
| // Avoids recording these cases uniformly. |
| if (conservative + moderate + eager + immediate == 0) { |
| continue; |
| } |
| |
| // Record per single eagerness. |
| RecordReceivedPrerendersPerPrimaryPageChangedCount( |
| conservative, trigger_type, "Conservative"); |
| RecordReceivedPrerendersPerPrimaryPageChangedCount(moderate, trigger_type, |
| "Moderate"); |
| // `kEager` is treated as `kImmediate` here for historical reasons. |
| // TODO(crbug.com/40287486): Create new metrics to separate them. |
| RecordReceivedPrerendersPerPrimaryPageChangedCount( |
| eager + immediate, trigger_type, "Immediate"); |
| } |
| } |
| |
| } // namespace content |