blob: 547d969b39062833a80bf9f56a07ccd6e1eee542 [file] [log] [blame]
// 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 "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "components/variations/net/variations_http_headers.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/devtools/network_service_devtools_observer.h"
#include "content/browser/loader/navigation_url_loader_impl.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_features.h"
#include "content/browser/preloading/prefetch/prefetch_match_resolver.h"
#include "content/browser/preloading/prefetch/prefetch_network_context.h"
#include "content/browser/preloading/prefetch/prefetch_params.h"
#include "content/browser/preloading/prefetch/prefetch_request.h"
#include "content/browser/preloading/prefetch/prefetch_response_reader.h"
#include "content/browser/preloading/prefetch/prefetch_servable_state.h"
#include "content/browser/preloading/prefetch/prefetch_serving_handle.h"
#include "content/browser/preloading/prefetch/prefetch_serving_page_metrics_container.h"
#include "content/browser/preloading/prefetch/prefetch_single_redirect_hop.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/preloading_attempt_impl.h"
#include "content/browser/preloading/preloading_trigger_type_impl.h"
#include "content/browser/preloading/prerender/prerender_features.h"
#include "content/browser/preloading/proxy_lookup_client_impl.h"
#include "content/browser/preloading/speculation_rules/speculation_rules_tags.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/client_hints.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/prefetch_request_status_listener.h"
#include "content/public/browser/preloading.h"
#include "content/public/browser/web_contents.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "net/url_request/redirect_util.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/network/public/cpp/client_hints.h"
#include "services/network/public/cpp/devtools_observer_util.h"
#include "third_party/blink/public/common/client_hints/client_hints.h"
#include "third_party/blink/public/common/navigation/preloading_headers.h"
#include "url/gurl.h"
namespace content {
namespace {
PrefetchStatus PrefetchStatusFromIneligibleReason(
PreloadingEligibility eligibility) {
switch (eligibility) {
case PreloadingEligibility::kBatterySaverEnabled:
return PrefetchStatus::kPrefetchIneligibleBatterySaverEnabled;
case PreloadingEligibility::kDataSaverEnabled:
return PrefetchStatus::kPrefetchIneligibleDataSaverEnabled;
case PreloadingEligibility::kExistingProxy:
return PrefetchStatus::kPrefetchIneligibleExistingProxy;
case PreloadingEligibility::kHostIsNonUnique:
return PrefetchStatus::kPrefetchIneligibleHostIsNonUnique;
case PreloadingEligibility::kNonDefaultStoragePartition:
return PrefetchStatus::kPrefetchIneligibleNonDefaultStoragePartition;
case PreloadingEligibility::kPrefetchProxyNotAvailable:
return PrefetchStatus::kPrefetchIneligiblePrefetchProxyNotAvailable;
case PreloadingEligibility::kPreloadingDisabled:
return PrefetchStatus::kPrefetchIneligiblePreloadingDisabled;
case PreloadingEligibility::kRetryAfter:
return PrefetchStatus::kPrefetchIneligibleRetryAfter;
case PreloadingEligibility::kSameSiteCrossOriginPrefetchRequiredProxy:
return PrefetchStatus::
kPrefetchIneligibleSameSiteCrossOriginPrefetchRequiredProxy;
case PreloadingEligibility::kSchemeIsNotHttps:
return PrefetchStatus::kPrefetchIneligibleSchemeIsNotHttps;
case PreloadingEligibility::kUserHasCookies:
return PrefetchStatus::kPrefetchIneligibleUserHasCookies;
case PreloadingEligibility::kUserHasServiceWorker:
return PrefetchStatus::kPrefetchIneligibleUserHasServiceWorker;
case PreloadingEligibility::kUserHasServiceWorkerNoFetchHandler:
return PrefetchStatus::
kPrefetchIneligibleUserHasServiceWorkerNoFetchHandler;
case PreloadingEligibility::kRedirectFromServiceWorker:
return PrefetchStatus::kPrefetchIneligibleRedirectFromServiceWorker;
case PreloadingEligibility::kRedirectToServiceWorker:
return PrefetchStatus::kPrefetchIneligibleRedirectToServiceWorker;
case PreloadingEligibility::kEligible:
default:
// Other ineligible cases are not used in `PrefetchService`.
NOTREACHED();
}
}
std::optional<PreloadingTriggeringOutcome> TriggeringOutcomeFromStatus(
PrefetchStatus prefetch_status) {
switch (prefetch_status) {
case PrefetchStatus::kPrefetchNotFinishedInTime:
return PreloadingTriggeringOutcome::kRunning;
case PrefetchStatus::kPrefetchSuccessful:
return PreloadingTriggeringOutcome::kReady;
case PrefetchStatus::kPrefetchResponseUsed:
return PreloadingTriggeringOutcome::kSuccess;
case PrefetchStatus::kPrefetchIsPrivacyDecoy:
case PrefetchStatus::kPrefetchIsStale:
case PrefetchStatus::kPrefetchFailedNetError:
case PrefetchStatus::kPrefetchFailedNon2XX:
case PrefetchStatus::kPrefetchFailedMIMENotSupported:
case PrefetchStatus::kPrefetchFailedInvalidRedirect:
case PrefetchStatus::kPrefetchFailedIneligibleRedirect:
case PrefetchStatus::kPrefetchEvictedAfterCandidateRemoved:
case PrefetchStatus::kPrefetchEvictedForNewerPrefetch:
case PrefetchStatus::kPrefetchIneligibleUserHasServiceWorker:
case PrefetchStatus::kPrefetchIneligibleUserHasServiceWorkerNoFetchHandler:
case PrefetchStatus::kPrefetchIneligibleRedirectFromServiceWorker:
case PrefetchStatus::kPrefetchIneligibleRedirectToServiceWorker:
case PrefetchStatus::kPrefetchIneligibleSchemeIsNotHttps:
case PrefetchStatus::kPrefetchIneligibleNonDefaultStoragePartition:
case PrefetchStatus::kPrefetchIneligibleHostIsNonUnique:
case PrefetchStatus::kPrefetchIneligibleDataSaverEnabled:
case PrefetchStatus::kPrefetchIneligibleBatterySaverEnabled:
case PrefetchStatus::kPrefetchIneligiblePreloadingDisabled:
case PrefetchStatus::kPrefetchIneligibleExistingProxy:
case PrefetchStatus::kPrefetchIneligibleUserHasCookies:
case PrefetchStatus::kPrefetchIneligibleRetryAfter:
case PrefetchStatus::kPrefetchNotUsedCookiesChanged:
case PrefetchStatus::kPrefetchNotUsedProbeFailed:
case PrefetchStatus::
kPrefetchIneligibleSameSiteCrossOriginPrefetchRequiredProxy:
case PrefetchStatus::kPrefetchIneligiblePrefetchProxyNotAvailable:
case PrefetchStatus::kPrefetchEvictedAfterBrowsingDataRemoved:
return PreloadingTriggeringOutcome::kFailure;
case PrefetchStatus::kPrefetchHeldback:
case PrefetchStatus::kPrefetchNotStarted:
return std::nullopt;
}
return std::nullopt;
}
// Returns true if SetPrefetchStatus(|status|) can be called after a prefetch
// has already been marked as failed. We ignore such status updates
// as they may end up overwriting the initial failure reason.
bool StatusUpdateIsPossibleAfterFailure(PrefetchStatus status) {
switch (status) {
case PrefetchStatus::kPrefetchEvictedAfterCandidateRemoved:
case PrefetchStatus::kPrefetchIsStale:
case PrefetchStatus::kPrefetchEvictedForNewerPrefetch:
case PrefetchStatus::kPrefetchEvictedAfterBrowsingDataRemoved: {
CHECK(TriggeringOutcomeFromStatus(status) ==
PreloadingTriggeringOutcome::kFailure);
return true;
}
case PrefetchStatus::kPrefetchNotFinishedInTime:
case PrefetchStatus::kPrefetchSuccessful:
case PrefetchStatus::kPrefetchResponseUsed:
case PrefetchStatus::kPrefetchIsPrivacyDecoy:
case PrefetchStatus::kPrefetchFailedNetError:
case PrefetchStatus::kPrefetchFailedNon2XX:
case PrefetchStatus::kPrefetchFailedMIMENotSupported:
case PrefetchStatus::kPrefetchFailedInvalidRedirect:
case PrefetchStatus::kPrefetchFailedIneligibleRedirect:
case PrefetchStatus::kPrefetchIneligibleUserHasServiceWorker:
case PrefetchStatus::kPrefetchIneligibleUserHasServiceWorkerNoFetchHandler:
case PrefetchStatus::kPrefetchIneligibleRedirectFromServiceWorker:
case PrefetchStatus::kPrefetchIneligibleRedirectToServiceWorker:
case PrefetchStatus::kPrefetchIneligibleSchemeIsNotHttps:
case PrefetchStatus::kPrefetchIneligibleNonDefaultStoragePartition:
case PrefetchStatus::kPrefetchIneligibleHostIsNonUnique:
case PrefetchStatus::kPrefetchIneligibleDataSaverEnabled:
case PrefetchStatus::kPrefetchIneligibleBatterySaverEnabled:
case PrefetchStatus::kPrefetchIneligiblePreloadingDisabled:
case PrefetchStatus::kPrefetchIneligibleExistingProxy:
case PrefetchStatus::kPrefetchIneligibleUserHasCookies:
case PrefetchStatus::kPrefetchIneligibleRetryAfter:
case PrefetchStatus::kPrefetchNotUsedCookiesChanged:
case PrefetchStatus::kPrefetchNotUsedProbeFailed:
case PrefetchStatus::
kPrefetchIneligibleSameSiteCrossOriginPrefetchRequiredProxy:
case PrefetchStatus::kPrefetchHeldback:
case PrefetchStatus::kPrefetchNotStarted:
case PrefetchStatus::kPrefetchIneligiblePrefetchProxyNotAvailable:
return false;
}
}
void RecordPrefetchProxyPrefetchMainframeNetError(int net_error) {
base::UmaHistogramSparse("PrefetchProxy.Prefetch.Mainframe.NetError",
std::abs(net_error));
}
void RecordPrefetchProxyPrefetchMainframeBodyLength(int64_t body_length) {
UMA_HISTOGRAM_COUNTS_10M("PrefetchProxy.Prefetch.Mainframe.BodyLength",
body_length);
}
bool CalculateIsLikelyAheadOfPrerender(
const PreloadPipelineInfoImpl& preload_pipeline_info) {
if (!features::UsePrefetchPrerenderIntegration()) {
return false;
}
switch (preload_pipeline_info.planned_max_preloading_type()) {
case PreloadingType::kPrefetch:
return false;
case PreloadingType::kPrerender:
case PreloadingType::kPrerenderUntilScript:
return true;
case PreloadingType::kUnspecified:
case PreloadingType::kPreconnect:
case PreloadingType::kNoStatePrefetch:
case PreloadingType::kLinkPreview:
NOTREACHED();
}
}
PrefetchContainer::PrefetchResponseCompletedCallbackForTesting&
GetPrefetchResponseCompletedCallbackForTesting() {
static base::NoDestructor<
PrefetchContainer::PrefetchResponseCompletedCallbackForTesting>
prefetch_response_completed_callback_for_testing;
return *prefetch_response_completed_callback_for_testing;
}
} // namespace
PrefetchContainer::PrefetchContainer(
RenderFrameHostImpl& referring_render_frame_host,
const blink::DocumentToken& referring_document_token,
const GURL& url,
const PrefetchType& prefetch_type,
const blink::mojom::Referrer& referrer,
std::optional<SpeculationRulesTags> speculation_rules_tags,
std::optional<net::HttpNoVarySearchData> no_vary_search_hint,
std::optional<PrefetchPriority> priority,
base::WeakPtr<PrefetchDocumentManager> prefetch_document_manager,
scoped_refptr<PreloadPipelineInfo> preload_pipeline_info,
base::WeakPtr<PreloadingAttempt> attempt)
: PrefetchContainer(std::make_unique<PrefetchRequest>(
prefetch_type,
PrefetchKey(referring_document_token, url),
std::move(no_vary_search_hint),
std::move(priority),
std::move(preload_pipeline_info),
std::move(attempt),
WebContentsImpl::FromRenderFrameHostImpl(&referring_render_frame_host)
->GetOrCreateWebPreferences()
.javascript_enabled,
referrer,
referring_render_frame_host.GetLastCommittedOrigin(),
referring_render_frame_host.GetBrowserContext()->GetWeakPtr(),
std::move(speculation_rules_tags),
/*Must be empty: additional_headers=*/net::HttpRequestHeaders(),
PrefetchContainerDefaultTtlInPrefetchService(),
/*holdback_status_override=*/std::nullopt,
/*should_append_variations_header=*/true,
/*should_disable_block_until_head_timeout=*/false,
PrefetchRendererInitiatorInfo(
referring_render_frame_host,
std::move(prefetch_document_manager)))) {}
PrefetchContainer::PrefetchContainer(
WebContents& referring_web_contents,
const GURL& url,
const PrefetchType& prefetch_type,
const std::string& embedder_histogram_suffix,
const blink::mojom::Referrer& referrer,
const std::optional<url::Origin>& referring_origin,
std::optional<net::HttpNoVarySearchData> no_vary_search_hint,
std::optional<PrefetchPriority> priority,
scoped_refptr<PreloadPipelineInfo> preload_pipeline_info,
base::WeakPtr<PreloadingAttempt> attempt,
std::optional<PreloadingHoldbackStatus> holdback_status_override,
std::optional<base::TimeDelta> ttl)
: PrefetchContainer(std::make_unique<PrefetchRequest>(
prefetch_type,
PrefetchKey(std::optional<blink::DocumentToken>(std::nullopt), url),
std::move(no_vary_search_hint),
std::move(priority),
std::move(preload_pipeline_info),
std::move(attempt),
referring_web_contents.GetOrCreateWebPreferences().javascript_enabled,
referrer,
referring_origin,
referring_web_contents.GetBrowserContext()->GetWeakPtr(),
/*speculation_rules_tags=*/std::nullopt,
/*Must be empty: additional_headers=*/net::HttpRequestHeaders(),
ttl.has_value() ? ttl.value()
: PrefetchContainerDefaultTtlInPrefetchService(),
std::move(holdback_status_override),
/*should_append_variations_header=*/true,
/*should_disable_block_until_head_timeout=*/false,
PrefetchBrowserInitiatorInfo(embedder_histogram_suffix,
/*request_status_listener=*/nullptr))) {}
PrefetchContainer::PrefetchContainer(
BrowserContext* browser_context,
const GURL& url,
const PrefetchType& prefetch_type,
const std::string& embedder_histogram_suffix,
const blink::mojom::Referrer& referrer,
bool javascript_enabled,
const std::optional<url::Origin>& referring_origin,
std::optional<net::HttpNoVarySearchData> no_vary_search_hint,
std::optional<PrefetchPriority> priority,
base::WeakPtr<PreloadingAttempt> attempt,
const net::HttpRequestHeaders& additional_headers,
std::unique_ptr<PrefetchRequestStatusListener> request_status_listener,
base::TimeDelta ttl,
bool should_append_variations_header,
bool should_disable_block_until_head_timeout)
: PrefetchContainer(std::make_unique<PrefetchRequest>(
prefetch_type,
PrefetchKey(std::optional<blink::DocumentToken>(std::nullopt), url),
std::move(no_vary_search_hint),
std::move(priority),
PreloadPipelineInfo::Create(
/*planned_max_preloading_type=*/PreloadingType::kPrefetch),
std::move(attempt),
javascript_enabled,
referrer,
referring_origin,
browser_context->GetWeakPtr(),
/*speculation_rules_tags=*/std::nullopt,
additional_headers,
ttl,
/*holdback_status_override=*/std::nullopt,
should_append_variations_header,
should_disable_block_until_head_timeout,
PrefetchBrowserInitiatorInfo(embedder_histogram_suffix,
std::move(request_status_listener)))) {}
PrefetchContainer::PrefetchContainer(std::unique_ptr<PrefetchRequest> request)
: request_(std::move(request)),
referrer_(request_->initial_referrer()),
request_id_(base::UnguessableToken::Create().ToString()) {
CHECK(request_);
is_likely_ahead_of_prerender_ =
CalculateIsLikelyAheadOfPrerender(request_->preload_pipeline_info());
redirect_chain_.push_back(std::make_unique<PrefetchSingleRedirectHop>(
*this, GetURL(), IsCrossSiteRequest(url::Origin::Create(GetURL()))));
// Disallow prefetching ServiceWorker-controlled responses for isolated
// network contexts.
if (!features::IsPrefetchServiceWorkerEnabled(request_->browser_context()) ||
IsIsolatedNetworkContextRequiredForCurrentPrefetch()) {
service_worker_state_ = PrefetchServiceWorkerState::kDisallowed;
}
}
PrefetchContainer::~PrefetchContainer() {
is_in_dtor_ = true;
// Ideally, this method should be called just before dtor.
// https://chromium-review.googlesource.com/c/chromium/src/+/5657659/comments/0cfb14c0_3050963e
//
// TODO(crbug.com/356314759): Do it.
OnWillBeDestroyed();
CancelStreamingURLLoaderIfNotServing();
MaybeRecordPrefetchStatusToUMA(
prefetch_status_.value_or(PrefetchStatus::kPrefetchNotStarted));
RecordPrefetchDurationHistogram();
RecordPrefetchContainerServedCountHistogram();
ukm::SourceId ukm_source_id = ukm::kInvalidSourceId;
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
ukm_source_id = renderer_initiator_info->ukm_source_id();
}
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(served_count_ > 0);
if (GetNonRedirectResponseReader()) {
GetNonRedirectResponseReader()->RecordOnPrefetchContainerDestroyed(
base::PassKey<PrefetchContainer>(), builder);
}
if (probe_result_) {
builder.SetISPFilteringStatus(static_cast<int>(probe_result_.value()));
}
// TODO(crbug.com/40215782): Get the navigation start time and set the
// NavigationStartToFetchStartMs field of the PrefetchProxy.PrefetchedResource
// UKM event.
builder.Record(ukm::UkmRecorder::Get());
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
if (renderer_initiator_info->prefetch_document_manager()) {
renderer_initiator_info->prefetch_document_manager()
->PrefetchWillBeDestroyed(this);
}
}
}
void PrefetchContainer::OnWillBeDestroyed() {
for (auto& observer : observers_) {
observer.OnWillBeDestroyed(*this);
}
}
PrefetchServingHandle PrefetchContainer::CreateServingHandle() {
return PrefetchServingHandle(GetWeakPtr(), 0);
}
const std::vector<std::unique_ptr<PrefetchSingleRedirectHop>>&
PrefetchContainer::redirect_chain(base::PassKey<PrefetchServingHandle>) const {
return redirect_chain_;
}
void PrefetchContainer::SetProbeResult(base::PassKey<PrefetchServingHandle>,
PrefetchProbeResult probe_result) {
probe_result_ = probe_result;
}
std::optional<PreloadingTriggeringOutcome>
PrefetchContainer::TriggeringOutcomeFromStatusForServingHandle(
base::PassKey<PrefetchServingHandle>,
PrefetchStatus prefetch_status) {
return TriggeringOutcomeFromStatus(prefetch_status);
}
// Please follow go/preloading-dashboard-updates if a new outcome enum or a
// failure reason enum is added.
void PrefetchContainer::SetTriggeringOutcomeAndFailureReasonFromStatus(
PrefetchStatus new_prefetch_status) {
std::optional<PrefetchStatus> old_prefetch_status = prefetch_status_;
if (old_prefetch_status &&
old_prefetch_status.value() == PrefetchStatus::kPrefetchResponseUsed) {
// Skip this update if the triggering outcome has already been updated
// to kSuccess.
return;
}
if (old_prefetch_status &&
(TriggeringOutcomeFromStatus(old_prefetch_status.value()) ==
PreloadingTriggeringOutcome::kFailure)) {
if (StatusUpdateIsPossibleAfterFailure(new_prefetch_status)) {
// Note that `StatusUpdateIsPossibleAfterFailure()` implies that
// the new status is a failure.
CHECK(TriggeringOutcomeFromStatus(new_prefetch_status) ==
PreloadingTriggeringOutcome::kFailure);
// Skip this update since if the triggering outcome has already been
// updated to kFailure, we don't need to overwrite it.
return;
} else {
SCOPED_CRASH_KEY_NUMBER("PrefetchContainer", "prefetch_status_from",
static_cast<int>(old_prefetch_status.value()));
SCOPED_CRASH_KEY_NUMBER("PrefetchContainer", "prefetch_status_to",
static_cast<int>(new_prefetch_status));
NOTREACHED()
<< "PrefetchStatus illegal transition: (old_prefetch_status, "
"new_prefetch_status) = ("
<< static_cast<int>(old_prefetch_status.value()) << ", "
<< static_cast<int>(new_prefetch_status) << ")";
}
}
// We record the prefetch status to UMA if it's a failure, or if the prefetch
// response is being used. For other statuses, there may be more updates in
// the future, so we only record them in the destructor.
// Note: The prefetch may have an updated failure status in the future
// (for example: if the triggering speculation candidate for a failed prefetch
// is removed), but the original failure is more pertinent for metrics
// purposes.
if (TriggeringOutcomeFromStatus(new_prefetch_status) ==
PreloadingTriggeringOutcome::kFailure ||
new_prefetch_status == PrefetchStatus::kPrefetchResponseUsed) {
MaybeRecordPrefetchStatusToUMA(new_prefetch_status);
}
if (request().attempt()) {
switch (new_prefetch_status) {
case PrefetchStatus::kPrefetchNotFinishedInTime:
request().attempt()->SetTriggeringOutcome(
PreloadingTriggeringOutcome::kRunning);
break;
case PrefetchStatus::kPrefetchSuccessful:
// A successful prefetch means the response is ready to be used for the
// next navigation.
request().attempt()->SetTriggeringOutcome(
PreloadingTriggeringOutcome::kReady);
break;
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.
request().attempt()->SetTriggeringOutcome(
PreloadingTriggeringOutcome::kReady);
}
request().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:
case PrefetchStatus::kPrefetchNotUsedProbeFailed:
case PrefetchStatus::kPrefetchNotUsedCookiesChanged:
// TODO(adithyas): This would report 'eviction' as a failure even though
// the initial prefetch succeeded, consider introducing a different
// PreloadingTriggerOutcome for eviction.
case PrefetchStatus::kPrefetchEvictedAfterCandidateRemoved:
case PrefetchStatus::kPrefetchEvictedForNewerPrefetch:
case PrefetchStatus::kPrefetchIsStale:
case PrefetchStatus::kPrefetchEvictedAfterBrowsingDataRemoved:
request().attempt()->SetFailureReason(
ToPreloadingFailureReason(new_prefetch_status));
break;
case PrefetchStatus::kPrefetchHeldback:
case PrefetchStatus::kPrefetchNotStarted:
// `kPrefetchNotStarted` is set in
// `PrefetchService::OnGotEligibilityForNonRedirect()` 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::kPrefetchIneligibleUserHasServiceWorker:
case PrefetchStatus::
kPrefetchIneligibleUserHasServiceWorkerNoFetchHandler:
case PrefetchStatus::kPrefetchIneligibleRedirectFromServiceWorker:
case PrefetchStatus::kPrefetchIneligibleRedirectToServiceWorker:
case PrefetchStatus::kPrefetchIneligibleSchemeIsNotHttps:
case PrefetchStatus::kPrefetchIneligibleNonDefaultStoragePartition:
case PrefetchStatus::kPrefetchIneligibleHostIsNonUnique:
case PrefetchStatus::kPrefetchIneligibleDataSaverEnabled:
case PrefetchStatus::kPrefetchIneligibleBatterySaverEnabled:
case PrefetchStatus::kPrefetchIneligiblePreloadingDisabled:
case PrefetchStatus::kPrefetchIneligibleExistingProxy:
case PrefetchStatus::kPrefetchIneligibleUserHasCookies:
case PrefetchStatus::kPrefetchIneligibleRetryAfter:
case PrefetchStatus::kPrefetchIneligiblePrefetchProxyNotAvailable:
case PrefetchStatus::
kPrefetchIneligibleSameSiteCrossOriginPrefetchRequiredProxy:
NOTIMPLEMENTED();
}
}
}
void PrefetchContainer::SetPrefetchStatusWithoutUpdatingTriggeringOutcome(
PrefetchStatus prefetch_status) {
prefetch_status_ = prefetch_status;
request().preload_pipeline_info().SetPrefetchStatus(prefetch_status);
for (auto& preload_pipeline_info : inherited_preload_pipeline_infos_) {
preload_pipeline_info->SetPrefetchStatus(prefetch_status);
}
// Currently DevTools only supports when the prefetch is initiated by
// renderer.
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
std::optional<PreloadingTriggeringOutcome> preloading_trigger_outcome =
TriggeringOutcomeFromStatus(prefetch_status);
if (renderer_initiator_info->devtools_navigation_token().has_value() &&
preloading_trigger_outcome.has_value()) {
devtools_instrumentation::DidUpdatePrefetchStatus(
FrameTreeNode::From(renderer_initiator_info->GetRenderFrameHost()),
renderer_initiator_info->devtools_navigation_token().value(),
GetURL(), request().preload_pipeline_info().id(),
preloading_trigger_outcome.value(), prefetch_status, RequestId());
}
}
}
void PrefetchContainer::SetPrefetchStatus(PrefetchStatus prefetch_status) {
// The concept of `PreloadingAttempt`'s `PreloadingTriggeringOutcome` is to
// record the outcomes of started triggers. Therefore, this should
// only be called once prefetching has actually started, and not for
// ineligible or eligibled but not started triggers (e.g., holdback triggers,
// triggers waiting on a queue).
switch (GetLoadState()) {
case LoadState::kStarted:
case LoadState::kDeterminedHead:
case LoadState::kCompletedOrFailed:
SetTriggeringOutcomeAndFailureReasonFromStatus(prefetch_status);
break;
case LoadState::kNotStarted:
case LoadState::kEligible:
case LoadState::kFailedIneligible:
case LoadState::kFailedHeldback:
break;
}
SetPrefetchStatusWithoutUpdatingTriggeringOutcome(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::GetOrCreateNetworkContextForCurrentPrefetch() {
bool is_isolated_network_context_required =
IsIsolatedNetworkContextRequiredForCurrentPrefetch();
PrefetchNetworkContext* network_context =
GetNetworkContext(is_isolated_network_context_required);
if (network_context) {
return network_context;
}
GlobalRenderFrameHostId referring_render_frame_host_id;
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
referring_render_frame_host_id =
renderer_initiator_info->GetRenderFrameHostId();
}
auto owned_network_context = std::make_unique<PrefetchNetworkContext>(
is_isolated_network_context_required, request().prefetch_type(),
referring_render_frame_host_id, request().referring_origin());
network_context = owned_network_context.get();
network_contexts_.emplace(is_isolated_network_context_required,
std::move(owned_network_context));
return network_context;
}
PrefetchNetworkContext* PrefetchContainer::GetNetworkContext(
bool is_isolated_network_context_required) const {
const auto network_context_itr =
network_contexts_.find(is_isolated_network_context_required);
if (network_context_itr == network_contexts_.end()) {
return nullptr;
}
return network_context_itr->second.get();
}
void PrefetchContainer::CloseIdleConnections() {
for (const auto& network_context_itr : network_contexts_) {
CHECK(network_context_itr.second);
network_context_itr.second->CloseIdleConnections();
}
}
void PrefetchContainer::SetLoadState(LoadState new_load_state) {
switch (new_load_state) {
case LoadState::kNotStarted:
NOTREACHED();
case LoadState::kEligible:
case LoadState::kFailedIneligible:
CHECK_EQ(load_state_, LoadState::kNotStarted);
break;
case LoadState::kStarted:
case LoadState::kFailedHeldback:
CHECK_EQ(load_state_, LoadState::kEligible);
break;
case LoadState::kDeterminedHead:
CHECK_EQ(load_state_, LoadState::kStarted);
break;
case LoadState::kCompletedOrFailed:
CHECK_EQ(load_state_, LoadState::kDeterminedHead);
break;
}
DVLOG(1) << (*this) << " LoadState " << load_state_ << " -> "
<< new_load_state;
load_state_ = new_load_state;
}
PrefetchContainer::LoadState PrefetchContainer::GetLoadState() const {
return load_state_;
}
void PrefetchContainer::OnAddedToPrefetchService() {
time_added_to_prefetch_service_ = base::TimeTicks::Now();
}
void PrefetchContainer::OnEligibilityCheckComplete(
PreloadingEligibility eligibility) {
request().preload_pipeline_info().SetPrefetchEligibility(eligibility);
for (auto& preload_pipeline_info : inherited_preload_pipeline_infos_) {
preload_pipeline_info->SetPrefetchEligibility(eligibility);
}
bool is_eligible = eligibility == PreloadingEligibility::kEligible;
if (redirect_chain_.size() == 1) {
// This case is for just the URL that was originally requested to be
// prefetched.
if (is_eligible) {
SetLoadState(LoadState::kEligible);
} else {
SetLoadState(LoadState::kFailedIneligible);
PrefetchStatus new_prefetch_status =
PrefetchStatusFromIneligibleReason(eligibility);
MaybeRecordPrefetchStatusToUMA(new_prefetch_status);
SetPrefetchStatusWithoutUpdatingTriggeringOutcome(new_prefetch_status);
OnInitialPrefetchFailedIneligible(eligibility);
}
if (request().attempt()) {
// Please follow go/preloading-dashboard-updates if a new eligibility is
// added.
request().attempt()->SetEligibility(eligibility);
}
time_initial_eligibility_got_ = base::TimeTicks::Now();
// Recording an eligiblity for PrefetchReferringPageMetrics.
// TODO(crbug.com/40946257): Current code doesn't support
// PrefetchReferringPageMetrics when the prefetch is initiated by browser.
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
if (renderer_initiator_info->prefetch_document_manager()) {
renderer_initiator_info->prefetch_document_manager()
->OnEligibilityCheckComplete(is_eligible);
}
}
for (auto& observer : observers_) {
observer.OnGotInitialEligibility(*this, eligibility);
}
} else {
// This case is for any URLs from redirects.
if (!is_eligible) {
SetPrefetchStatus(PrefetchStatus::kPrefetchFailedIneligibleRedirect);
}
}
}
void PrefetchContainer::AddRedirectHop(const net::RedirectInfo& redirect_info) {
CHECK(resource_request_);
// There are sometimes other headers that are modified during navigation
// redirects; see |NavigationRequest::OnRedirectChecksComplete| (including
// some which are added by throttles). These aren't yet supported for
// prefetch, including browsing topics and client hints.
net::HttpRequestHeaders updated_headers;
std::vector<std::string> headers_to_remove = {variations::kClientDataHeader};
updated_headers.SetHeader(blink::kSecPurposeHeaderName,
GetSecPurposeHeaderValue(redirect_info.new_url));
// Remove any existing client hints headers (except, below, if we still want
// to send this particular hint).
if (base::FeatureList::IsEnabled(features::kPrefetchClientHints)) {
const auto& client_hints = network::GetClientHintToNameMap();
headers_to_remove.reserve(headers_to_remove.size() + client_hints.size());
for (const auto& [_, header] : client_hints) {
headers_to_remove.push_back(header);
}
}
// Sec-Speculation-Tags is set only when the prefetch is triggered
// by speculation rules and it is not cross-site prefetch redirection.
// To see more details:
// https://github.com/WICG/nav-speculation/blob/main/speculation-rules-tags.md#the-cross-site-case
headers_to_remove.push_back(blink::kSecSpeculationTagsHeaderName);
if (request().speculation_rules_tags().has_value() &&
!IsCrossSiteRequest(url::Origin::Create(redirect_info.new_url))) {
std::optional<std::string> serialized_list =
request().speculation_rules_tags()->ConvertStringToHeaderString();
CHECK(serialized_list.has_value());
updated_headers.SetHeader(blink::kSecSpeculationTagsHeaderName,
serialized_list.value());
}
// Then add the client hints that are appropriate for the redirect.
AddClientHintsHeaders(url::Origin::Create(redirect_info.new_url),
&updated_headers);
// To avoid spurious reordering, don't remove headers that will be updated
// anyway.
std::erase_if(headers_to_remove, [&](const std::string& header) {
return updated_headers.HasHeader(header);
});
// TODO(jbroman): We have several places that invoke
// `net::RedirectUtil::UpdateHttpRequest` and then need to do very similar
// work afterward. Ideally we would deduplicate these more.
bool should_clear_upload = false;
net::RedirectUtil::UpdateHttpRequest(
resource_request_->url, resource_request_->method, redirect_info,
std::move(headers_to_remove), std::move(updated_headers),
&resource_request_->headers, &should_clear_upload);
CHECK(!should_clear_upload);
resource_request_->url = redirect_info.new_url;
resource_request_->method = redirect_info.new_method;
resource_request_->site_for_cookies = redirect_info.new_site_for_cookies;
resource_request_->trusted_params->isolation_info =
resource_request_->trusted_params->isolation_info.CreateForRedirect(
url::Origin::Create(resource_request_->url));
// TODO(jbroman): This somewhat duplicates |referrer_|. Revisit usage of that
// (and related data members) to see if they can/should use this data instead.
resource_request_->referrer = GURL(redirect_info.new_referrer);
resource_request_->referrer_policy = redirect_info.new_referrer_policy;
AddXClientDataHeader(*resource_request_.get());
redirect_chain_.push_back(std::make_unique<PrefetchSingleRedirectHop>(
*this, redirect_info.new_url,
IsCrossSiteRequest(url::Origin::Create(redirect_info.new_url))));
}
bool PrefetchContainer::IsCrossSiteRequest(const url::Origin& origin) const {
return request().referring_origin().has_value() &&
!net::SchemefulSite::IsSameSite(request().referring_origin().value(),
origin);
}
bool PrefetchContainer::IsCrossOriginRequest(const url::Origin& origin) const {
return request().referring_origin().has_value() &&
!request().referring_origin().value().IsSameOriginWith(origin);
}
void PrefetchContainer::MarkCrossSiteContaminated() {
is_cross_site_contaminated_ = true;
}
void PrefetchContainer::AddXClientDataHeader(
network::ResourceRequest& resource_request) {
if (request().browser_context()) {
// Add X-Client-Data header with experiment IDs from field trials.
variations::AppendVariationsHeader(
resource_request.url,
request().browser_context()->IsOffTheRecord()
? variations::InIncognito::kYes
: variations::InIncognito::kNo,
variations::SignedIn::kNo, &resource_request);
}
}
void PrefetchContainer::RegisterCookieListener(
network::mojom::CookieManager* cookie_manager) {
PrefetchSingleRedirectHop& this_prefetch =
GetCurrentSingleRedirectHopToPrefetch();
this_prefetch.cookie_listener_ = PrefetchCookieListener::MakeAndRegister(
this_prefetch.url_, cookie_manager);
}
void PrefetchContainer::PauseAllCookieListeners() {
// TODO(crbug.com/377440445): Consider whether we actually need to
// pause/resume all single prefetch's cookie listener during each single
// prefetch's isolated cookie copy.
for (const auto& single_prefetch : redirect_chain_) {
if (single_prefetch->cookie_listener_) {
single_prefetch->cookie_listener_->PauseListening();
}
}
}
void PrefetchContainer::ResumeAllCookieListeners() {
// TODO(crbug.com/377440445): Consider whether we actually need to
// pause/resume all single prefetch's cookie listener during each single
// prefetch's isolated cookie copy.
for (const auto& single_prefetch : redirect_chain_) {
if (single_prefetch->cookie_listener_) {
single_prefetch->cookie_listener_->ResumeListening();
}
}
}
void PrefetchContainer::SetStreamingURLLoader(
base::WeakPtr<PrefetchStreamingURLLoader> streaming_loader) {
// The previous streaming loader (if any) should be already deleted or to be
// deleted soon when the new `streaming_loader` is set here.
CHECK(!streaming_loader_ || streaming_loader_->IsDeletionScheduledForCHECK());
streaming_loader_ = std::move(streaming_loader);
}
base::WeakPtr<PrefetchStreamingURLLoader>
PrefetchContainer::GetStreamingURLLoader() const {
// Streaming loaders already deleted or scheduled to be deleted shouldn't be
// used.
if (!streaming_loader_ || streaming_loader_->IsDeletionScheduledForCHECK()) {
return nullptr;
}
return streaming_loader_;
}
bool PrefetchContainer::IsStreamingURLLoaderDeletionScheduledForTesting()
const {
return streaming_loader_ && streaming_loader_->IsDeletionScheduledForCHECK();
}
const PrefetchResponseReader* PrefetchContainer::GetNonRedirectResponseReader()
const {
CHECK(!redirect_chain_.empty());
if (!redirect_chain_.back()->response_reader_->GetHead()) {
// Either the last PrefetchResponseReader is for a redirect response, or for
// a final response not yet receiving its header.
return nullptr;
}
return redirect_chain_.back()->response_reader_.get();
}
const network::mojom::URLResponseHead* PrefetchContainer::GetNonRedirectHead()
const {
return GetNonRedirectResponseReader()
? GetNonRedirectResponseReader()->GetHead()
: nullptr;
}
void PrefetchContainer::CancelStreamingURLLoaderIfNotServing() {
if (!streaming_loader_) {
return;
}
streaming_loader_->CancelIfNotServing();
streaming_loader_.reset();
}
void PrefetchContainer::OnDeterminedHead() {
SetLoadState(LoadState::kDeterminedHead);
if (GetNonRedirectHead()) {
time_header_determined_successfully_ = base::TimeTicks::Now();
}
// Propagates the header to `no_vary_search_data_` if a non-redirect response
// header is got.
MaybeSetNoVarySearchData();
for (auto& observer : observers_) {
observer.OnDeterminedHead(*this);
}
}
void PrefetchContainer::MaybeSetNoVarySearchData() {
CHECK(!no_vary_search_data_.has_value());
if (!GetNonRedirectHead()) {
return;
}
// RenderFrameHostImpl will be used to display error messagse in DevTools
// console. Can be null when the prefetch is browser-initiated.
RenderFrameHostImpl* rfhi_can_be_null = nullptr;
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
rfhi_can_be_null = renderer_initiator_info->GetRenderFrameHost();
}
no_vary_search_data_ = no_vary_search::ProcessHead(
*GetNonRedirectHead(), GetURL(), rfhi_can_be_null);
}
void PrefetchContainer::StartTimeoutTimerIfNeeded(
base::OnceClosure on_timeout_callback) {
if (request().ttl().is_positive()) {
CHECK(!timeout_timer_);
timeout_timer_ = std::make_unique<base::OneShotTimer>();
timeout_timer_->Start(FROM_HERE, request().ttl(),
std::move(on_timeout_callback));
}
}
// static
void PrefetchContainer::SetPrefetchResponseCompletedCallbackForTesting(
PrefetchResponseCompletedCallbackForTesting callback) {
GetPrefetchResponseCompletedCallbackForTesting() = // IN-TEST
std::move(callback);
}
void PrefetchContainer::OnPrefetchCompleteInternal(
const network::URLLoaderCompletionStatus& completion_status) {
DVLOG(1) << *this << "::OnPrefetchComplete";
UMA_HISTOGRAM_COUNTS_100("PrefetchProxy.Prefetch.RedirectChainSize",
redirect_chain_.size());
if (GetNonRedirectResponseReader()) {
UpdatePrefetchRequestMetrics(
GetNonRedirectResponseReader()->GetHead());
UpdateServingPageMetrics();
} else {
DVLOG(1) << *this << "::OnPrefetchComplete:"
<< "no non redirect response reader";
}
if (IsDecoy()) {
SetPrefetchStatus(PrefetchStatus::kPrefetchIsPrivacyDecoy);
return;
}
// TODO(crbug.com/40250089): Call
// `devtools_instrumentation::OnPrefetchBodyDataReceived()` with body of the
// response.
NotifyPrefetchRequestComplete(completion_status);
int net_error = completion_status.error_code;
int64_t body_length = completion_status.decoded_body_length;
RecordPrefetchProxyPrefetchMainframeNetError(net_error);
// Updates the prefetch's status if it hasn't been updated since the request
// first started. For the prefetch to reach the network stack, it must have
// `PrefetchStatus::kPrefetchNotStarted` or beyond.
DCHECK(HasPrefetchStatus());
if (GetPrefetchStatus() == PrefetchStatus::kPrefetchNotFinishedInTime) {
SetPrefetchStatus(net_error == net::OK
? PrefetchStatus::kPrefetchSuccessful
: PrefetchStatus::kPrefetchFailedNetError);
UpdateServingPageMetrics();
}
if (net_error == net::OK) {
time_prefetch_completed_successfully_ = base::TimeTicks::Now();
RecordPrefetchProxyPrefetchMainframeBodyLength(body_length);
}
const PrefetchStatus prefetch_status = GetPrefetchStatus();
if (prefetch_status == PrefetchStatus::kPrefetchSuccessful) {
// TODO(crbug.com/40946257): Current code doesn't support
// PrefetchReferringPageMetrics when the prefetch is initiated by browser.
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
if (renderer_initiator_info->prefetch_document_manager()) {
renderer_initiator_info->prefetch_document_manager()
->OnPrefetchSuccessful(this);
}
}
}
if (auto* browser_initiator_info = request().GetBrowserInitiatorInfo()) {
if (auto* listener = browser_initiator_info->request_status_listener()) {
switch (prefetch_status) {
case PrefetchStatus::kPrefetchSuccessful:
case PrefetchStatus::kPrefetchResponseUsed:
listener->OnPrefetchResponseCompleted();
break;
case PrefetchStatus::kPrefetchFailedNon2XX: {
int response_code =
GetNonRedirectHead()
? GetNonRedirectHead()->headers->response_code()
: 0;
listener->OnPrefetchResponseServerError(response_code);
break;
}
default:
listener->OnPrefetchResponseError();
break;
}
}
}
}
void PrefetchContainer::OnPrefetchComplete(
const network::URLLoaderCompletionStatus& completion_status) {
SetLoadState(LoadState::kCompletedOrFailed);
OnPrefetchCompleteInternal(completion_status);
std::optional<int> response_code = std::nullopt;
int net_error = completion_status.error_code;
if (net_error == net::OK && GetNonRedirectHead() &&
GetNonRedirectHead()->headers) {
response_code = GetNonRedirectHead()->headers->response_code();
}
for (auto& observer : observers_) {
observer.OnPrefetchCompletedOrFailed(*this, completion_status,
response_code);
}
if (GetPrefetchResponseCompletedCallbackForTesting()) {
GetPrefetchResponseCompletedCallbackForTesting().Run( // IN-TEST
GetWeakPtr());
}
}
void PrefetchContainer::UpdatePrefetchRequestMetrics(
const network::mojom::URLResponseHead* head) {
DVLOG(1) << *this << "::UpdatePrefetchRequestMetrics:"
<< "head = " << head;
if (head)
header_latency_ =
head->load_timing.receive_headers_end - head->load_timing.request_start;
}
PrefetchServableState PrefetchContainer::GetServableState(
base::TimeDelta cacheable_duration) const {
// Servable if the non-redirect response (either fully or partially
// received body) is servable.
if (GetNonRedirectResponseReader() &&
GetNonRedirectResponseReader()->Servable(cacheable_duration)) {
return PrefetchServableState::kServable;
}
DVLOG(1) << *this << "(GetServableState)"
<< "(streaming_loader=" << GetStreamingURLLoader().get()
<< ", LoadState=" << load_state_ << ")";
// Can only block until head if the request has been started using a
// streaming URL loader and head/failure/redirect hasn't been received yet.
if (GetStreamingURLLoader() &&
redirect_chain_.back()->response_reader_->IsWaitingForResponse()) {
return PrefetchServableState::kShouldBlockUntilHeadReceived;
}
if (features::UsePrefetchPrerenderIntegration()) {
switch (load_state_) {
case LoadState::kNotStarted:
case LoadState::kEligible:
return PrefetchServableState::kShouldBlockUntilEligibilityGot;
case LoadState::kFailedIneligible:
case LoadState::kStarted:
case LoadState::kDeterminedHead:
case LoadState::kCompletedOrFailed:
case LoadState::kFailedHeldback:
// nop
break;
}
}
return PrefetchServableState::kNotServable;
}
PrefetchSingleRedirectHop&
PrefetchContainer::GetCurrentSingleRedirectHopToPrefetch() const {
CHECK(redirect_chain_.size() > 0);
return *redirect_chain_[redirect_chain_.size() - 1];
}
const PrefetchSingleRedirectHop&
PrefetchContainer::GetPreviousSingleRedirectHopToPrefetch() const {
CHECK(redirect_chain_.size() > 1);
return *redirect_chain_[redirect_chain_.size() - 2];
}
void PrefetchContainer::SetServingPageMetrics(
base::WeakPtr<PrefetchServingPageMetricsContainer>
serving_page_metrics_container) {
serving_page_metrics_container_ = serving_page_metrics_container;
}
void PrefetchContainer::UpdateServingPageMetrics() {
DVLOG(1) << *this << "::UpdateServingPageMetrics:"
<< "serving_page_metrics_container_ = "
<< serving_page_metrics_container_.get();
if (!serving_page_metrics_container_) {
return;
}
serving_page_metrics_container_->SetRequiredPrivatePrefetchProxy(
request().prefetch_type().IsProxyRequiredWhenCrossOrigin());
serving_page_metrics_container_->SetPrefetchHeaderLatency(
GetPrefetchHeaderLatency());
if (HasPrefetchStatus()) {
serving_page_metrics_container_->SetPrefetchStatus(GetPrefetchStatus());
}
}
void PrefetchContainer::SimulatePrefetchEligibleForTest() {
if (request().attempt()) {
request().attempt()->SetEligibility(PreloadingEligibility::kEligible);
request().attempt()->SetHoldbackStatus(PreloadingHoldbackStatus::kAllowed);
}
SetLoadState(LoadState::kEligible);
SetPrefetchStatus(PrefetchStatus::kPrefetchNotStarted);
}
void PrefetchContainer::SimulatePrefetchStartedForTest() {
SetLoadState(LoadState::kStarted);
SetPrefetchStatus(PrefetchStatus::kPrefetchNotFinishedInTime);
}
void PrefetchContainer::SimulatePrefetchCompletedForTest() {
SetPrefetchStatus(PrefetchStatus::kPrefetchSuccessful);
}
void PrefetchContainer::SimulatePrefetchFailedIneligibleForTest(
PreloadingEligibility eligibility) {
CHECK_NE(PreloadingEligibility::kEligible, eligibility);
if (request().attempt()) {
request().attempt()->SetEligibility(eligibility);
}
SetLoadState(LoadState::kFailedIneligible);
}
void PrefetchContainer::OnDetectedCookiesChange(
std::optional<bool>
is_unblock_for_cookies_changed_triggered_by_this_prefetch_container) {
// Multiple `PrefetchMatchResolver` can wait the same `PrefetchContainer`. So,
// `OnDetectedCookiesChange()` can be called multiple times,
if (on_detected_cookies_change_called_) {
return;
}
on_detected_cookies_change_called_ = true;
// There are cases that `prefetch_status_` is failure but this method is
// called. For more details, see
// https://docs.google.com/document/d/1G48SaWbdOy1yNBT1wio2IHVuUtddF5VLFsT6BRSYPMI/edit?tab=t.hpkotaxo7tfh#heading=h.woaoy8erwx63
//
// To prevent crash, we don't call `SetPrefetchStatus()`.
if (prefetch_status_ &&
TriggeringOutcomeFromStatus(prefetch_status_.value()) ==
PreloadingTriggeringOutcome::kFailure) {
SCOPED_CRASH_KEY_NUMBER("PrefetchContainer", "ODCC2_from",
static_cast<int>(prefetch_status_.value()));
if (is_unblock_for_cookies_changed_triggered_by_this_prefetch_container
.has_value()) {
SCOPED_CRASH_KEY_BOOL(
"PrefetchContainer", "ODCC2_iufcctbtpc",
is_unblock_for_cookies_changed_triggered_by_this_prefetch_container
.value());
}
base::debug::DumpWithoutCrashing();
return;
}
CHECK_NE(GetPrefetchStatus(), PrefetchStatus::kPrefetchNotUsedCookiesChanged);
SetPrefetchStatus(PrefetchStatus::kPrefetchNotUsedCookiesChanged);
UpdateServingPageMetrics();
CancelStreamingURLLoaderIfNotServing();
}
void PrefetchContainer::OnPrefetchStarted() {
SetLoadState(PrefetchContainer::LoadState::kStarted);
time_prefetch_started_ = base::TimeTicks::Now();
}
bool PrefetchContainer::HasSameReferringURLForMetrics(
const PrefetchContainer& other) const {
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
if (auto* other_renderer_initiator_info =
other.request().GetRendererInitiatorInfo()) {
return renderer_initiator_info->url_hash() ==
other_renderer_initiator_info->url_hash();
}
}
return false;
}
bool PrefetchContainer::HasSameReferringRenderFrameHostIdForMetrics(
const PrefetchContainer& other) const {
if (auto* renderer_initiator_info = request().GetRendererInitiatorInfo()) {
if (auto* other_renderer_initiator_info =
other.request().GetRendererInitiatorInfo()) {
return renderer_initiator_info->GetRenderFrameHostId() ==
other_renderer_initiator_info->GetRenderFrameHostId();
}
}
return false;
}
GURL PrefetchContainer::GetCurrentURL() const {
return GetCurrentSingleRedirectHopToPrefetch().url_;
}
GURL PrefetchContainer::GetPreviousURL() const {
return GetPreviousSingleRedirectHopToPrefetch().url_;
}
bool PrefetchContainer::IsIsolatedNetworkContextRequiredForCurrentPrefetch()
const {
const PrefetchSingleRedirectHop& this_prefetch =
GetCurrentSingleRedirectHopToPrefetch();
return this_prefetch.is_isolated_network_context_required_;
}
bool PrefetchContainer::IsIsolatedNetworkContextRequiredForPreviousRedirectHop()
const {
const PrefetchSingleRedirectHop& previous_prefetch =
GetPreviousSingleRedirectHopToPrefetch();
return previous_prefetch.is_isolated_network_context_required_;
}
base::WeakPtr<PrefetchResponseReader>
PrefetchContainer::GetResponseReaderForCurrentPrefetch() {
const PrefetchSingleRedirectHop& this_prefetch =
GetCurrentSingleRedirectHopToPrefetch();
CHECK(this_prefetch.response_reader_);
return this_prefetch.response_reader_->GetWeakPtr();
}
bool PrefetchContainer::IsProxyRequiredForURL(const GURL& url) const {
return IsCrossOriginRequest(url::Origin::Create(url)) &&
request().prefetch_type().IsProxyRequiredWhenCrossOrigin();
}
void PrefetchContainer::MakeResourceRequest(
const net::HttpRequestHeaders& additional_headers) {
// |AddRedirectHop| updates this request later on. Anything here that should
// be changed on redirect should happen there.
const GURL& url = GetURL();
url::Origin origin = url::Origin::Create(url);
net::IsolationInfo isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromOrigin(origin));
auto priority = [&] {
if (request().priority().has_value()) {
switch (request().priority().value()) {
case PrefetchPriority::kLow:
return net::RequestPriority::IDLE;
case PrefetchPriority::kMedium:
return net::RequestPriority::LOW;
case PrefetchPriority::kHigh:
return net::RequestPriority::MEDIUM;
case PrefetchPriority::kHighest:
return net::RequestPriority::HIGHEST;
}
}
// TODO(crbug.com/426404355): Migrate to use `PrefetchPriority`.
if (IsSpeculationRuleType(request().prefetch_type().trigger_type())) {
// This may seem inverted (surely immediate prefetches would be higher
// priority), but the fact that we're doing this at all for more
// conservative candidates suggests a strong engagement signal.
//
// TODO(crbug.com/40276985): Ideally, we would actually use a combination
// of the actual engagement seen (rather than the minimum required to
// trigger the candidate) and the declared eagerness, and update them as
// the prefetch becomes increasingly likely.
blink::mojom::SpeculationEagerness eagerness =
request().prefetch_type().GetEagerness();
switch (eagerness) {
case blink::mojom::SpeculationEagerness::kConservative:
return net::RequestPriority::MEDIUM;
case blink::mojom::SpeculationEagerness::kModerate:
return net::RequestPriority::LOW;
// TODO(crbug.com/40287486, crbug.com/406927300): Set appropriate value
// after changing the behavior for `kEager`
case blink::mojom::SpeculationEagerness::kEager:
case blink::mojom::SpeculationEagerness::kImmediate:
return net::RequestPriority::IDLE;
}
} else {
if (base::FeatureList::IsEnabled(
features::kPrefetchNetworkPriorityForEmbedders)) {
return net::RequestPriority::MEDIUM;
} else {
return net::RequestPriority::IDLE;
}
}
}();
mojo::PendingRemote<network::mojom::DevToolsObserver>
devtools_observer_remote;
if (std::optional<mojo::PendingRemote<network::mojom::DevToolsObserver>>
devtools_observer = MakeSelfOwnedNetworkServiceDevToolsObserver()) {
devtools_observer_remote = std::move(devtools_observer.value());
}
// If we ever implement prefetching for subframes, this value should be
// reconsidered, as this causes us to reset the site for cookies on cross-site
// redirect.
const bool is_main_frame = true;
auto resource_request = CreateResourceRequestForNavigation(
net::HttpRequestHeaders::kGetMethod, url,
network::mojom::RequestDestination::kDocument, referrer_, isolation_info,
std::move(devtools_observer_remote), priority, is_main_frame);
// Note: Even without LOAD_DISABLE_CACHE, a cross-site prefetch uses a
// separate network context, which means responses cached before the prefetch
// are not visible to the prefetch, and anything cached by this request will
// not be visible outside of the network context.
resource_request->load_flags = net::LOAD_PREFETCH;
resource_request->headers.MergeFrom(request().additional_headers());
resource_request->headers.MergeFrom(additional_headers);
if (!base::FeatureList::IsEnabled(
blink::features::kRemovePurposeHeaderForPrefetch)) {
resource_request->headers.SetHeader(blink::kPurposeHeaderName,
blink::kSecPurposePrefetchHeaderValue);
}
resource_request->headers.SetHeader(blink::kSecPurposeHeaderName,
GetSecPurposeHeaderValue(url));
resource_request->headers.SetHeader("Upgrade-Insecure-Requests", "1");
// Sec-Speculation-Tags is set only when the prefetch is triggered
// by speculation rules and it is not cross-site prefetch.
// To see more details:
// https://github.com/WICG/nav-speculation/blob/main/speculation-rules-tags.md#the-cross-site-case
if (request().speculation_rules_tags().has_value() &&
!IsCrossSiteRequest(origin)) {
std::optional<std::string> serialized_list =
request().speculation_rules_tags()->ConvertStringToHeaderString();
CHECK(serialized_list.has_value());
resource_request->headers.SetHeader(blink::kSecSpeculationTagsHeaderName,
serialized_list.value());
}
// There are sometimes other headers that are set during navigation. These
// aren't yet supported for prefetch, including browsing topics.
resource_request->devtools_request_id = RequestId();
AddClientHintsHeaders(origin, &resource_request->headers);
if (request().should_append_variations_header()) {
AddXClientDataHeader(*resource_request.get());
}
// `URLLoaderNetworkServiceObserver`
// (`resource_request->trusted_params->url_loader_network_observer`) is NOT
// set here, because for prefetching request we don't want to ask users e.g.
// for authentication/cert errors, and instead make the prefetch fail. Because
// of this, `ServiceWorkerClient::GetOngoingNavigationRequestBeforeCommit()`
// is never called. `NavPrefetchBrowserTest` has the corresponding test
// coverage.
resource_request_ = std::move(resource_request);
}
void PrefetchContainer::UpdateReferrer(
const GURL& new_referrer_url,
const network::mojom::ReferrerPolicy& new_referrer_policy) {
referrer_.url = new_referrer_url;
referrer_.policy = new_referrer_policy;
}
const PrefetchKey& PrefetchContainer::key() const {
return request().key();
}
const GURL& PrefetchContainer::GetURL() const {
return request().key().url();
}
const std::optional<net::HttpNoVarySearchData>&
PrefetchContainer::GetNoVarySearchHint() const {
return request().no_vary_search_hint();
}
void PrefetchContainer::AddClientHintsHeaders(
const url::Origin& origin,
net::HttpRequestHeaders* request_headers) {
if (!base::FeatureList::IsEnabled(features::kPrefetchClientHints)) {
return;
}
if (!request().browser_context()) {
return;
}
ClientHintsControllerDelegate* client_hints_delegate =
request().browser_context()->GetClientHintsControllerDelegate();
if (!client_hints_delegate) {
return;
}
// TODO(crbug.com/41497015): Consider supporting UA override mode here
const bool is_ua_override_on = false;
net::HttpRequestHeaders client_hints_headers;
if (request().is_javascript_enabled()) {
// Historically, `AddClientHintsHeadersToPrefetchNavigation` added
// Client Hints headers iff `request().is_javascript_enabled()`, so the `if`
// block here is to persist the behavior.
// TODO(crbug.com/394716357): Revisit if we really want to allow prefetch
// for non-Javascript enabled profile/origins.
AddClientHintsHeadersToPrefetchNavigation(
origin, &client_hints_headers, request().browser_context(),
client_hints_delegate, is_ua_override_on);
}
// Merge in the client hints which are suitable to include given this is a
// prefetch, and potentially a cross-site only. (This logic might need to be
// revisited if we ever supported prefetching in another site's partition,
// such as in a subframe.)
const bool is_cross_site = IsCrossSiteRequest(origin);
const auto cross_site_behavior =
features::kPrefetchClientHintsCrossSiteBehavior.Get();
if (!is_cross_site ||
cross_site_behavior ==
features::PrefetchClientHintsCrossSiteBehavior::kAll) {
request_headers->MergeFrom(client_hints_headers);
} else if (cross_site_behavior ==
features::PrefetchClientHintsCrossSiteBehavior::kLowEntropy) {
for (const auto& [ch, header] : network::GetClientHintToNameMap()) {
if (blink::IsClientHintSentByDefault(ch)) {
std::optional<std::string> header_value =
client_hints_headers.GetHeader(header);
if (header_value) {
request_headers->SetHeader(header, std::move(header_value).value());
}
}
}
}
}
std::ostream& operator<<(std::ostream& ostream,
const PrefetchContainer& prefetch_container) {
return ostream << "PrefetchContainer[" << &prefetch_container
<< ", Key=" << prefetch_container.key() << "]";
}
std::ostream& operator<<(std::ostream& ostream,
PrefetchContainer::LoadState state) {
switch (state) {
case PrefetchContainer::LoadState::kNotStarted:
return ostream << "NotStarted";
case PrefetchContainer::LoadState::kEligible:
return ostream << "Eligible";
case PrefetchContainer::LoadState::kFailedIneligible:
return ostream << "FailedIneligible";
case PrefetchContainer::LoadState::kStarted:
return ostream << "Started";
case PrefetchContainer::LoadState::kDeterminedHead:
return ostream << "DeterminedHead";
case PrefetchContainer::LoadState::kCompletedOrFailed:
return ostream << "CompletedOrFailed";
case PrefetchContainer::LoadState::kFailedHeldback:
return ostream << "FailedHeldback";
}
}
const char* PrefetchContainer::GetSecPurposeHeaderValue(
const GURL& request_url) const {
switch (request().preload_pipeline_info().planned_max_preloading_type()) {
case PreloadingType::kPrefetch:
if (IsProxyRequiredForURL(request_url)) {
return blink::kSecPurposePrefetchAnonymousClientIpHeaderValue;
} else {
return blink::kSecPurposePrefetchHeaderValue;
}
case PreloadingType::kPrerenderUntilScript:
case PreloadingType::kPrerender:
if (IsProxyRequiredForURL(request_url)) {
// Note that this path would be reachable if a prefetch ahead of
// prerender were triggered with a speculation candidate with
// `requires_anonymous_client_ip_when_cross_origin`. But such
// Speculation Rules are discarded in blink.
//
// See
// https://github.com/WICG/nav-speculation/blob/main/triggers.md#requirements
NOTREACHED();
} else {
return blink::kSecPurposePrefetchPrerenderHeaderValue;
}
case PreloadingType::kUnspecified:
case PreloadingType::kPreconnect:
case PreloadingType::kNoStatePrefetch:
case PreloadingType::kLinkPreview:
NOTREACHED();
}
}
void PrefetchContainer::OnInitialPrefetchFailedIneligible(
PreloadingEligibility eligibility) {
CHECK(redirect_chain_.size() == 1);
CHECK_NE(eligibility, PreloadingEligibility::kEligible);
if (auto* browser_initiator_info = request().GetBrowserInitiatorInfo()) {
if (auto* listener = browser_initiator_info->request_status_listener()) {
listener->OnPrefetchStartFailedGeneric();
}
}
}
void PrefetchContainer::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void PrefetchContainer::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
bool PrefetchContainer::IsExactMatch(const GURL& url) const {
return url == GetURL();
}
bool PrefetchContainer::IsNoVarySearchHeaderMatch(const GURL& url) const {
const std::optional<net::HttpNoVarySearchData>& no_vary_search_data =
GetNoVarySearchData();
return no_vary_search_data &&
no_vary_search_data->AreEquivalent(url, GetURL());
}
bool PrefetchContainer::ShouldWaitForNoVarySearchHeader(const GURL& url) const {
const std::optional<net::HttpNoVarySearchData>& no_vary_search_hint =
request().no_vary_search_hint();
return !GetNonRedirectHead() && no_vary_search_hint &&
no_vary_search_hint->AreEquivalent(url, GetURL());
}
void PrefetchContainer::OnUnregisterCandidate(
const GURL& navigated_url,
bool is_served,
PrefetchPotentialCandidateServingResult matching_result,
bool is_nav_prerender,
std::optional<base::TimeDelta> blocked_duration) {
// Note that this method can be called with `is_in_dtor_` true.
//
// TODO(crbug.com/356314759): Avoid calling this with `is_in_dtor_`
// true.
if (is_served) {
served_count_++;
UMA_HISTOGRAM_COUNTS_100("PrefetchProxy.AfterClick.RedirectChainSize",
redirect_chain_.size());
}
RecordPrefetchMatchingBlockedNavigationHistogram(blocked_duration.has_value(),
is_nav_prerender);
RecordBlockUntilHeadDurationHistogram(blocked_duration, is_served,
is_nav_prerender);
RecordPrefetchPotentialCandidateServingResultHistogram(matching_result);
// Note that `PreloadingAttemptImpl::SetIsAccurateTriggering()` is called for
// prefetch in
//
// - A. `PreloadingDataImpl::DidStartNavigation()`
// - B. Here
//
// A covers prefetches that satisfy `bool(GetNonRedirectHead())` at that
// timing. B covers almost all ones that were once potentially matching to the
// navigation, including that was `kBlockUntilHead` state.
//
// Note that multiple calls are safe and set a correct value.
//
// Historical note: Before No-Vary-Search hint, the decision to use a
// prefetched response was made at A. With No-Vary-Search hint the decision to
// use an in-flight prefetched response is delayed until the headers are
// received from the server. This happens after `DidStartNavigation()`. At
// this point in the code we have already decided we are going to use the
// prefetch, so we can safely call `SetIsAccurateTriggering()`.
if (request().attempt()) {
static_cast<PreloadingAttemptImpl*>(request().attempt())
->SetIsAccurateTriggering(navigated_url);
}
}
void PrefetchContainer::MigrateNewlyAdded(
std::unique_ptr<PrefetchContainer> added) {
// `inherited_preload_pipeline_infos_` increases only if it is managed under
// `PrefetchService`.
CHECK(added->inherited_preload_pipeline_infos_.empty());
// Propagate eligibility (and status) to `added`.
//
// Assume we don't. (*) case is problematic.
//
// - If eligibility is not got, eligibility and status will be propagated by
// the following `OnEligibilityCheckComplete()` and
// `SetPrefetchStatusWithoutUpdatingTriggeringOutcome()`.
// - If eligibility is got and ineligible, this `PrefetchContainer` is
// `kNotServed` and `MigrateNewlyAdded()` is not called.
// - If eligibility is got and `kEligible`:
// - If status is not got, status will be propagated by the following
// `SetPrefetchStatusWithoutUpdatingTriggeringOutcome()`.
// - If status is eventually `kPrefetchSuccessful` or
// `kPrefetchResponseUsed`, `kPrefetchResponseUsed` will be propagated
// at the prefetch matching end.
// - If status is eventually failure, status is propagated, but
// eligibility is `kUnspecified`. (*)
// - If status is got and `kPrefetchSuccessful` or `kPrefetchResponseUsed`,
// `kPrefetchResponseUsed` will be propagated at the prefetch matching
// end.
// - If status is got and failure, this `PrefetchContainer` is `kNotServed`
// and `MigrateNewlyAdded()` is not called.
//
// In (*), `PrerenderHost` have to cancel prerender with eligibility
// `kUnspecified` and status failure. It's relatively complicated condition.
// See a test
// `PrerendererImplBrowserTestPrefetchAhead.PrefetchMigratedPrefetchFailurePrerenderFailure`.
//
// To make things simple, we propagate both eligibility and status.
scoped_refptr<PreloadPipelineInfoImpl> added_preload_pipeline_info =
base::WrapRefCounted(&added->request().preload_pipeline_info());
added_preload_pipeline_info->SetPrefetchEligibility(
request().preload_pipeline_info().prefetch_eligibility());
if (auto prefetch_status =
request().preload_pipeline_info().prefetch_status()) {
added_preload_pipeline_info->SetPrefetchStatus(*prefetch_status);
}
inherited_preload_pipeline_infos_.push_back(
std::move(added_preload_pipeline_info));
is_likely_ahead_of_prerender_ |= added->is_likely_ahead_of_prerender_;
}
void PrefetchContainer::NotifyPrefetchRequestWillBeSent(
const network::mojom::URLResponseHeadPtr* redirect_head) {
if (IsDecoy()) {
return;
}
auto* renderer_initiator_info = request().GetRendererInitiatorInfo();
if (!renderer_initiator_info) {
// Don't emit CDP events if the trigger is not speculation rules.
return;
}
auto* rfh = renderer_initiator_info->GetRenderFrameHost();
auto* ftn = FrameTreeNode::From(rfh);
if (!rfh) {
// Don't emit CDP events if the initiator document isn't alive.
return;
}
if (redirect_head && *redirect_head) {
const network::mojom::URLResponseHeadDevToolsInfoPtr info =
network::ExtractDevToolsInfo(**redirect_head);
const GURL url = GetPreviousURL();
std::pair<const GURL&, const network::mojom::URLResponseHeadDevToolsInfo&>
redirect_info{url, *info.get()};
devtools_instrumentation::OnPrefetchRequestWillBeSent(
*ftn, RequestId(), rfh->GetLastCommittedURL(), *GetResourceRequest(),
std::move(redirect_info));
} else {
devtools_instrumentation::OnPrefetchRequestWillBeSent(
*ftn, RequestId(), rfh->GetLastCommittedURL(), *GetResourceRequest(),
std::nullopt);
}
}
void PrefetchContainer::NotifyPrefetchResponseReceived(
const network::mojom::URLResponseHead& head) {
// Ensured by the caller `PrefetchService::OnPrefetchResponseStarted()`.
CHECK(!IsDecoy());
time_url_request_started_ = head.load_timing.request_start;
// DevTools plumbing.
auto* renderer_initiator_info = request().GetRendererInitiatorInfo();
if (!renderer_initiator_info) {
// Don't emit CDP events if the trigger is not speculation rules.
return;
}
auto* ftn =
FrameTreeNode::From(renderer_initiator_info->GetRenderFrameHost());
if (!ftn) {
// Don't emit CDP events if the initiator document isn't alive.
return;
}
devtools_instrumentation::OnPrefetchResponseReceived(ftn, RequestId(),
GetCurrentURL(), head);
}
void PrefetchContainer::NotifyPrefetchRequestComplete(
const network::URLLoaderCompletionStatus& completion_status) {
// Ensured by the caller `PrefetchService::OnPrefetchResponseStarted()`.
CHECK(!IsDecoy());
auto* renderer_initiator_info = request().GetRendererInitiatorInfo();
if (!renderer_initiator_info) {
// Don't emit CDP events if the trigger is not speculation rules.
return;
}
auto* ftn =
FrameTreeNode::From(renderer_initiator_info->GetRenderFrameHost());
if (!ftn) {
// Don't emit CDP events if the initiator document isn't alive.
return;
}
devtools_instrumentation::OnPrefetchRequestComplete(ftn, RequestId(),
completion_status);
}
std::optional<mojo::PendingRemote<network::mojom::DevToolsObserver>>
PrefetchContainer::MakeSelfOwnedNetworkServiceDevToolsObserver() {
if (IsDecoy()) {
return std::nullopt;
}
auto* renderer_initiator_info = request().GetRendererInitiatorInfo();
if (!renderer_initiator_info) {
// Don't emit CDP events if the trigger is not speculation rules.
return std::nullopt;
}
auto* ftn =
FrameTreeNode::From(renderer_initiator_info->GetRenderFrameHost());
if (!ftn) {
// Don't emit CDP events if the initiator document isn't alive.
return std::nullopt;
}
return NetworkServiceDevToolsObserver::MakeSelfOwned(ftn);
}
std::string PrefetchContainer::GetMetricsSuffix() const {
std::optional<std::string> embedder_histogram_suffix;
if (auto* browser_initiator_info = request().GetBrowserInitiatorInfo()) {
embedder_histogram_suffix =
browser_initiator_info->embedder_histogram_suffix();
}
return GetMetricsSuffixTriggerTypeAndEagerness(request().prefetch_type(),
embedder_histogram_suffix);
}
void PrefetchContainer::MaybeRecordPrefetchStatusToUMA(
PrefetchStatus prefetch_status) {
if (prefetch_status_recorded_to_uma_) {
return;
}
base::UmaHistogramEnumeration("Preloading.Prefetch.PrefetchStatus",
prefetch_status);
prefetch_status_recorded_to_uma_ = true;
}
void PrefetchContainer::OnServiceWorkerStateDetermined(
PrefetchServiceWorkerState service_worker_state) {
switch (service_worker_state_) {
case PrefetchServiceWorkerState::kDisallowed:
CHECK_EQ(service_worker_state, PrefetchServiceWorkerState::kDisallowed);
break;
case PrefetchServiceWorkerState::kAllowed:
CHECK_NE(service_worker_state, PrefetchServiceWorkerState::kAllowed);
service_worker_state_ = service_worker_state;
break;
case PrefetchServiceWorkerState::kControlled:
NOTREACHED();
}
}
void PrefetchContainer::RecordPrefetchDurationHistogram() {
if (!time_added_to_prefetch_service_.has_value()) {
return;
}
if (!time_initial_eligibility_got_.has_value()) {
return;
}
base::UmaHistogramTimes(
base::StrCat({
"Prefetch.PrefetchContainer.AddedToInitialEligibility.",
GetMetricsSuffix(),
}),
time_initial_eligibility_got_.value() -
time_added_to_prefetch_service_.value());
if (!time_prefetch_started_.has_value()) {
return;
}
base::UmaHistogramTimes(
base::StrCat({
"Prefetch.PrefetchContainer.AddedToPrefetchStarted.",
GetMetricsSuffix(),
}),
time_prefetch_started_.value() - time_added_to_prefetch_service_.value());
base::UmaHistogramTimes(
base::StrCat({
"Prefetch.PrefetchContainer.InitialEligibilityToPrefetchStarted.",
GetMetricsSuffix(),
}),
time_prefetch_started_.value() - time_initial_eligibility_got_.value());
if (!time_url_request_started_.has_value()) {
return;
}
base::UmaHistogramTimes(base::StrCat({
"Prefetch.PrefetchContainer."
"AddedToURLRequestStarted.",
GetMetricsSuffix(),
}),
time_url_request_started_.value() -
time_added_to_prefetch_service_.value());
base::UmaHistogramTimes(
base::StrCat({
"Prefetch.PrefetchContainer.PrefetchStartedToURLRequestStarted.",
GetMetricsSuffix(),
}),
time_url_request_started_.value() - time_prefetch_started_.value());
if (!time_header_determined_successfully_.has_value()) {
return;
}
base::UmaHistogramTimes(base::StrCat({
"Prefetch.PrefetchContainer."
"AddedToHeaderDeterminedSuccessfully.",
GetMetricsSuffix(),
}),
time_header_determined_successfully_.value() -
time_added_to_prefetch_service_.value());
base::UmaHistogramTimes(base::StrCat({
"Prefetch.PrefetchContainer."
"PrefetchStartedToHeaderDeterminedSuccessfully.",
GetMetricsSuffix(),
}),
time_header_determined_successfully_.value() -
time_prefetch_started_.value());
if (!time_prefetch_completed_successfully_.has_value()) {
return;
}
base::UmaHistogramTimes(base::StrCat({
"Prefetch.PrefetchContainer."
"AddedToPrefetchCompletedSuccessfully.",
GetMetricsSuffix(),
}),
time_prefetch_completed_successfully_.value() -
time_added_to_prefetch_service_.value());
base::UmaHistogramTimes(base::StrCat({
"Prefetch.PrefetchContainer."
"PrefetchStartedToPrefetchCompletedSuccessfully.",
GetMetricsSuffix(),
}),
time_prefetch_completed_successfully_.value() -
time_prefetch_started_.value());
}
void PrefetchContainer::RecordPrefetchMatchingBlockedNavigationHistogram(
bool blocked_until_head,
bool is_nav_prerender) {
base::UmaHistogramBoolean(
base::StrCat(
{"Prefetch.PrefetchMatchingBlockedNavigation.PerMatchingCandidate.",
GetMetricsSuffix()}),
blocked_until_head);
base::UmaHistogramBoolean(
base::StrCat(
{"Prefetch.PrefetchMatchingBlockedNavigation.PerMatchingCandidate.",
is_nav_prerender ? "Prerender." : "NonPrerender.",
GetMetricsSuffix()}),
blocked_until_head);
}
void PrefetchContainer::RecordBlockUntilHeadDurationHistogram(
const std::optional<base::TimeDelta>& blocked_duration,
bool served,
bool is_nav_prerender) {
base::UmaHistogramTimes(
base::StrCat({"Prefetch.BlockUntilHeadDuration.PerMatchingCandidate.",
served ? "Served." : "NotServed.", GetMetricsSuffix()}),
blocked_duration.value_or(base::Seconds(0)));
base::UmaHistogramTimes(
base::StrCat({"Prefetch.BlockUntilHeadDuration.PerMatchingCandidate.",
is_nav_prerender ? "Prerender." : "NonPrerender.",
served ? "Served." : "NotServed.", GetMetricsSuffix()}),
blocked_duration.value_or(base::Seconds(0)));
}
void PrefetchContainer::RecordPrefetchPotentialCandidateServingResultHistogram(
PrefetchPotentialCandidateServingResult matching_result) {
base::UmaHistogramEnumeration(
base::StrCat({"Prefetch.PrefetchPotentialCandidateServingResult."
"PerMatchingCandidate.",
GetMetricsSuffix()}),
matching_result);
}
void PrefetchContainer::RecordPrefetchContainerServedCountHistogram() {
base::UmaHistogramCounts100(
base::StrCat(
{"Prefetch.PrefetchContainer.ServedCount.", GetMetricsSuffix()}),
served_count_);
}
} // namespace content