blob: 8102accbd31fbf11cb274518e01b6e018665e68b [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/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"
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::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}},
}));
DCHECK_STATE_TRANSITION(allowed_transitions,
/*old_state=*/old_state,
/*new_state=*/new_state);
#endif // DCHECK_IS_ON()
}
} // 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.
DCHECK_EQ(eligibility_, PreloadingEligibility::kUnspecified);
DCHECK_EQ(holdback_status_, PreloadingHoldbackStatus::kUnspecified);
DCHECK_EQ(triggering_outcome_, PreloadingTriggeringOutcome::kUnspecified);
DCHECK_NE(eligibility, PreloadingEligibility::kUnspecified);
eligibility_ = eligibility;
}
// TODO(crbug.com/1383267): remove this once we've switched to the
// PreloadingConfig feature. The holdback status should be determined by the
// preloading configuration only.
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.
DCHECK_EQ(eligibility_, PreloadingEligibility::kEligible);
DCHECK_EQ(holdback_status_, PreloadingHoldbackStatus::kUnspecified);
DCHECK_EQ(triggering_outcome_, PreloadingTriggeringOutcome::kUnspecified);
DCHECK_NE(holdback_status, PreloadingHoldbackStatus::kUnspecified);
holdback_status_ = holdback_status;
}
bool PreloadingAttemptImpl::ShouldHoldback() {
DCHECK_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 = PreloadingConfig::GetInstance().ShouldHoldback(
preloading_type_, predictor_type_);
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.
DCHECK_EQ(eligibility_, PreloadingEligibility::kEligible);
DCHECK_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:
DCHECK(preloading_type_ == PreloadingType::kPrefetch ||
preloading_type_ == PreloadingType::kPrerender ||
preloading_type_ == PreloadingType::kNoStatePrefetch);
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.
DCHECK_EQ(eligibility_, PreloadingEligibility::kEligible);
DCHECK_EQ(holdback_status_, PreloadingHoldbackStatus::kAllowed);
DCHECK_EQ(failure_reason_, PreloadingFailureReason::kUnspecified);
DCHECK_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(
PreloadingPredictor predictor,
PreloadingType preloading_type,
ukm::SourceId triggered_primary_page_source_id,
base::RepeatingCallback<bool(const GURL&)> url_match_predicate,
uint32_t sampling_seed)
: predictor_type_(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;
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) {
DCHECK(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 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();
uint32_t sampled_num = sampling_seed_;
sampled_num =
base::Crc32(sampled_num, &sampling_source, sizeof(sampling_source));
double sampling_likelihood =
config.SamplingLikelihood(preloading_type_, predictor_type_);
if (sampled_num >
sampling_likelihood * std::numeric_limits<uint32_t>::max()) {
// PreloadingAttempt is sampled out.
return;
}
// 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_type_.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()));
}
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_type_.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()));
}
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).
const auto uma_triggering_outcome_histogram =
base::StrCat({"Preloading.", PreloadingTypeToString(preloading_type_),
".Attempt.", predictor_type_.name(), ".TriggeringOutcome"});
base::UmaHistogramEnumeration(std::move(uma_triggering_outcome_histogram),
triggering_outcome_);
}
void PreloadingAttemptImpl::SetIsAccurateTriggering(const GURL& navigated_url) {
DCHECK(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);
}
// 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;
}
return os;
}
} // namespace content