blob: 74931209baa6d17063c65e5013d1ebf1f1ee11d3 [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/preloading_attempt_impl.h"
#include "base/containers/span.h"
#include "base/metrics/crc32.h"
#include "base/metrics/histogram_functions.h"
#include "base/state_transitions.h"
#include "base/strings/strcat.h"
#include "content/browser/preloading/preloading.h"
#include "content/browser/preloading/preloading_config.h"
#include "content/public/browser/preloading.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/metrics/public/cpp/ukm_source_id.h"
#include "third_party/blink/public/common/features.h"
namespace content {
namespace {
void DCHECKTriggeringOutcomeTransitions(PreloadingTriggeringOutcome old_state,
PreloadingTriggeringOutcome new_state) {
#if DCHECK_IS_ON()
static const base::NoDestructor<
base::StateTransitions<PreloadingTriggeringOutcome>>
allowed_transitions(base::StateTransitions<PreloadingTriggeringOutcome>({
{PreloadingTriggeringOutcome::kUnspecified,
{PreloadingTriggeringOutcome::kDuplicate,
PreloadingTriggeringOutcome::kRunning,
PreloadingTriggeringOutcome::kReady,
PreloadingTriggeringOutcome::kSuccess,
PreloadingTriggeringOutcome::kFailure,
PreloadingTriggeringOutcome::kTriggeredButOutcomeUnknown,
PreloadingTriggeringOutcome::kTriggeredButUpgradedToPrerender,
PreloadingTriggeringOutcome::kTriggeredButPending,
PreloadingTriggeringOutcome::kNoOp}},
{PreloadingTriggeringOutcome::kDuplicate, {}},
{PreloadingTriggeringOutcome::kRunning,
{PreloadingTriggeringOutcome::kReady,
PreloadingTriggeringOutcome::kFailure,
PreloadingTriggeringOutcome::kTriggeredButUpgradedToPrerender}},
// It can be possible that the preloading attempt may end up failing
// after being ready to use, for cases where we have to cancel the
// attempt for performance and security reasons.
// The transition of kReady to kReady occurs when the main frame
// navigation is completed in a preloaded page.
{PreloadingTriggeringOutcome::kReady,
{PreloadingTriggeringOutcome::kReady,
PreloadingTriggeringOutcome::kSuccess,
PreloadingTriggeringOutcome::kFailure,
PreloadingTriggeringOutcome::kTriggeredButUpgradedToPrerender}},
{PreloadingTriggeringOutcome::kSuccess, {}},
{PreloadingTriggeringOutcome::kFailure, {}},
{PreloadingTriggeringOutcome::kTriggeredButOutcomeUnknown, {}},
{PreloadingTriggeringOutcome::kTriggeredButUpgradedToPrerender,
{PreloadingTriggeringOutcome::kFailure}},
{PreloadingTriggeringOutcome::kTriggeredButPending,
{PreloadingTriggeringOutcome::kRunning,
PreloadingTriggeringOutcome::kFailure}},
{PreloadingTriggeringOutcome::kNoOp, {}},
}));
DCHECK_STATE_TRANSITION(allowed_transitions,
/*old_state=*/old_state,
/*new_state=*/new_state);
#endif // DCHECK_IS_ON()
}
void CheckReadyOutcomePreloadingType(PreloadingType type) {
switch (type) {
case PreloadingType::kPrefetch:
case PreloadingType::kPrerender:
case PreloadingType::kNoStatePrefetch:
case PreloadingType::kLinkPreview:
case PreloadingType::kPrerenderUntilScript:
return;
default:
NOTREACHED() << "Unexpected preloading type: " << static_cast<int>(type);
}
}
} // namespace
void PreloadingAttemptImpl::SetEligibility(PreloadingEligibility eligibility) {
// Ensure that eligiblity is only set once and that it's set before the
// holdback status and the triggering outcome.
CHECK_EQ(eligibility_, PreloadingEligibility::kUnspecified);
CHECK_EQ(holdback_status_, PreloadingHoldbackStatus::kUnspecified);
CHECK_EQ(triggering_outcome_, PreloadingTriggeringOutcome::kUnspecified);
CHECK_NE(eligibility, PreloadingEligibility::kUnspecified);
eligibility_ = eligibility;
}
// TODO(crbug.com/40275772): most call sites of this should be removed, as
// PreloadingConfig should subsume most feature-specific holdbacks that exist
// today. Some cases can remain as specific overrides of the PreloadingConfig
// logic, e.g. if DevTools is open, or for features that are still launching and
// thus have their own separate holdback feature while they ramp up.
void PreloadingAttemptImpl::SetHoldbackStatus(
PreloadingHoldbackStatus holdback_status) {
// Ensure that the holdback status is only set once and that it's set for
// eligible attempts and before the triggering outcome.
CHECK_EQ(eligibility_, PreloadingEligibility::kEligible);
CHECK_EQ(holdback_status_, PreloadingHoldbackStatus::kUnspecified);
CHECK_EQ(triggering_outcome_, PreloadingTriggeringOutcome::kUnspecified);
CHECK_NE(holdback_status, PreloadingHoldbackStatus::kUnspecified);
holdback_status_ = holdback_status;
}
bool PreloadingAttemptImpl::ShouldHoldback() {
CHECK_EQ(eligibility_, PreloadingEligibility::kEligible);
if (holdback_status_ != PreloadingHoldbackStatus::kUnspecified) {
// The holdback status has already been determined, just use that value.
return holdback_status_ == PreloadingHoldbackStatus::kHoldback;
}
bool should_holdback_due_to_preloading_config =
PreloadingConfig::GetInstance().ShouldHoldback(preloading_type_,
creating_predictor_);
bool should_holdback_due_to_autosr_holdback =
creating_predictor_ == content_preloading_predictor::
kSpeculationRulesFromAutoSpeculationRules &&
blink::features::kAutoSpeculationRulesHoldback.Get();
bool should_holdback = should_holdback_due_to_preloading_config ||
should_holdback_due_to_autosr_holdback;
if (should_holdback) {
holdback_status_ = PreloadingHoldbackStatus::kHoldback;
} else {
holdback_status_ = PreloadingHoldbackStatus::kAllowed;
}
return should_holdback;
}
void PreloadingAttemptImpl::SetTriggeringOutcome(
PreloadingTriggeringOutcome triggering_outcome) {
// Ensure that the triggering outcome is only set for eligible and
// non-holdback attempts.
CHECK_EQ(eligibility_, PreloadingEligibility::kEligible);
CHECK_EQ(holdback_status_, PreloadingHoldbackStatus::kAllowed);
// Check that we do the correct transition before setting
// `triggering_outcome_`.
DCHECKTriggeringOutcomeTransitions(/*old_state=*/triggering_outcome_,
/*new_state=*/triggering_outcome);
triggering_outcome_ = triggering_outcome;
// Set the ready time, if this attempt was not already ready.
switch (triggering_outcome_) {
// Currently only Prefetch, Prerender and NoStatePrefetch have a ready
// state. Other preloading features do not track the entire progress of the
// preloading attempt, where
// `PreloadingTriggeringOutcome::kTriggeredButOutcomeUnknown` is set for
// those other features.
case PreloadingTriggeringOutcome::kReady:
CheckReadyOutcomePreloadingType(preloading_type_);
if (!ready_time_) {
ready_time_ = elapsed_timer_.Elapsed();
}
break;
default:
break;
}
}
void PreloadingAttemptImpl::SetFailureReason(PreloadingFailureReason reason) {
// Ensure that the failure reason is only set once and is only set for
// eligible and non-holdback attempts.
CHECK_EQ(eligibility_, PreloadingEligibility::kEligible);
CHECK_EQ(holdback_status_, PreloadingHoldbackStatus::kAllowed);
CHECK_EQ(failure_reason_, PreloadingFailureReason::kUnspecified);
CHECK_NE(reason, PreloadingFailureReason::kUnspecified);
// It could be possible that the TriggeringOutcome is already kFailure, when
// we try to set FailureReason after setting TriggeringOutcome to kFailure.
if (triggering_outcome_ != PreloadingTriggeringOutcome::kFailure)
SetTriggeringOutcome(PreloadingTriggeringOutcome::kFailure);
failure_reason_ = reason;
}
base::WeakPtr<PreloadingAttempt> PreloadingAttemptImpl::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
PreloadingAttemptImpl::PreloadingAttemptImpl(
const PreloadingPredictor& creating_predictor,
const PreloadingPredictor& enacting_predictor,
PreloadingType preloading_type,
ukm::SourceId triggered_primary_page_source_id,
PreloadingURLMatchCallback url_match_predicate,
uint32_t sampling_seed)
: creating_predictor_(creating_predictor),
enacting_predictor_(enacting_predictor),
preloading_type_(preloading_type),
triggered_primary_page_source_id_(triggered_primary_page_source_id),
url_match_predicate_(std::move(url_match_predicate)),
sampling_seed_(sampling_seed) {}
PreloadingAttemptImpl::~PreloadingAttemptImpl() = default;
std::vector<PreloadingPredictor> PreloadingAttemptImpl::GetPredictors() const {
if (creating_predictor_ == enacting_predictor_) {
return {creating_predictor_};
}
return {creating_predictor_, enacting_predictor_};
}
void PreloadingAttemptImpl::RecordPreloadingAttemptMetrics(
ukm::SourceId navigated_page_source_id) {
ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get();
// Ensure that when the `triggering_outcome_` is kSuccess, then the
// accurate_triggering should be true.
if (triggering_outcome_ == PreloadingTriggeringOutcome::kSuccess) {
// TODO(crbug.com/40263357): Fix PreloadingAttempt for Prefetching in
// a different WebContents. It is allowed to activate a prefetched result in
// another WebContents instance, and the WebContents that stores `this`
// instance does not have the opportunity to set the
// `is_accurate_triggering_` flag to true in this case.
if (preloading_type_ != PreloadingType::kPrefetch) {
CHECK(is_accurate_triggering_)
<< "TriggeringOutcome set to kSuccess without correct prediction\n";
}
}
// Always record UMA, regardless of sampling.
RecordPreloadingAttemptUMA();
// Check if the preloading attempt is sampled in.
// We prefer to use the UKM source ID of the triggering page for determining
// sampling, so that all preloading attempts from a given (preloading_type,
// predictor) for the same page are included (or not) together. If there is
// no source for the triggering page, fallback to the navigated-to page.
ukm::SourceId sampling_source = triggered_primary_page_source_id_;
if (sampling_source == ukm::kInvalidSourceId) {
sampling_source = navigated_page_source_id;
}
if (sampling_source == ukm::kInvalidSourceId) {
// There is no valid UKM source, so there is nothing to log.
return;
}
PreloadingConfig& config = PreloadingConfig::GetInstance();
for (const auto& predictor : GetPredictors()) {
uint32_t sampled_num = sampling_seed_;
sampled_num =
base::Crc32(sampled_num, base::byte_span_from_ref(sampling_source));
double sampling_likelihood =
config.SamplingLikelihood(preloading_type_, predictor);
if (sampled_num >
sampling_likelihood * std::numeric_limits<uint32_t>::max()) {
// PreloadingAttempt is sampled out.
continue;
}
// Turn sampling_likelihood into an int64_t for UKM logging. Multiply by one
// million to preserve accuracy.
int64_t sampling_likelihood_per_million =
static_cast<int64_t>(1'000'000 * sampling_likelihood);
if (navigated_page_source_id != ukm::kInvalidSourceId) {
ukm::builders::Preloading_Attempt builder(navigated_page_source_id);
builder.SetPreloadingType(static_cast<int64_t>(preloading_type_))
.SetPreloadingPredictor(predictor.ukm_value())
.SetEligibility(static_cast<int64_t>(eligibility_))
.SetHoldbackStatus(static_cast<int64_t>(holdback_status_))
.SetTriggeringOutcome(static_cast<int64_t>(triggering_outcome_))
.SetFailureReason(static_cast<int64_t>(failure_reason_))
.SetAccurateTriggering(is_accurate_triggering_)
.SetSamplingLikelihood(sampling_likelihood_per_million);
if (time_to_next_navigation_) {
builder.SetTimeToNextNavigation(
ukm::GetExponentialBucketMinForCounts1000(
time_to_next_navigation_->InMilliseconds()));
}
if (ready_time_) {
builder.SetReadyTime(ukm::GetExponentialBucketMinForCounts1000(
ready_time_->InMilliseconds()));
}
if (eagerness_) {
builder.SetSpeculationEagerness(
static_cast<int64_t>(eagerness_.value()));
}
if (service_worker_registered_check_) {
builder.SetPrefetchServiceWorkerRegisteredCheck(
static_cast<int64_t>(service_worker_registered_check_.value()));
}
if (service_worker_registered_check_duration_) {
builder.SetPrefetchServiceWorkerRegisteredForURLCheckDuration(
ukm::GetExponentialBucketMin(
service_worker_registered_check_duration_.value()
.InMicroseconds(),
kServiceWorkerRegisteredCheckDurationBucketSpacing));
}
builder.Record(ukm_recorder);
}
if (triggered_primary_page_source_id_ != ukm::kInvalidSourceId) {
ukm::builders::Preloading_Attempt_PreviousPrimaryPage builder(
triggered_primary_page_source_id_);
builder.SetPreloadingType(static_cast<int64_t>(preloading_type_))
.SetPreloadingPredictor(predictor.ukm_value())
.SetEligibility(static_cast<int64_t>(eligibility_))
.SetHoldbackStatus(static_cast<int64_t>(holdback_status_))
.SetTriggeringOutcome(static_cast<int64_t>(triggering_outcome_))
.SetFailureReason(static_cast<int64_t>(failure_reason_))
.SetAccurateTriggering(is_accurate_triggering_)
.SetSamplingLikelihood(sampling_likelihood_per_million);
if (time_to_next_navigation_) {
builder.SetTimeToNextNavigation(
ukm::GetExponentialBucketMinForCounts1000(
time_to_next_navigation_->InMilliseconds()));
}
if (ready_time_) {
builder.SetReadyTime(ukm::GetExponentialBucketMinForCounts1000(
ready_time_->InMilliseconds()));
}
if (eagerness_) {
builder.SetSpeculationEagerness(
static_cast<int64_t>(eagerness_.value()));
}
if (service_worker_registered_check_) {
builder.SetPrefetchServiceWorkerRegisteredCheck(
static_cast<int64_t>(service_worker_registered_check_.value()));
}
if (service_worker_registered_check_duration_) {
builder.SetPrefetchServiceWorkerRegisteredForURLCheckDuration(
ukm::GetExponentialBucketMin(
service_worker_registered_check_duration_.value()
.InMicroseconds(),
kServiceWorkerRegisteredCheckDurationBucketSpacing));
}
builder.Record(ukm_recorder);
}
}
}
void PreloadingAttemptImpl::RecordPreloadingAttemptUMA() {
// Records the triggering outcome enum. This can be used to:
// 1. Track the number of attempts;
// 2. Track the attempts' rates of various terminal status (i.e. success
// rate).
for (const auto& predictor : GetPredictors()) {
const auto uma_triggering_outcome_histogram =
base::StrCat({"Preloading.", PreloadingTypeToString(preloading_type_),
".Attempt.", predictor.name(), ".TriggeringOutcome"});
base::UmaHistogramEnumeration(std::move(uma_triggering_outcome_histogram),
triggering_outcome_);
}
}
void PreloadingAttemptImpl::SetNoVarySearchMatchPredicate(
PreloadingURLMatchCallback no_vary_search_match_predicate) {
CHECK(!no_vary_search_match_predicate_);
no_vary_search_match_predicate_ = std::move(no_vary_search_match_predicate);
}
void PreloadingAttemptImpl::SetIsAccurateTriggering(const GURL& navigated_url) {
CHECK(url_match_predicate_);
// `PreloadingAttemptImpl::SetIsAccurateTriggering` is called during
// `WCO::DidStartNavigation`.
if (!time_to_next_navigation_) {
time_to_next_navigation_ = elapsed_timer_.Elapsed();
}
// Use the predicate to match the URLs as the matching logic varies for each
// predictor.
is_accurate_triggering_ |= url_match_predicate_.Run(navigated_url);
if (no_vary_search_match_predicate_) {
is_accurate_triggering_ |=
no_vary_search_match_predicate_.Run(navigated_url);
}
}
void PreloadingAttemptImpl::SetSpeculationEagerness(
blink::mojom::SpeculationEagerness eagerness) {
CHECK(creating_predictor_ ==
content_preloading_predictor::kSpeculationRules ||
creating_predictor_ ==
content_preloading_predictor::kSpeculationRulesFromIsolatedWorld ||
creating_predictor_ == content_preloading_predictor::
kSpeculationRulesFromAutoSpeculationRules)
<< "predictor_type_: " << creating_predictor_.name()
<< " (ukm_value = " << creating_predictor_.ukm_value() << ")";
eagerness_ = eagerness;
}
void PreloadingAttemptImpl::SetServiceWorkerRegisteredCheck(
ServiceWorkerRegisteredCheck check) {
service_worker_registered_check_ = check;
}
void PreloadingAttemptImpl::SetServiceWorkerRegisteredCheckDuration(
base::TimeDelta duration) {
service_worker_registered_check_duration_ = duration;
}
// Used for StateTransitions matching.
std::ostream& operator<<(std::ostream& os,
const PreloadingTriggeringOutcome& outcome) {
switch (outcome) {
case PreloadingTriggeringOutcome::kUnspecified:
os << "Unspecified";
break;
case PreloadingTriggeringOutcome::kDuplicate:
os << "Duplicate";
break;
case PreloadingTriggeringOutcome::kRunning:
os << "Running";
break;
case PreloadingTriggeringOutcome::kReady:
os << "Ready";
break;
case PreloadingTriggeringOutcome::kSuccess:
os << "Success";
break;
case PreloadingTriggeringOutcome::kFailure:
os << "Failure";
break;
case PreloadingTriggeringOutcome::kTriggeredButOutcomeUnknown:
os << "TriggeredButOutcomeUnknown";
break;
case PreloadingTriggeringOutcome::kTriggeredButUpgradedToPrerender:
os << "TriggeredButUpgradedToPrerender";
break;
case PreloadingTriggeringOutcome::kTriggeredButPending:
os << "TriggeredButPending";
break;
case PreloadingTriggeringOutcome::kNoOp:
os << "NoOp";
break;
}
return os;
}
} // namespace content