| // 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 |