| // 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_container.h" |
| |
| #include <memory> |
| |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/time/time.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/preloading/prefetch/no_vary_search_helper.h" |
| #include "content/browser/preloading/prefetch/prefetch_cookie_listener.h" |
| #include "content/browser/preloading/prefetch/prefetch_document_manager.h" |
| #include "content/browser/preloading/prefetch/prefetch_network_context.h" |
| #include "content/browser/preloading/prefetch/prefetch_params.h" |
| #include "content/browser/preloading/prefetch/prefetch_probe_result.h" |
| #include "content/browser/preloading/prefetch/prefetch_service.h" |
| #include "content/browser/preloading/prefetch/prefetch_serving_page_metrics_container.h" |
| #include "content/browser/preloading/prefetch/prefetch_status.h" |
| #include "content/browser/preloading/prefetch/prefetch_streaming_url_loader.h" |
| #include "content/browser/preloading/prefetch/prefetch_type.h" |
| #include "content/browser/preloading/prefetch/proxy_lookup_client_impl.h" |
| #include "content/browser/preloading/preloading.h" |
| #include "content/browser/preloading/preloading_data_impl.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/preloading.h" |
| #include "content/public/browser/web_contents.h" |
| #include "services/metrics/public/cpp/metrics_utils.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/mojom/cookie_manager.mojom.h" |
| #include "url/gurl.h" |
| |
| namespace content { |
| namespace { |
| |
| void RecordCookieCopyTimes( |
| const base::TimeTicks& cookie_copy_start_time, |
| const base::TimeTicks& cookie_read_end_and_write_start_time, |
| const base::TimeTicks& cookie_copy_end_time) { |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "PrefetchProxy.AfterClick.Mainframe.CookieReadTime", |
| cookie_read_end_and_write_start_time - cookie_copy_start_time, |
| base::TimeDelta(), base::Seconds(5), 50); |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "PrefetchProxy.AfterClick.Mainframe.CookieWriteTime", |
| cookie_copy_end_time - cookie_read_end_and_write_start_time, |
| base::TimeDelta(), base::Seconds(5), 50); |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "PrefetchProxy.AfterClick.Mainframe.CookieCopyTime", |
| cookie_copy_end_time - cookie_copy_start_time, base::TimeDelta(), |
| base::Seconds(5), 50); |
| } |
| |
| static_assert( |
| static_cast<int>(PrefetchStatus::kMaxValue) + |
| static_cast<int>( |
| PreloadingEligibility::kPreloadingEligibilityCommonEnd) <= |
| static_cast<int>(PreloadingEligibility::kPreloadingEligibilityContentEnd)); |
| |
| PreloadingEligibility ToPreloadingEligibility(PrefetchStatus status) { |
| if (status == PrefetchStatus::kPrefetchNotEligibleDataSaverEnabled) { |
| return PreloadingEligibility::kDataSaverEnabled; |
| } |
| return static_cast<PreloadingEligibility>( |
| static_cast<int>(status) + |
| static_cast<int>(PreloadingEligibility::kPreloadingEligibilityCommonEnd)); |
| } |
| |
| // Please follow go/preloading-dashboard-updates if a new eligibility is added. |
| void SetIneligibilityFromStatus(PreloadingAttempt* attempt, |
| PrefetchStatus status) { |
| if (attempt) { |
| switch (status) { |
| case PrefetchStatus::kPrefetchNotEligibleBrowserContextOffTheRecord: |
| case PrefetchStatus::kPrefetchNotEligibleDataSaverEnabled: |
| case PrefetchStatus::kPrefetchNotEligibleHostIsNonUnique: |
| case PrefetchStatus::kPrefetchNotEligibleSchemeIsNotHttps: |
| case PrefetchStatus::kPrefetchProxyNotAvailable: |
| case PrefetchStatus::kPrefetchNotEligibleNonDefaultStoragePartition: |
| case PrefetchStatus::kPrefetchIneligibleRetryAfter: |
| case PrefetchStatus::kPrefetchNotEligibleUserHasServiceWorker: |
| case PrefetchStatus::kPrefetchNotEligibleUserHasCookies: |
| case PrefetchStatus::kPrefetchNotEligibleExistingProxy: |
| attempt->SetEligibility(ToPreloadingEligibility(status)); |
| break; |
| default: |
| NOTIMPLEMENTED(); |
| } |
| } |
| } |
| |
| static_assert( |
| static_cast<int>(PrefetchStatus::kMaxValue) + |
| static_cast<int>( |
| PreloadingFailureReason::kPreloadingFailureReasonCommonEnd) <= |
| static_cast<int>( |
| PreloadingFailureReason::kPreloadingFailureReasonContentEnd)); |
| |
| PreloadingFailureReason ToPreloadingFailureReason(PrefetchStatus status) { |
| return static_cast<PreloadingFailureReason>( |
| static_cast<int>(status) + |
| static_cast<int>( |
| PreloadingFailureReason::kPreloadingFailureReasonCommonEnd)); |
| } |
| |
| // Please follow go/preloading-dashboard-updates if a new outcome enum or a |
| // failure reason enum is added. |
| void SetTriggeringOutcomeAndFailureReasonFromStatus( |
| const absl::optional<base::UnguessableToken>& |
| initiator_devtools_navigation_token, |
| PreloadingAttempt* attempt, |
| FrameTreeNode* ftn, |
| const GURL& url, |
| absl::optional<PrefetchStatus> old_prefetch_status, |
| PrefetchStatus new_prefetch_status) { |
| if (old_prefetch_status && |
| (old_prefetch_status.value() == PrefetchStatus::kPrefetchUsedNoProbe || |
| old_prefetch_status.value() == PrefetchStatus::kPrefetchResponseUsed)) { |
| // Skip this update if the triggering outcome has already been updated |
| // to kSuccess. |
| return; |
| } |
| |
| if (attempt) { |
| switch (new_prefetch_status) { |
| case PrefetchStatus::kPrefetchNotFinishedInTime: |
| if (initiator_devtools_navigation_token.has_value()) { |
| devtools_instrumentation::DidUpdatePrefetchStatus( |
| ftn, initiator_devtools_navigation_token.value(), url, |
| PreloadingTriggeringOutcome::kRunning); |
| } |
| attempt->SetTriggeringOutcome(PreloadingTriggeringOutcome::kRunning); |
| break; |
| case PrefetchStatus::kPrefetchSuccessful: |
| // A successful prefetch means the response is ready to be used for the |
| // next navigation. |
| if (initiator_devtools_navigation_token.has_value()) { |
| devtools_instrumentation::DidUpdatePrefetchStatus( |
| ftn, initiator_devtools_navigation_token.value(), url, |
| PreloadingTriggeringOutcome::kReady); |
| } |
| attempt->SetTriggeringOutcome(PreloadingTriggeringOutcome::kReady); |
| break; |
| case PrefetchStatus::kPrefetchUsedNoProbe: |
| case PrefetchStatus::kPrefetchResponseUsed: |
| if (old_prefetch_status && old_prefetch_status.value() != |
| PrefetchStatus::kPrefetchSuccessful) { |
| // If the new prefetch status is |kPrefetchResponseUsed| or |
| // |kPrefetchUsedNoProbe| but the previous status is not |
| // |kPrefetchSuccessful|, then temporarily update the triggering |
| // outcome to |kReady| to ensure valid triggering outcome state |
| // transitions. This can occur in cases where the prefetch is served |
| // before the body is fully received. |
| attempt->SetTriggeringOutcome(PreloadingTriggeringOutcome::kReady); |
| } |
| |
| if (initiator_devtools_navigation_token.has_value()) { |
| devtools_instrumentation::DidUpdatePrefetchStatus( |
| ftn, initiator_devtools_navigation_token.value(), url, |
| PreloadingTriggeringOutcome::kSuccess); |
| } |
| attempt->SetTriggeringOutcome(PreloadingTriggeringOutcome::kSuccess); |
| break; |
| // A decoy is considered eligible because a network request is made for |
| // it. It is considered as a failure as the final response is never |
| // served. |
| case PrefetchStatus::kPrefetchIsPrivacyDecoy: |
| case PrefetchStatus::kPrefetchFailedNetError: |
| case PrefetchStatus::kPrefetchFailedNon2XX: |
| case PrefetchStatus::kPrefetchFailedMIMENotSupported: |
| case PrefetchStatus::kPrefetchFailedInvalidRedirect: |
| case PrefetchStatus::kPrefetchFailedIneligibleRedirect: |
| if (initiator_devtools_navigation_token.has_value()) { |
| devtools_instrumentation::DidUpdatePrefetchStatus( |
| ftn, initiator_devtools_navigation_token.value(), url, |
| PreloadingTriggeringOutcome::kFailure); |
| } |
| attempt->SetFailureReason( |
| ToPreloadingFailureReason(new_prefetch_status)); |
| break; |
| case PrefetchStatus::kPrefetchHeldback: |
| // `kPrefetchAllowed` will soon transition into `kPrefetchNotStarted`. |
| case PrefetchStatus::kPrefetchAllowed: |
| case PrefetchStatus::kPrefetchNotStarted: |
| // `kPrefetchNotStarted` is set in |
| // `PrefetchService::OnGotEligibilityResult` when the container is |
| // pushed onto the prefetch queue, which occurs before the holdback |
| // status is determined in `PrefetchService::StartSinglePrefetch`. |
| // After the container is queued and before it is sent for prefetch, the |
| // only status change is when the container is popped from the queue but |
| // heldback. This is covered by attempt's holdback status. For these two |
| // reasons this PrefetchStatus does not fire a `SetTriggeringOutcome`. |
| break; |
| case PrefetchStatus::kPrefetchNotEligibleGoogleDomain: |
| case PrefetchStatus::kPrefetchNotEligibleUserHasServiceWorker: |
| case PrefetchStatus::kPrefetchNotEligibleSchemeIsNotHttps: |
| case PrefetchStatus::kPrefetchNotEligibleNonDefaultStoragePartition: |
| case PrefetchStatus::kPrefetchNotEligibleHostIsNonUnique: |
| case PrefetchStatus::kPrefetchNotEligibleDataSaverEnabled: |
| case PrefetchStatus::kPrefetchNotEligibleExistingProxy: |
| case PrefetchStatus::kPrefetchNotEligibleUserHasCookies: |
| case PrefetchStatus::kPrefetchIneligibleRetryAfter: |
| case PrefetchStatus::kPrefetchProxyNotAvailable: |
| case PrefetchStatus::kPrefetchNotEligibleBrowserContextOffTheRecord: |
| case PrefetchStatus::kPrefetchNotUsedCookiesChanged: |
| case PrefetchStatus::kPrefetchIsStale: |
| case PrefetchStatus::kPrefetchNotUsedProbeFailed: |
| case PrefetchStatus::kNavigatedToLinkNotOnSRP: |
| case PrefetchStatus::kPrefetchUsedNoProbeWithNSP: |
| case PrefetchStatus::kPrefetchUsedProbeSuccessWithNSP: |
| case PrefetchStatus::kPrefetchNotUsedProbeFailedWithNSP: |
| case PrefetchStatus::kPrefetchUsedNoProbeNSPAttemptDenied: |
| case PrefetchStatus::kPrefetchUsedProbeSuccessNSPAttemptDenied: |
| case PrefetchStatus::kPrefetchNotUsedProbeFailedNSPAttemptDenied: |
| case PrefetchStatus::kPrefetchUsedNoProbeNSPNotStarted: |
| case PrefetchStatus::kPrefetchUsedProbeSuccessNSPNotStarted: |
| case PrefetchStatus::kPrefetchNotUsedProbeFailedNSPNotStarted: |
| case PrefetchStatus::kPrefetchIsStaleWithNSP: |
| case PrefetchStatus::kPrefetchIsStaleNSPAttemptDenied: |
| case PrefetchStatus::kPrefetchIsStaleNSPNotStarted: |
| case PrefetchStatus::kSubresourceThrottled: |
| case PrefetchStatus::kPrefetchPositionIneligible: |
| case PrefetchStatus::kPrefetchFailedRedirectsDisabled_DEPRECATED: |
| NOTIMPLEMENTED(); |
| } |
| } |
| } |
| |
| std::string GetEagernessHistogramSuffix( |
| const blink::mojom::SpeculationEagerness& eagerness) { |
| switch (eagerness) { |
| case blink::mojom::SpeculationEagerness::kEager: |
| return "Eager"; |
| case blink::mojom::SpeculationEagerness::kModerate: |
| return "Moderate"; |
| case blink::mojom::SpeculationEagerness::kConservative: |
| return "Conservative"; |
| } |
| } |
| |
| void RecordWasBlockedUntilHeadWhenServingHistogram( |
| const blink::mojom::SpeculationEagerness& eagerness, |
| bool blocked_until_head) { |
| base::UmaHistogramBoolean( |
| base::StringPrintf( |
| "PrefetchProxy.AfterClick.WasBlockedUntilHeadWhenServing.%s", |
| GetEagernessHistogramSuffix(eagerness).c_str()), |
| blocked_until_head); |
| } |
| |
| void RecordBlockUntilHeadDurationHistogram( |
| const blink::mojom::SpeculationEagerness& eagerness, |
| const base::TimeDelta& block_until_head_duration, |
| bool served) { |
| base::UmaHistogramTimes( |
| base::StringPrintf( |
| "PrefetchProxy.AfterClick.BlockUntilHeadDuration.%s.%s", |
| served ? "Served" : "NotServed", |
| GetEagernessHistogramSuffix(eagerness).c_str()), |
| block_until_head_duration); |
| } |
| |
| } // namespace |
| |
| PrefetchContainer::PrefetchContainer( |
| const GlobalRenderFrameHostId& referring_render_frame_host_id, |
| const GURL& url, |
| const PrefetchType& prefetch_type, |
| const blink::mojom::Referrer& referrer, |
| base::WeakPtr<PrefetchDocumentManager> prefetch_document_manager) |
| : referring_render_frame_host_id_(referring_render_frame_host_id), |
| prefetch_url_(url), |
| prefetch_type_(prefetch_type), |
| referrer_(referrer), |
| prefetch_document_manager_(prefetch_document_manager), |
| ukm_source_id_(prefetch_document_manager_ |
| ? prefetch_document_manager_->render_frame_host() |
| .GetPageUkmSourceId() |
| : ukm::kInvalidSourceId), |
| request_id_(base::UnguessableToken::Create().ToString()), |
| initiator_devtools_navigation_token_( |
| prefetch_document_manager |
| ? prefetch_document_manager->initiator_devtools_navigation_token() |
| : absl::nullopt) { |
| auto* rfh = RenderFrameHost::FromID(referring_render_frame_host_id_); |
| if (rfh) { |
| auto* preloading_data = PreloadingData::GetOrCreateForWebContents( |
| WebContents::FromRenderFrameHost(rfh)); |
| auto matcher = |
| base::FeatureList::IsEnabled(network::features::kPrefetchNoVarySearch) |
| ? PreloadingDataImpl::GetSameURLAndNoVarySearchURLMatcher( |
| prefetch_document_manager_, prefetch_url_) |
| : PreloadingDataImpl::GetSameURLMatcher(prefetch_url_); |
| auto* attempt = preloading_data->AddPreloadingAttempt( |
| content_preloading_predictor::kSpeculationRules, |
| PreloadingType::kPrefetch, std::move(matcher)); |
| attempt_ = attempt->GetWeakPtr(); |
| // `PreloadingPrediction` is added in `PreloadingDecider`. |
| } |
| |
| redirect_chain_.push_back(std::make_unique<SinglePrefetch>(prefetch_url_)); |
| } |
| |
| PrefetchContainer::~PrefetchContainer() { |
| ukm::builders::PrefetchProxy_PrefetchedResource builder(ukm_source_id_); |
| builder.SetResourceType(/*mainframe*/ 1); |
| builder.SetStatus(static_cast<int>( |
| prefetch_status_.value_or(PrefetchStatus::kPrefetchNotStarted))); |
| builder.SetLinkClicked(navigated_to_); |
| |
| if (prefetch_response_sizes_) { |
| builder.SetDataLength(ukm::GetExponentialBucketMinForBytes( |
| prefetch_response_sizes_->encoded_data_length)); |
| } |
| |
| if (fetch_duration_) { |
| builder.SetFetchDurationMS(fetch_duration_->InMilliseconds()); |
| } |
| |
| if (probe_result_) { |
| builder.SetISPFilteringStatus(static_cast<int>(probe_result_.value())); |
| } |
| |
| // TODO(https://crbug.com/1299059): Get the navigation start time and set the |
| // NavigationStartToFetchStartMs field of the PrefetchProxy.PrefetchedResource |
| // UKM event. |
| |
| builder.Record(ukm::UkmRecorder::Get()); |
| } |
| |
| void PrefetchContainer::SetPrefetchStatus(PrefetchStatus prefetch_status) { |
| FrameTreeNode* ftn = FrameTreeNode::From( |
| RenderFrameHostImpl::FromID(referring_render_frame_host_id_)); |
| SetTriggeringOutcomeAndFailureReasonFromStatus( |
| initiator_devtools_navigation_token_, attempt_.get(), ftn, prefetch_url_, |
| /*old_prefetch_status=*/prefetch_status_, |
| /*new_prefetch_status=*/prefetch_status); |
| prefetch_status_ = prefetch_status; |
| } |
| |
| PrefetchStatus PrefetchContainer::GetPrefetchStatus() const { |
| DCHECK(prefetch_status_); |
| return prefetch_status_.value(); |
| } |
| |
| void PrefetchContainer::TakeProxyLookupClient( |
| std::unique_ptr<ProxyLookupClientImpl> proxy_lookup_client) { |
| DCHECK(!proxy_lookup_client_); |
| proxy_lookup_client_ = std::move(proxy_lookup_client); |
| } |
| |
| std::unique_ptr<ProxyLookupClientImpl> |
| PrefetchContainer::ReleaseProxyLookupClient() { |
| DCHECK(proxy_lookup_client_); |
| return std::move(proxy_lookup_client_); |
| } |
| |
| PrefetchNetworkContext* PrefetchContainer::GetOrCreateNetworkContext( |
| PrefetchService* prefetch_service) { |
| if (!network_context_) { |
| network_context_ = std::make_unique<PrefetchNetworkContext>( |
| prefetch_service, prefetch_type_, referrer_, |
| referring_render_frame_host_id_); |
| } |
| return network_context_.get(); |
| } |
| |
| PrefetchDocumentManager* PrefetchContainer::GetPrefetchDocumentManager() const { |
| return prefetch_document_manager_.get(); |
| } |
| |
| void PrefetchContainer::OnEligibilityCheckComplete( |
| const GURL& url, |
| bool is_eligible, |
| absl::optional<PrefetchStatus> status) { |
| SinglePrefetch* this_prefetch = GetSinglePrefetch(url); |
| DCHECK(this_prefetch); |
| this_prefetch->is_eligible_ = is_eligible; |
| if (this_prefetch->on_eligibility_check_complete_callback_) { |
| std::move(this_prefetch->on_eligibility_check_complete_callback_) |
| .Run(is_eligible); |
| } |
| |
| if (url == prefetch_url_) { |
| // This case is for just the URL that was originally requested to be |
| // prefetched. |
| if (!is_eligible) { |
| // Expect a reason (status) if not eligible. |
| DCHECK(status.has_value()); |
| prefetch_status_ = status; |
| } |
| |
| if (attempt_) { |
| if (is_eligible) { |
| attempt_->SetEligibility(PreloadingEligibility::kEligible); |
| } else { |
| SetIneligibilityFromStatus(attempt_.get(), prefetch_status_.value()); |
| } |
| } |
| |
| prefetch_document_manager_->OnEligibilityCheckComplete(is_eligible); |
| } else { |
| // This case is for any URLs from redirects. |
| if (!is_eligible) { |
| SetPrefetchStatus(PrefetchStatus::kPrefetchFailedIneligibleRedirect); |
| } |
| } |
| } |
| |
| bool PrefetchContainer::IsInitialPrefetchEligible() const { |
| DCHECK(redirect_chain_.size() > 0); |
| return redirect_chain_[0]->is_eligible_ |
| ? redirect_chain_[0]->is_eligible_.value() |
| : false; |
| } |
| |
| void PrefetchContainer::AddRedirectHop(const GURL& url) { |
| redirect_chain_.push_back(std::make_unique<SinglePrefetch>(url)); |
| } |
| |
| absl::optional<bool> PrefetchContainer::GetEligibilityResultForRedirect( |
| const GURL& url) { |
| SinglePrefetch* this_prefetch = GetSinglePrefetch(url); |
| DCHECK(this_prefetch); |
| return this_prefetch->is_eligible_; |
| } |
| |
| void PrefetchContainer::SetOnEligibilityCheckCompleteCallback( |
| const GURL& url, |
| OnEligibilityCheckCompleteCallback on_eligibility_check_complete_callback) { |
| SinglePrefetch* this_prefetch = GetSinglePrefetch(url); |
| DCHECK(this_prefetch); |
| |
| this_prefetch->on_eligibility_check_complete_callback_ = |
| std::move(on_eligibility_check_complete_callback); |
| } |
| |
| void PrefetchContainer::RegisterCookieListener( |
| const GURL& url, |
| network::mojom::CookieManager* cookie_manager) { |
| SinglePrefetch* this_prefetch = GetSinglePrefetch(url); |
| DCHECK(this_prefetch); |
| |
| this_prefetch->cookie_listener_ = PrefetchCookieListener::MakeAndRegister( |
| this_prefetch->url_, cookie_manager); |
| } |
| |
| void PrefetchContainer::StopAllCookieListeners() { |
| for (const auto& single_prefetch : redirect_chain_) { |
| if (single_prefetch->cookie_listener_) { |
| single_prefetch->cookie_listener_->StopListening(); |
| } |
| } |
| } |
| |
| bool PrefetchContainer::HaveDefaultContextCookiesChanged( |
| const GURL& url) const { |
| SinglePrefetch* this_prefetch = GetSinglePrefetch(url); |
| DCHECK(this_prefetch); |
| |
| if (this_prefetch->cookie_listener_) { |
| return this_prefetch->cookie_listener_->HaveCookiesChanged(); |
| } |
| return false; |
| } |
| |
| bool PrefetchContainer::HasIsolatedCookieCopyStarted() const { |
| switch ( |
| redirect_chain_[index_redirect_chain_to_serve_]->cookie_copy_status_) { |
| case SinglePrefetch::CookieCopyStatus::kNotStarted: |
| return false; |
| case SinglePrefetch::CookieCopyStatus::kInProgress: |
| case SinglePrefetch::CookieCopyStatus::kCompleted: |
| return true; |
| } |
| } |
| |
| bool PrefetchContainer::IsIsolatedCookieCopyInProgress() const { |
| switch ( |
| redirect_chain_[index_redirect_chain_to_serve_]->cookie_copy_status_) { |
| case SinglePrefetch::CookieCopyStatus::kNotStarted: |
| case SinglePrefetch::CookieCopyStatus::kCompleted: |
| return false; |
| case SinglePrefetch::CookieCopyStatus::kInProgress: |
| return true; |
| } |
| } |
| |
| void PrefetchContainer::OnIsolatedCookieCopyStart() { |
| DCHECK(!IsIsolatedCookieCopyInProgress()); |
| |
| // We don't want any of the cookie listeners for this prefetch to pick up |
| // changes from the copy. |
| StopAllCookieListeners(); |
| |
| redirect_chain_[index_redirect_chain_to_serve_]->cookie_copy_status_ = |
| SinglePrefetch::CookieCopyStatus::kInProgress; |
| |
| redirect_chain_[index_redirect_chain_to_serve_]->cookie_copy_start_time_ = |
| base::TimeTicks::Now(); |
| } |
| |
| void PrefetchContainer::OnIsolatedCookiesReadCompleteAndWriteStart() { |
| DCHECK(IsIsolatedCookieCopyInProgress()); |
| |
| redirect_chain_[index_redirect_chain_to_serve_] |
| ->cookie_read_end_and_write_start_time_ = base::TimeTicks::Now(); |
| } |
| |
| void PrefetchContainer::OnIsolatedCookieCopyComplete() { |
| DCHECK(IsIsolatedCookieCopyInProgress()); |
| |
| const auto& this_prefetch = redirect_chain_[index_redirect_chain_to_serve_]; |
| |
| this_prefetch->cookie_copy_status_ = |
| SinglePrefetch::CookieCopyStatus::kCompleted; |
| |
| if (this_prefetch->cookie_copy_start_time_.has_value() && |
| this_prefetch->cookie_read_end_and_write_start_time_.has_value()) { |
| RecordCookieCopyTimes( |
| this_prefetch->cookie_copy_start_time_.value(), |
| this_prefetch->cookie_read_end_and_write_start_time_.value(), |
| base::TimeTicks::Now()); |
| } |
| |
| if (this_prefetch->on_cookie_copy_complete_callback_) { |
| std::move(this_prefetch->on_cookie_copy_complete_callback_).Run(); |
| } |
| } |
| |
| void PrefetchContainer::OnInterceptorCheckCookieCopy() { |
| if (!redirect_chain_[index_redirect_chain_to_serve_] |
| ->cookie_copy_start_time_) { |
| return; |
| } |
| |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "PrefetchProxy.AfterClick.Mainframe.CookieCopyStartToInterceptorCheck", |
| base::TimeTicks::Now() - redirect_chain_[index_redirect_chain_to_serve_] |
| ->cookie_copy_start_time_.value(), |
| base::TimeDelta(), base::Seconds(5), 50); |
| } |
| |
| void PrefetchContainer::SetOnCookieCopyCompleteCallback( |
| base::OnceClosure callback) { |
| DCHECK(IsIsolatedCookieCopyInProgress()); |
| |
| redirect_chain_[index_redirect_chain_to_serve_] |
| ->on_cookie_copy_complete_callback_ = std::move(callback); |
| } |
| |
| void PrefetchContainer::TakeStreamingURLLoader( |
| std::unique_ptr<PrefetchStreamingURLLoader> streaming_loader) { |
| DCHECK(!streaming_loader_); |
| streaming_loader_ = std::move(streaming_loader); |
| } |
| |
| std::unique_ptr<PrefetchStreamingURLLoader> |
| PrefetchContainer::ReleaseStreamingLoader() { |
| DCHECK(streaming_loader_); |
| return std::move(streaming_loader_); |
| } |
| |
| void PrefetchContainer::ResetStreamingLoader() { |
| DCHECK(streaming_loader_); |
| |
| // The streaming URL loader can be deleted in one of its callbacks, so instead |
| // of deleting it immediately, it is made self owned and then deletes itself. |
| PrefetchStreamingURLLoader* raw_streaming_loader = streaming_loader_.get(); |
| raw_streaming_loader->MakeSelfOwnedAndDeleteSoon( |
| std::move(streaming_loader_)); |
| } |
| |
| void PrefetchContainer::OnPrefetchProbeResult( |
| PrefetchProbeResult probe_result) { |
| probe_result_ = probe_result; |
| |
| switch (probe_result) { |
| case PrefetchProbeResult::kNoProbing: |
| case PrefetchProbeResult::kDNSProbeSuccess: |
| case PrefetchProbeResult::kTLSProbeSuccess: |
| // Wait to update the prefetch status until the probe for the final |
| // redirect hop is a success. |
| if (index_redirect_chain_to_serve_ == redirect_chain_.size() - 1) { |
| SetPrefetchStatus(PrefetchStatus::kPrefetchResponseUsed); |
| } |
| break; |
| case PrefetchProbeResult::kDNSProbeFailure: |
| case PrefetchProbeResult::kTLSProbeFailure: |
| prefetch_status_ = PrefetchStatus::kPrefetchNotUsedProbeFailed; |
| break; |
| default: |
| NOTIMPLEMENTED(); |
| } |
| } |
| |
| void PrefetchContainer::OnPrefetchedResponseHeadReceived() { |
| if (prefetch_document_manager_) { |
| prefetch_document_manager_->OnPrefetchedHeadReceived(GetURL()); |
| } |
| } |
| |
| void PrefetchContainer::OnPrefetchComplete() { |
| if (!streaming_loader_) { |
| return; |
| } |
| |
| UpdatePrefetchRequestMetrics(streaming_loader_->GetCompletionStatus(), |
| streaming_loader_->GetHead()); |
| UpdateServingPageMetrics(); |
| } |
| |
| void PrefetchContainer::UpdatePrefetchRequestMetrics( |
| const absl::optional<network::URLLoaderCompletionStatus>& completion_status, |
| const network::mojom::URLResponseHead* head) { |
| if (completion_status) { |
| prefetch_response_sizes_ = { |
| .encoded_data_length = completion_status->encoded_data_length, |
| .encoded_body_length = completion_status->encoded_body_length, |
| .decoded_body_length = completion_status->decoded_body_length, |
| }; |
| } |
| |
| if (head) |
| header_latency_ = |
| head->load_timing.receive_headers_end - head->load_timing.request_start; |
| |
| if (completion_status && head) |
| fetch_duration_ = |
| completion_status->completion_time - head->load_timing.request_start; |
| } |
| |
| bool PrefetchContainer::ShouldBlockUntilHeadReceived() const { |
| // Can only block until head if the request has been started using a streaming |
| // URL loader and head hasn't been received yet. |
| if (!streaming_loader_ || streaming_loader_->GetHead() || |
| streaming_loader_->Failed()) { |
| return false; |
| } |
| return PrefetchShouldBlockUntilHead(prefetch_type_.GetEagerness()); |
| } |
| |
| bool PrefetchContainer::IsPrefetchServable( |
| base::TimeDelta cacheable_duration) const { |
| // Whether or not the response (either full or partial) from the streaming URL |
| // loader is servable. |
| return streaming_loader_ && streaming_loader_->Servable(cacheable_duration); |
| } |
| |
| bool PrefetchContainer::DoesCurrentURLToServeMatch(const GURL& url) const { |
| DCHECK(index_redirect_chain_to_serve_ >= 1 && |
| index_redirect_chain_to_serve_ < redirect_chain_.size()); |
| return redirect_chain_[index_redirect_chain_to_serve_]->url_ == url || |
| IsMatchingNoVarySearchUrl( |
| redirect_chain_[index_redirect_chain_to_serve_]->url_, url); |
| } |
| |
| const GURL& PrefetchContainer::GetCurrentURLToServe() const { |
| DCHECK(index_redirect_chain_to_serve_ >= 0 && |
| index_redirect_chain_to_serve_ < redirect_chain_.size()); |
| return redirect_chain_[index_redirect_chain_to_serve_]->url_; |
| } |
| |
| const network::mojom::URLResponseHead* PrefetchContainer::GetHead() { |
| return streaming_loader_ ? streaming_loader_->GetHead() : nullptr; |
| } |
| |
| void PrefetchContainer::SetServingPageMetrics( |
| base::WeakPtr<PrefetchServingPageMetricsContainer> |
| serving_page_metrics_container) { |
| serving_page_metrics_container_ = serving_page_metrics_container; |
| } |
| |
| void PrefetchContainer::UpdateServingPageMetrics() { |
| if (!serving_page_metrics_container_) { |
| return; |
| } |
| |
| serving_page_metrics_container_->SetRequiredPrivatePrefetchProxy( |
| GetPrefetchType().IsProxyRequired()); |
| serving_page_metrics_container_->SetPrefetchHeaderLatency( |
| GetPrefetchHeaderLatency()); |
| if (HasPrefetchStatus()) { |
| serving_page_metrics_container_->SetPrefetchStatus(GetPrefetchStatus()); |
| } |
| } |
| |
| void PrefetchContainer::SimulateAttemptAtInterceptorForTest() { |
| if (attempt_) { |
| attempt_->SetEligibility(PreloadingEligibility::kEligible); |
| attempt_->SetHoldbackStatus(PreloadingHoldbackStatus::kAllowed); |
| } |
| SetPrefetchStatus(PrefetchStatus::kPrefetchAllowed); |
| SetPrefetchStatus(PrefetchStatus::kPrefetchSuccessful); |
| } |
| |
| PrefetchContainer::SinglePrefetch* PrefetchContainer::GetSinglePrefetch( |
| const GURL& url) const { |
| for (auto itr = redirect_chain_.rbegin(); itr != redirect_chain_.rend(); |
| itr++) { |
| GURL single_prefetch_url = (*itr)->url_; |
| if (single_prefetch_url == url || |
| IsMatchingNoVarySearchUrl(single_prefetch_url, url)) { |
| return itr->get(); |
| } |
| } |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| bool PrefetchContainer::IsMatchingNoVarySearchUrl( |
| const GURL& internal_url, |
| const GURL& external_url) const { |
| if (!no_vary_search_helper_) { |
| return false; |
| } |
| |
| absl::optional<GURL> no_vary_search_match_url = |
| no_vary_search_helper_->MatchUrl(external_url); |
| if (!no_vary_search_match_url) { |
| return false; |
| } |
| |
| return internal_url == no_vary_search_match_url.value(); |
| } |
| |
| void PrefetchContainer::OnGetPrefetchToServe(bool blocked_until_head) { |
| if (blocked_until_head) { |
| blocked_until_head_start_time_ = base::TimeTicks::Now(); |
| } |
| |
| RecordWasBlockedUntilHeadWhenServingHistogram(prefetch_type_.GetEagerness(), |
| blocked_until_head); |
| } |
| |
| void PrefetchContainer::OnReturnPrefetchToServe(bool served) { |
| if (served) { |
| navigated_to_ = true; |
| } |
| |
| if (blocked_until_head_start_time_.has_value()) { |
| RecordBlockUntilHeadDurationHistogram( |
| prefetch_type_.GetEagerness(), |
| base::TimeTicks::Now() - blocked_until_head_start_time_.value(), |
| served); |
| } |
| } |
| |
| PrefetchContainer::SinglePrefetch::SinglePrefetch(const GURL& url) |
| : url_(url) {} |
| |
| PrefetchContainer::SinglePrefetch::~SinglePrefetch() = default; |
| |
| } // namespace content |