| // Copyright 2021 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/optimization_guide/core/hints_manager.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/histogram_macros_local.h" |
| #include "base/notreached.h" |
| #include "base/rand_util.h" |
| #include "base/task/post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_runner_util.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/default_clock.h" |
| #include "build/build_config.h" |
| #include "components/optimization_guide/core/bloom_filter.h" |
| #include "components/optimization_guide/core/hint_cache.h" |
| #include "components/optimization_guide/core/hints_component_util.h" |
| #include "components/optimization_guide/core/hints_fetcher_factory.h" |
| #include "components/optimization_guide/core/hints_processing_util.h" |
| #include "components/optimization_guide/core/insertion_ordered_set.h" |
| #include "components/optimization_guide/core/optimization_filter.h" |
| #include "components/optimization_guide/core/optimization_guide_constants.h" |
| #include "components/optimization_guide/core/optimization_guide_enums.h" |
| #include "components/optimization_guide/core/optimization_guide_features.h" |
| #include "components/optimization_guide/core/optimization_guide_navigation_data.h" |
| #include "components/optimization_guide/core/optimization_guide_permissions_util.h" |
| #include "components/optimization_guide/core/optimization_guide_prefs.h" |
| #include "components/optimization_guide/core/optimization_guide_store.h" |
| #include "components/optimization_guide/core/optimization_guide_switches.h" |
| #include "components/optimization_guide/core/optimization_guide_util.h" |
| #include "components/optimization_guide/core/optimization_hints_component_update_listener.h" |
| #include "components/optimization_guide/core/optimization_metadata.h" |
| #include "components/optimization_guide/core/tab_url_provider.h" |
| #include "components/optimization_guide/core/top_host_provider.h" |
| #include "components/optimization_guide/proto/models.pb.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "services/metrics/public/cpp/ukm_source.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "services/network/public/cpp/network_connection_tracker.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| |
| namespace optimization_guide { |
| |
| namespace { |
| |
| // The component version used with a manual config. This ensures that any hint |
| // component received from the Optimization Hints component on a subsequent |
| // startup will have a newer version than it. |
| constexpr char kManualConfigComponentVersion[] = "0.0.0"; |
| |
| // Provides a random time delta in seconds between |kFetchRandomMinDelay| and |
| // |kFetchRandomMaxDelay|. |
| base::TimeDelta RandomFetchDelay() { |
| return base::Seconds( |
| base::RandInt(features::ActiveTabsHintsFetchRandomMinDelaySecs(), |
| features::ActiveTabsHintsFetchRandomMaxDelaySecs())); |
| } |
| |
| void MaybeRunUpdateClosure(base::OnceClosure update_closure) { |
| if (update_closure) |
| std::move(update_closure).Run(); |
| } |
| |
| // Returns whether the particular component version can be processed, and if it |
| // can be, locks the semaphore (in the form of a pref) to signal that the |
| // processing of this particular version has started. |
| bool CanProcessComponentVersion(PrefService* pref_service, |
| const base::Version& version, |
| ProcessHintsComponentResult* out_result) { |
| DCHECK(version.IsValid()); |
| DCHECK(out_result); |
| |
| const std::string previous_attempted_version_string = |
| pref_service->GetString(prefs::kPendingHintsProcessingVersion); |
| if (!previous_attempted_version_string.empty()) { |
| const base::Version previous_attempted_version = |
| base::Version(previous_attempted_version_string); |
| if (!previous_attempted_version.IsValid()) { |
| DLOG(ERROR) << "Bad contents in hints processing pref"; |
| // Clear pref for fresh start next time. |
| pref_service->ClearPref(prefs::kPendingHintsProcessingVersion); |
| *out_result = |
| ProcessHintsComponentResult::kFailedPreviouslyAttemptedVersionInvalid; |
| return false; |
| } |
| if (previous_attempted_version.CompareTo(version) == 0) { |
| *out_result = ProcessHintsComponentResult::kFailedFinishProcessing; |
| // Previously attempted same version without completion. |
| return false; |
| } |
| } |
| |
| // Write config version to pref. |
| pref_service->SetString(prefs::kPendingHintsProcessingVersion, |
| version.GetString()); |
| return true; |
| } |
| |
| // Returns whether |optimization_type| is allowlisted by |optimizations|. If |
| // it is allowlisted, this will return true and |optimization_metadata| will be |
| // populated with the metadata provided by the hint, if applicable. If |
| // |page_hint| is not provided or |optimization_type| is not allowlisted, this |
| // will return false. |
| bool IsOptimizationTypeAllowed(const google::protobuf::RepeatedPtrField< |
| proto::Optimization>& optimizations, |
| proto::OptimizationType optimization_type, |
| OptimizationMetadata* optimization_metadata) { |
| for (const auto& optimization : optimizations) { |
| if (optimization_type != optimization.optimization_type()) |
| continue; |
| |
| // We found an optimization that can be applied. Populate optimization |
| // metadata if applicable and return. |
| if (optimization_metadata) { |
| switch (optimization.metadata_case()) { |
| case proto::Optimization::kPerformanceHintsMetadata: |
| optimization_metadata->set_performance_hints_metadata( |
| optimization.performance_hints_metadata()); |
| break; |
| case proto::Optimization::kPublicImageMetadata: |
| optimization_metadata->set_public_image_metadata( |
| optimization.public_image_metadata()); |
| break; |
| case proto::Optimization::kLoadingPredictorMetadata: |
| optimization_metadata->set_loading_predictor_metadata( |
| optimization.loading_predictor_metadata()); |
| break; |
| case proto::Optimization::kAnyMetadata: |
| optimization_metadata->set_any_metadata(optimization.any_metadata()); |
| break; |
| case proto::Optimization::METADATA_NOT_SET: |
| // Some optimization types do not have metadata, make sure we do not |
| // DCHECK. |
| break; |
| } |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // Util class for recording whether a hints fetch race against the current |
| // navigation was attempted. The result is recorded when it goes out of scope |
| // and its destructor is called. |
| class ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder { |
| public: |
| explicit ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder( |
| OptimizationGuideNavigationData* navigation_data) |
| : race_attempt_status_(RaceNavigationFetchAttemptStatus::kUnknown), |
| navigation_data_(navigation_data) {} |
| |
| ~ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder() { |
| DCHECK_NE(race_attempt_status_, RaceNavigationFetchAttemptStatus::kUnknown); |
| DCHECK_NE( |
| race_attempt_status_, |
| RaceNavigationFetchAttemptStatus:: |
| kDeprecatedRaceNavigationFetchNotAttemptedTooManyConcurrentFetches); |
| base::UmaHistogramEnumeration( |
| "OptimizationGuide.HintsManager.RaceNavigationFetchAttemptStatus", |
| race_attempt_status_); |
| if (navigation_data_) |
| navigation_data_->set_hints_fetch_attempt_status(race_attempt_status_); |
| } |
| |
| void set_race_attempt_status( |
| RaceNavigationFetchAttemptStatus race_attempt_status) { |
| race_attempt_status_ = race_attempt_status; |
| } |
| |
| private: |
| RaceNavigationFetchAttemptStatus race_attempt_status_; |
| raw_ptr<OptimizationGuideNavigationData> navigation_data_; |
| }; |
| |
| // Returns true if the optimization type should be ignored when is newly |
| // registered as the optimization type is likely launched. |
| bool ShouldIgnoreNewlyRegisteredOptimizationType( |
| proto::OptimizationType optimization_type) { |
| switch (optimization_type) { |
| case proto::NOSCRIPT: |
| case proto::RESOURCE_LOADING: |
| case proto::LITE_PAGE_REDIRECT: |
| case proto::DEFER_ALL_SCRIPT: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| class ScopedCanApplyOptimizationLogger { |
| public: |
| ScopedCanApplyOptimizationLogger(proto::OptimizationType opt_type, GURL url) |
| : decision_(OptimizationGuideDecision::kUnknown), |
| type_decision_(OptimizationTypeDecision::kUnknown), |
| opt_type_(opt_type), |
| has_metadata_(false), |
| url_(url) {} |
| |
| ~ScopedCanApplyOptimizationLogger() { |
| if (!switches::IsDebugLogsEnabled()) |
| return; |
| DCHECK_NE(type_decision_, OptimizationTypeDecision::kUnknown); |
| DVLOG(0) << "OptimizationGuide: CanApplyOptimization: " |
| << GetStringNameForOptimizationType(opt_type_) |
| << "\nqueried on: " << url_ << "\nDecision: " |
| << GetStringForOptimizationGuideDecision(decision_) |
| << "\nTypeDecision: " << static_cast<int>(type_decision_) |
| << "\nHas Metadata: " << has_metadata_; |
| } |
| |
| void set_has_metadata() { has_metadata_ = true; } |
| |
| void set_type_decision(OptimizationTypeDecision type_decision) { |
| type_decision_ = type_decision; |
| decision_ = |
| HintsManager::GetOptimizationGuideDecisionFromOptimizationTypeDecision( |
| type_decision_); |
| } |
| |
| private: |
| OptimizationGuideDecision decision_; |
| OptimizationTypeDecision type_decision_; |
| proto::OptimizationType opt_type_; |
| bool has_metadata_; |
| GURL url_; |
| }; |
| |
| // Reads component file and parses it into a Configuration proto. Should not be |
| // called on the UI thread. |
| std::unique_ptr<proto::Configuration> ReadComponentFile( |
| const HintsComponentInfo& info) { |
| ProcessHintsComponentResult out_result; |
| std::unique_ptr<proto::Configuration> config = |
| ProcessHintsComponent(info, &out_result); |
| if (!config) { |
| RecordProcessHintsComponentResult(out_result); |
| return nullptr; |
| } |
| |
| // Do not record the process hints component result for success cases until |
| // we processed all of the hints and filters in it. |
| return config; |
| } |
| |
| // Logs information that will be requested from the remote Optimization Guide |
| // service. |
| void MaybeLogGetHintRequestInfo( |
| proto::RequestContext request_context, |
| const base::flat_set<proto::OptimizationType>& |
| registered_optimization_types, |
| const std::vector<GURL>& urls_to_fetch, |
| const std::vector<std::string>& hosts_to_fetch) { |
| if (!switches::IsDebugLogsEnabled()) |
| return; |
| |
| DVLOG(0) << "OptimizationGuide: Starting fetch for request context " |
| << proto::RequestContext_Name(request_context); |
| DVLOG(0) << "OptimizationGuide: Registered Optimization Types: "; |
| for (const auto& optimization_type : registered_optimization_types) { |
| DVLOG(0) << "OptimizationGuide: Optimization Type: " |
| << proto::OptimizationType_Name(optimization_type); |
| } |
| DVLOG(0) << "OptimizationGuide: URLs and Hosts: "; |
| for (const auto& url : urls_to_fetch) { |
| DVLOG(0) << "OptimizationGuide: URL: " << url; |
| } |
| for (const auto& host : hosts_to_fetch) { |
| DVLOG(0) << "OptimizationGuide: Host: " << host; |
| } |
| } |
| |
| } // namespace |
| |
| HintsManager::HintsManager( |
| bool is_off_the_record, |
| const std::string& application_locale, |
| PrefService* pref_service, |
| base::WeakPtr<OptimizationGuideStore> hint_store, |
| TopHostProvider* top_host_provider, |
| TabUrlProvider* tab_url_provider, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| network::NetworkConnectionTracker* network_connection_tracker, |
| std::unique_ptr<PushNotificationManager> push_notification_manager) |
| : is_off_the_record_(is_off_the_record), |
| application_locale_(application_locale), |
| pref_service_(pref_service), |
| hint_cache_( |
| std::make_unique<HintCache>(hint_store, |
| features::MaxHostKeyedHintCacheSize())), |
| batch_update_hints_fetchers_(features::MaxConcurrentBatchUpdateFetches()), |
| page_navigation_hints_fetchers_( |
| features::MaxConcurrentPageNavigationFetches()), |
| hints_fetcher_factory_(std::make_unique<HintsFetcherFactory>( |
| url_loader_factory, |
| features::GetOptimizationGuideServiceGetHintsURL(), |
| pref_service, |
| network_connection_tracker)), |
| top_host_provider_(top_host_provider), |
| tab_url_provider_(tab_url_provider), |
| push_notification_manager_(std::move(push_notification_manager)), |
| clock_(base::DefaultClock::GetInstance()), |
| background_task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT})) { |
| if (push_notification_manager_) |
| push_notification_manager_->SetDelegate(this); |
| |
| hint_cache_->Initialize( |
| switches::ShouldPurgeOptimizationGuideStoreOnStartup(), |
| base::BindOnce(&HintsManager::OnHintCacheInitialized, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| HintsManager::~HintsManager() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| void HintsManager::Shutdown() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| OptimizationHintsComponentUpdateListener::GetInstance()->RemoveObserver(this); |
| |
| base::UmaHistogramBoolean("OptimizationGuide.ProcessingComponentAtShutdown", |
| is_processing_component_); |
| if (is_processing_component_) { |
| // If we are currently processing the component and we are asked to shut |
| // down, we should clear the pref since the function to clear the pref will |
| // not run after shut down and we will think that we failed to process the |
| // component due to a crash. |
| pref_service_->ClearPref(prefs::kPendingHintsProcessingVersion); |
| } |
| } |
| |
| // static |
| OptimizationGuideDecision |
| HintsManager::GetOptimizationGuideDecisionFromOptimizationTypeDecision( |
| OptimizationTypeDecision optimization_type_decision) { |
| switch (optimization_type_decision) { |
| case OptimizationTypeDecision::kAllowedByOptimizationFilter: |
| case OptimizationTypeDecision::kAllowedByHint: |
| return OptimizationGuideDecision::kTrue; |
| case OptimizationTypeDecision::kUnknown: |
| case OptimizationTypeDecision::kHadOptimizationFilterButNotLoadedInTime: |
| case OptimizationTypeDecision::kHadHintButNotLoadedInTime: |
| case OptimizationTypeDecision::kHintFetchStartedButNotAvailableInTime: |
| case OptimizationTypeDecision::kDeciderNotInitialized: |
| return OptimizationGuideDecision::kUnknown; |
| case OptimizationTypeDecision::kNotAllowedByHint: |
| case OptimizationTypeDecision::kNoMatchingPageHint: |
| case OptimizationTypeDecision::kNoHintAvailable: |
| case OptimizationTypeDecision::kNotAllowedByOptimizationFilter: |
| return OptimizationGuideDecision::kFalse; |
| } |
| } |
| |
| void HintsManager::OnHintsComponentAvailable(const HintsComponentInfo& info) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Check for if hint component is disabled. This check is needed because the |
| // optimization guide still registers with the service as an observer for |
| // components as a signal during testing. |
| if (switches::IsHintComponentProcessingDisabled()) { |
| MaybeRunUpdateClosure(std::move(next_update_closure_)); |
| return; |
| } |
| |
| ProcessHintsComponentResult out_result; |
| if (!CanProcessComponentVersion(pref_service_, info.version, &out_result)) { |
| RecordProcessHintsComponentResult(out_result); |
| MaybeRunUpdateClosure(std::move(next_update_closure_)); |
| return; |
| } |
| |
| std::unique_ptr<StoreUpdateData> update_data = |
| is_off_the_record_ |
| ? nullptr |
| : hint_cache_->MaybeCreateUpdateDataForComponentHints(info.version); |
| |
| // Processes the hints from the newly available component on a background |
| // thread, providing a StoreUpdateData for component update from the hint |
| // cache, so that each hint within the component can be moved into it. In the |
| // case where the component's version is not newer than the optimization guide |
| // store's component version, StoreUpdateData will be a nullptr and hint |
| // processing will be skipped. |
| // base::Unretained(this) is safe since |this| owns |background_task_runner_| |
| // and the callback will be canceled if destroyed. |
| is_processing_component_ = true; |
| background_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&ReadComponentFile, info), |
| base::BindOnce(&HintsManager::UpdateComponentHints, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(next_update_closure_), std::move(update_data))); |
| |
| // Only replace hints component info if it is not the same - otherwise we will |
| // destruct the object and it will be invalid later. |
| if (!hints_component_info_ || |
| hints_component_info_->version.CompareTo(info.version) != 0) { |
| hints_component_info_.emplace(info.version, info.path); |
| } |
| } |
| |
| void HintsManager::ProcessOptimizationFilters( |
| const google::protobuf::RepeatedPtrField<proto::OptimizationFilter>& |
| allowlist_optimization_filters, |
| const google::protobuf::RepeatedPtrField<proto::OptimizationFilter>& |
| blocklist_optimization_filters) { |
| optimization_types_with_filter_.clear(); |
| allowlist_optimization_filters_.clear(); |
| blocklist_optimization_filters_.clear(); |
| ProcessOptimizationFilterSet(allowlist_optimization_filters, |
| /*is_allowlist=*/true); |
| ProcessOptimizationFilterSet(blocklist_optimization_filters, |
| /*is_allowlist=*/false); |
| } |
| |
| void HintsManager::ProcessOptimizationFilterSet( |
| const google::protobuf::RepeatedPtrField<proto::OptimizationFilter>& |
| filters, |
| bool is_allowlist) { |
| for (const auto& filter : filters) { |
| if (filter.optimization_type() != proto::TYPE_UNSPECIFIED) { |
| optimization_types_with_filter_.insert(filter.optimization_type()); |
| } |
| |
| // Do not put anything in memory that we don't have registered. |
| if (registered_optimization_types_.find(filter.optimization_type()) == |
| registered_optimization_types_.end()) { |
| continue; |
| } |
| |
| RecordOptimizationFilterStatus( |
| filter.optimization_type(), |
| OptimizationFilterStatus::kFoundServerFilterConfig); |
| |
| // Do not parse duplicate optimization filters. |
| if (allowlist_optimization_filters_.find(filter.optimization_type()) != |
| allowlist_optimization_filters_.end() || |
| blocklist_optimization_filters_.find(filter.optimization_type()) != |
| blocklist_optimization_filters_.end()) { |
| RecordOptimizationFilterStatus( |
| filter.optimization_type(), |
| OptimizationFilterStatus::kFailedServerFilterDuplicateConfig); |
| continue; |
| } |
| |
| // Parse optimization filter. |
| OptimizationFilterStatus status; |
| std::unique_ptr<OptimizationFilter> optimization_filter = |
| ProcessOptimizationFilter(filter, &status); |
| if (optimization_filter) { |
| if (is_allowlist) { |
| allowlist_optimization_filters_.insert( |
| {filter.optimization_type(), std::move(optimization_filter)}); |
| } else { |
| blocklist_optimization_filters_.insert( |
| {filter.optimization_type(), std::move(optimization_filter)}); |
| } |
| } |
| RecordOptimizationFilterStatus(filter.optimization_type(), status); |
| } |
| } |
| |
| void HintsManager::OnHintCacheInitialized() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (push_notification_manager_) { |
| push_notification_manager_->OnDelegateReady(); |
| } |
| |
| // Check if there is a valid hint proto given on the command line first. We |
| // don't normally expect one, but if one is provided then use that and do not |
| // register as an observer as the opt_guide service. |
| std::unique_ptr<proto::Configuration> manual_config = |
| switches::ParseComponentConfigFromCommandLine(); |
| if (manual_config) { |
| std::unique_ptr<StoreUpdateData> update_data = |
| is_off_the_record_ |
| ? nullptr |
| : hint_cache_->MaybeCreateUpdateDataForComponentHints( |
| base::Version(kManualConfigComponentVersion)); |
| // Allow |UpdateComponentHints| to block startup so that the first |
| // navigation gets the hints when a command line hint proto is provided. |
| UpdateComponentHints(base::DoNothing(), std::move(update_data), |
| std::move(manual_config)); |
| } |
| |
| // If the store is available, clear all hint state so newly registered types |
| // can have their hints immediately included in hint fetches. |
| if (hint_cache_->IsHintStoreAvailable() && should_clear_hints_for_new_type_) { |
| ClearHostKeyedHints(); |
| should_clear_hints_for_new_type_ = false; |
| } |
| |
| // Register as an observer regardless of hint proto override usage. This is |
| // needed as a signal during testing. |
| OptimizationHintsComponentUpdateListener::GetInstance()->AddObserver(this); |
| } |
| |
| void HintsManager::UpdateComponentHints( |
| base::OnceClosure update_closure, |
| std::unique_ptr<StoreUpdateData> update_data, |
| std::unique_ptr<proto::Configuration> config) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // If we get here, the component file has been processed correctly and did not |
| // crash the device. |
| is_processing_component_ = false; |
| pref_service_->ClearPref(prefs::kPendingHintsProcessingVersion); |
| |
| if (!config) { |
| MaybeRunUpdateClosure(std::move(update_closure)); |
| return; |
| } |
| |
| ProcessOptimizationFilters(config->optimization_allowlists(), |
| config->optimization_blocklists()); |
| |
| // Don't store hints in the store if it's off the record. |
| if (update_data && !is_off_the_record_) { |
| bool did_process_hints = hint_cache_->ProcessAndCacheHints( |
| config->mutable_hints(), update_data.get()); |
| RecordProcessHintsComponentResult( |
| did_process_hints ? ProcessHintsComponentResult::kSuccess |
| : ProcessHintsComponentResult::kProcessedNoHints); |
| } else { |
| RecordProcessHintsComponentResult( |
| ProcessHintsComponentResult::kSkippedProcessingHints); |
| } |
| |
| if (update_data) { |
| hint_cache_->UpdateComponentHints( |
| std::move(update_data), |
| base::BindOnce(&HintsManager::OnComponentHintsUpdated, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(update_closure), |
| /* hints_updated=*/true)); |
| } else { |
| OnComponentHintsUpdated(std::move(update_closure), /*hints_updated=*/false); |
| } |
| } |
| |
| void HintsManager::OnComponentHintsUpdated(base::OnceClosure update_closure, |
| bool hints_updated) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Record the result of updating the hints. This is used as a signal for the |
| // hints being fully processed in testing. |
| LOCAL_HISTOGRAM_BOOLEAN(kComponentHintsUpdatedResultHistogramString, |
| hints_updated); |
| |
| // Initiate the hints fetch scheduling if deferred startup handling is not |
| // enabled. Otherwise OnDeferredStartup() will iniitate it. |
| if (!features::ShouldDeferStartupActiveTabsHintsFetch()) |
| InitiateHintsFetchScheduling(); |
| MaybeRunUpdateClosure(std::move(update_closure)); |
| } |
| |
| void HintsManager::InitiateHintsFetchScheduling() { |
| if (features::ShouldBatchUpdateHintsForActiveTabsAndTopHosts()) { |
| SetLastHintsFetchAttemptTime(clock_->Now()); |
| |
| if (switches::ShouldOverrideFetchHintsTimer() || |
| features::ShouldDeferStartupActiveTabsHintsFetch()) { |
| FetchHintsForActiveTabs(); |
| } else if (!active_tabs_hints_fetch_timer_.IsRunning()) { |
| // Batch update hints with a random delay. |
| active_tabs_hints_fetch_timer_.Start( |
| FROM_HERE, RandomFetchDelay(), this, |
| &HintsManager::FetchHintsForActiveTabs); |
| } |
| } |
| } |
| |
| void HintsManager::ListenForNextUpdateForTesting( |
| base::OnceClosure next_update_closure) { |
| DCHECK(!next_update_closure_) |
| << "Only one update closure is supported at a time"; |
| next_update_closure_ = std::move(next_update_closure); |
| } |
| |
| void HintsManager::SetHintsFetcherFactoryForTesting( |
| std::unique_ptr<HintsFetcherFactory> hints_fetcher_factory) { |
| hints_fetcher_factory_ = std::move(hints_fetcher_factory); |
| } |
| |
| void HintsManager::SetClockForTesting(const base::Clock* clock) { |
| clock_ = clock; |
| } |
| |
| void HintsManager::ScheduleActiveTabsHintsFetch() { |
| DCHECK(!active_tabs_hints_fetch_timer_.IsRunning()); |
| |
| const base::TimeDelta active_tabs_refresh_duration = |
| features::GetActiveTabsFetchRefreshDuration(); |
| const base::TimeDelta time_since_last_fetch = |
| clock_->Now() - GetLastHintsFetchAttemptTime(); |
| if (time_since_last_fetch >= active_tabs_refresh_duration) { |
| // Fetched hints in the store should be updated and an attempt has not |
| // been made in last |
| // |features::GetActiveTabsFetchRefreshDuration()|. |
| SetLastHintsFetchAttemptTime(clock_->Now()); |
| active_tabs_hints_fetch_timer_.Start( |
| FROM_HERE, RandomFetchDelay(), this, |
| &HintsManager::FetchHintsForActiveTabs); |
| } else { |
| // If the fetched hints in the store are still up-to-date, set a timer |
| // for when the hints need to be updated. |
| base::TimeDelta fetcher_delay = |
| active_tabs_refresh_duration - time_since_last_fetch; |
| active_tabs_hints_fetch_timer_.Start( |
| FROM_HERE, fetcher_delay, this, |
| &HintsManager::ScheduleActiveTabsHintsFetch); |
| } |
| } |
| |
| const std::vector<GURL> HintsManager::GetActiveTabURLsToRefresh() { |
| if (!tab_url_provider_) |
| return {}; |
| |
| std::vector<GURL> active_tab_urls = tab_url_provider_->GetUrlsOfActiveTabs( |
| features::GetActiveTabsStalenessTolerance()); |
| |
| std::set<GURL> urls_to_refresh; |
| for (const auto& url : active_tab_urls) { |
| if (!IsValidURLForURLKeyedHint(url)) |
| continue; |
| |
| if (!hint_cache_->HasURLKeyedEntryForURL(url)) |
| urls_to_refresh.insert(url); |
| } |
| return std::vector<GURL>(urls_to_refresh.begin(), urls_to_refresh.end()); |
| } |
| |
| void HintsManager::FetchHintsForActiveTabs() { |
| active_tabs_hints_fetch_timer_.Stop(); |
| active_tabs_hints_fetch_timer_.Start( |
| FROM_HERE, features::GetActiveTabsFetchRefreshDuration(), this, |
| &HintsManager::ScheduleActiveTabsHintsFetch); |
| |
| if (!IsUserPermittedToFetchFromRemoteOptimizationGuide(is_off_the_record_, |
| pref_service_)) { |
| return; |
| } |
| |
| if (!HasOptimizationTypeToFetchFor()) |
| return; |
| |
| std::vector<std::string> top_hosts; |
| if (top_host_provider_) |
| top_hosts = top_host_provider_->GetTopHosts(); |
| |
| const std::vector<GURL> active_tab_urls_to_refresh = |
| GetActiveTabURLsToRefresh(); |
| |
| base::UmaHistogramCounts100( |
| "OptimizationGuide.HintsManager.ActiveTabUrlsToFetchFor", |
| active_tab_urls_to_refresh.size()); |
| |
| if (top_hosts.empty() && active_tab_urls_to_refresh.empty()) |
| return; |
| |
| // Add hosts of active tabs to list of hosts to fetch for. Since we are mainly |
| // fetching for updated information on tabs, add those to the front of the |
| // list. |
| base::flat_set<std::string> top_hosts_set = |
| base::flat_set<std::string>(top_hosts.begin(), top_hosts.end()); |
| for (const auto& url : active_tab_urls_to_refresh) { |
| if (!url.has_host() || |
| top_hosts_set.find(url.host()) == top_hosts_set.end()) { |
| continue; |
| } |
| if (!hint_cache_->HasHint(url.host())) { |
| top_hosts_set.insert(url.host()); |
| top_hosts.insert(top_hosts.begin(), url.host()); |
| } |
| } |
| MaybeLogGetHintRequestInfo(proto::CONTEXT_BATCH_UPDATE_ACTIVE_TABS, |
| registered_optimization_types_, |
| active_tab_urls_to_refresh, top_hosts); |
| |
| if (!active_tabs_batch_update_hints_fetcher_) { |
| DCHECK(hints_fetcher_factory_); |
| active_tabs_batch_update_hints_fetcher_ = |
| hints_fetcher_factory_->BuildInstance(); |
| } |
| active_tabs_batch_update_hints_fetcher_->FetchOptimizationGuideServiceHints( |
| top_hosts, active_tab_urls_to_refresh, registered_optimization_types_, |
| proto::CONTEXT_BATCH_UPDATE_ACTIVE_TABS, application_locale_, |
| base::BindOnce(&HintsManager::OnHintsForActiveTabsFetched, |
| weak_ptr_factory_.GetWeakPtr(), top_hosts_set, |
| base::flat_set<GURL>(active_tab_urls_to_refresh.begin(), |
| active_tab_urls_to_refresh.end()))); |
| } |
| |
| void HintsManager::OnHintsForActiveTabsFetched( |
| const base::flat_set<std::string>& hosts_fetched, |
| const base::flat_set<GURL>& urls_fetched, |
| absl::optional<std::unique_ptr<proto::GetHintsResponse>> |
| get_hints_response) { |
| if (!get_hints_response) |
| return; |
| |
| hint_cache_->UpdateFetchedHints( |
| std::move(*get_hints_response), |
| clock_->Now() + features::GetActiveTabsFetchRefreshDuration(), |
| hosts_fetched, urls_fetched, |
| base::BindOnce(&HintsManager::OnFetchedActiveTabsHintsStored, |
| weak_ptr_factory_.GetWeakPtr())); |
| if (switches::IsDebugLogsEnabled()) |
| DVLOG(0) << "OptimizationGuide: OnHintsForActiveTabsFetched complete"; |
| } |
| |
| void HintsManager::OnPageNavigationHintsFetched( |
| base::WeakPtr<OptimizationGuideNavigationData> navigation_data_weak_ptr, |
| const absl::optional<GURL>& navigation_url, |
| const base::flat_set<GURL>& page_navigation_urls_requested, |
| const base::flat_set<std::string>& page_navigation_hosts_requested, |
| absl::optional<std::unique_ptr<proto::GetHintsResponse>> |
| get_hints_response) { |
| if (navigation_url) { |
| CleanUpFetcherForNavigation(*navigation_url); |
| } |
| |
| if (!get_hints_response.has_value() || !get_hints_response.value()) { |
| if (navigation_url) { |
| PrepareToInvokeRegisteredCallbacks(*navigation_url); |
| } |
| return; |
| } |
| |
| hint_cache_->UpdateFetchedHints( |
| std::move(*get_hints_response), |
| clock_->Now() + features::GetActiveTabsFetchRefreshDuration(), |
| page_navigation_hosts_requested, page_navigation_urls_requested, |
| base::BindOnce(&HintsManager::OnFetchedPageNavigationHintsStored, |
| weak_ptr_factory_.GetWeakPtr(), navigation_data_weak_ptr, |
| navigation_url, page_navigation_hosts_requested)); |
| } |
| |
| void HintsManager::OnFetchedActiveTabsHintsStored() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| LOCAL_HISTOGRAM_BOOLEAN("OptimizationGuide.FetchedHints.Stored", true); |
| |
| if (!features::ShouldPersistHintsToDisk()) { |
| // If we aren't persisting hints to disk, there's no point in purging |
| // hints from disk or starting a new fetch since at this point we should |
| // just be fetching everything on page navigation and only storing |
| // in-memory. |
| return; |
| } |
| |
| hint_cache_->PurgeExpiredFetchedHints(); |
| } |
| |
| void HintsManager::OnFetchedPageNavigationHintsStored( |
| base::WeakPtr<OptimizationGuideNavigationData> navigation_data_weak_ptr, |
| const absl::optional<GURL>& navigation_url, |
| const base::flat_set<std::string>& page_navigation_hosts_requested) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (navigation_data_weak_ptr) { |
| navigation_data_weak_ptr->set_hints_fetch_end(base::TimeTicks::Now()); |
| } |
| base::UmaHistogramBoolean( |
| "OptimizationGuide.HintsManager." |
| "PageNavigationHintsReturnedBeforeDataFlushed", |
| navigation_data_weak_ptr.MaybeValid()); |
| |
| if (navigation_url) { |
| PrepareToInvokeRegisteredCallbacks(*navigation_url); |
| } |
| } |
| |
| bool HintsManager::IsHintBeingFetchedForNavigation(const GURL& navigation_url) { |
| return page_navigation_hints_fetchers_.Get(navigation_url) != |
| page_navigation_hints_fetchers_.end(); |
| } |
| |
| void HintsManager::CleanUpFetcherForNavigation(const GURL& navigation_url) { |
| auto it = page_navigation_hints_fetchers_.Peek(navigation_url); |
| if (it != page_navigation_hints_fetchers_.end()) |
| page_navigation_hints_fetchers_.Erase(it); |
| } |
| |
| base::Time HintsManager::GetLastHintsFetchAttemptTime() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds( |
| pref_service_->GetInt64(prefs::kHintsFetcherLastFetchAttempt))); |
| } |
| |
| void HintsManager::SetLastHintsFetchAttemptTime(base::Time last_attempt_time) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| pref_service_->SetInt64( |
| prefs::kHintsFetcherLastFetchAttempt, |
| last_attempt_time.ToDeltaSinceWindowsEpoch().InMicroseconds()); |
| } |
| |
| void HintsManager::LoadHintForURL(const GURL& url, base::OnceClosure callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!url.has_host()) { |
| MaybeRunUpdateClosure(std::move(callback)); |
| return; |
| } |
| |
| LoadHintForHost(url.host(), std::move(callback)); |
| } |
| |
| void HintsManager::LoadHintForHost(const std::string& host, |
| base::OnceClosure callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| hint_cache_->LoadHint(host, base::BindOnce(&HintsManager::OnHintLoaded, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(callback))); |
| } |
| |
| void HintsManager::FetchHintsForURLs(const std::vector<GURL>& urls, |
| proto::RequestContext request_context) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Collect hosts, stripping duplicates, but preserving the ordering. |
| InsertionOrderedSet<GURL> target_urls; |
| InsertionOrderedSet<std::string> target_hosts; |
| for (const auto& url : urls) { |
| target_hosts.insert(url.host()); |
| target_urls.insert(url); |
| } |
| |
| if (target_hosts.empty() && target_urls.empty()) |
| return; |
| |
| MaybeLogGetHintRequestInfo(request_context, registered_optimization_types_, |
| target_urls.vector(), target_hosts.vector()); |
| |
| std::pair<int32_t, HintsFetcher*> request_id_and_fetcher = |
| CreateAndTrackBatchUpdateHintsFetcher(); |
| |
| // Use the batch update hints fetcher for fetches off the SRP since we are |
| // not fetching for the current navigation. |
| // |
| // Caller does not expect to be notified when relevant hints have been fetched |
| // and stored. |
| request_id_and_fetcher.second->FetchOptimizationGuideServiceHints( |
| target_hosts.vector(), target_urls.vector(), |
| registered_optimization_types_, request_context, application_locale_, |
| base::BindOnce( |
| &HintsManager::OnBatchUpdateHintsFetched, |
| weak_ptr_factory_.GetWeakPtr(), request_id_and_fetcher.first, |
| request_context, target_hosts.set(), target_urls.set(), |
| target_urls.set(), registered_optimization_types_, |
| base::DoNothingAs<void( |
| const GURL&, const base::flat_map< |
| proto::OptimizationType, |
| OptimizationGuideDecisionWithMetadata>&)>())); |
| } |
| |
| void HintsManager::OnHintLoaded(base::OnceClosure callback, |
| const proto::Hint* loaded_hint) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Record the result of loading a hint. This is used as a signal for testing. |
| LOCAL_HISTOGRAM_BOOLEAN(kLoadedHintLocalHistogramString, loaded_hint); |
| |
| // Run the callback now that the hint is loaded. This is used as a signal by |
| // tests. |
| MaybeRunUpdateClosure(std::move(callback)); |
| } |
| |
| void HintsManager::RegisterOptimizationTypes( |
| const std::vector<proto::OptimizationType>& optimization_types) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| bool should_load_new_optimization_filter = false; |
| |
| DictionaryPrefUpdateDeprecated previously_registered_opt_types( |
| pref_service_, prefs::kPreviouslyRegisteredOptimizationTypes); |
| for (const auto optimization_type : optimization_types) { |
| if (optimization_type == proto::TYPE_UNSPECIFIED) |
| continue; |
| |
| if (registered_optimization_types_.find(optimization_type) != |
| registered_optimization_types_.end()) { |
| continue; |
| } |
| registered_optimization_types_.insert(optimization_type); |
| |
| if (switches::IsDebugLogsEnabled()) { |
| DVLOG(0) << "OptimizationGuide: Registered new OptimizationType: " |
| << proto::OptimizationType_Name(optimization_type); |
| } |
| |
| absl::optional<double> value = previously_registered_opt_types->FindBoolKey( |
| proto::OptimizationType_Name(optimization_type)); |
| if (!value) { |
| if (!is_off_the_record_ && |
| !ShouldIgnoreNewlyRegisteredOptimizationType(optimization_type) && |
| !base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kHintsProtoOverride)) { |
| should_clear_hints_for_new_type_ = true; |
| } |
| previously_registered_opt_types->SetBoolKey( |
| proto::OptimizationType_Name(optimization_type), true); |
| } |
| |
| if (!should_load_new_optimization_filter) { |
| if (optimization_types_with_filter_.find(optimization_type) != |
| optimization_types_with_filter_.end()) { |
| should_load_new_optimization_filter = true; |
| } |
| } |
| } |
| |
| // If the store is available, clear all hint state so newly registered types |
| // can have their hints immediately included in hint fetches. |
| if (hint_cache_->IsHintStoreAvailable() && should_clear_hints_for_new_type_) { |
| ClearHostKeyedHints(); |
| should_clear_hints_for_new_type_ = false; |
| } |
| |
| if (should_load_new_optimization_filter) { |
| if (switches::IsHintComponentProcessingDisabled()) { |
| std::unique_ptr<proto::Configuration> manual_config = |
| switches::ParseComponentConfigFromCommandLine(); |
| if (manual_config->optimization_allowlists_size() > 0 || |
| manual_config->optimization_blocklists_size() > 0) { |
| ProcessOptimizationFilters(manual_config->optimization_allowlists(), |
| manual_config->optimization_blocklists()); |
| } |
| } else { |
| DCHECK(hints_component_info_); |
| OnHintsComponentAvailable(*hints_component_info_); |
| } |
| } else { |
| MaybeRunUpdateClosure(std::move(next_update_closure_)); |
| } |
| } |
| |
| bool HintsManager::HasLoadedOptimizationAllowlist( |
| proto::OptimizationType optimization_type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return allowlist_optimization_filters_.find(optimization_type) != |
| allowlist_optimization_filters_.end(); |
| } |
| |
| bool HintsManager::HasLoadedOptimizationBlocklist( |
| proto::OptimizationType optimization_type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return blocklist_optimization_filters_.find(optimization_type) != |
| blocklist_optimization_filters_.end(); |
| } |
| |
| base::flat_map<proto::OptimizationType, OptimizationGuideDecisionWithMetadata> |
| HintsManager::GetDecisionsWithCachedInformationForURLAndOptimizationTypes( |
| const GURL& url, |
| const base::flat_set<proto::OptimizationType>& optimization_types) { |
| base::flat_map<proto::OptimizationType, OptimizationGuideDecisionWithMetadata> |
| decisions; |
| |
| for (const auto optimization_type : optimization_types) { |
| OptimizationMetadata metadata; |
| OptimizationTypeDecision type_decision = |
| CanApplyOptimization(url, optimization_type, &metadata); |
| OptimizationGuideDecision decision = |
| GetOptimizationGuideDecisionFromOptimizationTypeDecision(type_decision); |
| decisions[optimization_type] = {decision, metadata}; |
| } |
| |
| return decisions; |
| } |
| |
| void HintsManager::CanApplyOptimizationOnDemand( |
| const std::vector<GURL>& urls, |
| const base::flat_set<proto::OptimizationType>& optimization_types, |
| proto::RequestContext request_context, |
| OnDemandOptimizationGuideDecisionRepeatingCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // TODO(crbug/1275612): Check whether we have consent to fetch. |
| |
| // This set contains URLs that require some information to be fetched, whether |
| // that be a URL-keyed hint or a host-keyed hint. |
| base::flat_set<GURL> urls_with_pending_callback; |
| |
| InsertionOrderedSet<GURL> urls_to_fetch; |
| InsertionOrderedSet<std::string> hosts_to_fetch; |
| for (const auto& url : urls) { |
| if (!hint_cache_->HasURLKeyedEntryForURL(url)) { |
| urls_to_fetch.insert(url); |
| urls_with_pending_callback.insert(url); |
| } |
| // We check for the hint being loaded mostly for code simplicity. If we just |
| // check for the presence in the cache and the hint wasn't loaded, we need |
| // to run the load callback and then invoke the callbacks. However, as the |
| // fetch will just ignore the host if cached, this is ok since the callback |
| // from the fetch will initiate the loading of the hint and the resulting |
| // callback to be run. |
| if (!hint_cache_->HasHint(url.host()) || |
| (hint_cache_->GetHostKeyedHintIfLoaded(url.host()) == nullptr)) { |
| hosts_to_fetch.insert(url.host()); |
| urls_with_pending_callback.insert(url); |
| } |
| |
| if (!urls_with_pending_callback.contains(url)) { |
| // Only get decisions if we know we do not need to fetch for anything. |
| base::flat_map<proto::OptimizationType, |
| OptimizationGuideDecisionWithMetadata> |
| decisions = |
| GetDecisionsWithCachedInformationForURLAndOptimizationTypes( |
| url, optimization_types); |
| callback.Run(url, decisions); |
| } |
| } |
| |
| if (urls_with_pending_callback.empty()) { |
| // Nothing to fetch for. |
| return; |
| } |
| |
| MaybeLogGetHintRequestInfo(request_context, registered_optimization_types_, |
| urls_to_fetch.vector(), hosts_to_fetch.vector()); |
| |
| // Fetch the data for the entries we don't have all information for. |
| std::pair<int32_t, HintsFetcher*> request_id_and_fetcher = |
| CreateAndTrackBatchUpdateHintsFetcher(); |
| request_id_and_fetcher.second->FetchOptimizationGuideServiceHints( |
| hosts_to_fetch.vector(), urls_to_fetch.vector(), |
| registered_optimization_types_, request_context, application_locale_, |
| base::BindOnce(&HintsManager::OnBatchUpdateHintsFetched, |
| weak_ptr_factory_.GetWeakPtr(), |
| request_id_and_fetcher.first, request_context, |
| hosts_to_fetch.set(), urls_to_fetch.set(), |
| urls_with_pending_callback, optimization_types, callback)); |
| } |
| |
| void HintsManager::OnBatchUpdateHintsFetched( |
| int32_t request_id, |
| proto::RequestContext request_context, |
| const base::flat_set<std::string>& hosts_requested, |
| const base::flat_set<GURL>& urls_requested, |
| const base::flat_set<GURL>& urls_with_pending_callback, |
| const base::flat_set<proto::OptimizationType>& optimization_types, |
| OnDemandOptimizationGuideDecisionRepeatingCallback callback, |
| absl::optional<std::unique_ptr<proto::GetHintsResponse>> |
| get_hints_response) { |
| CleanUpBatchUpdateHintsFetcher(request_id); |
| |
| if (!get_hints_response.has_value() || !get_hints_response.value()) { |
| OnBatchUpdateHintsStored(urls_with_pending_callback, optimization_types, |
| callback); |
| return; |
| } |
| |
| // TODO(crbug/1278015): Figure out if the update time duration is the right |
| // one. |
| hint_cache_->UpdateFetchedHints( |
| std::move(*get_hints_response), |
| clock_->Now() + features::GetActiveTabsFetchRefreshDuration(), |
| hosts_requested, urls_requested, |
| base::BindOnce(&HintsManager::OnBatchUpdateHintsStored, |
| weak_ptr_factory_.GetWeakPtr(), urls_with_pending_callback, |
| optimization_types, callback)); |
| |
| if (switches::IsDebugLogsEnabled()) { |
| DVLOG(0) << "OptimizationGuide: OnBatchUpdateHintsFetched for " |
| << proto::RequestContext_Name(request_context) << " complete"; |
| } |
| } |
| |
| void HintsManager::OnBatchUpdateHintsStored( |
| const base::flat_set<GURL>& urls, |
| const base::flat_set<proto::OptimizationType>& optimization_types, |
| OnDemandOptimizationGuideDecisionRepeatingCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| for (const auto& url : urls) { |
| // Load the hint for host if we have a host-keyed hint before invoking the |
| // callbacks so we have all the necessary information to make the decision. |
| LoadHintForHost( |
| url.host(), |
| base::BindOnce(&HintsManager::InvokeOnDemandHintsCallbackForURL, |
| weak_ptr_factory_.GetWeakPtr(), url, optimization_types, |
| callback)); |
| } |
| } |
| |
| std::pair<int32_t, HintsFetcher*> |
| HintsManager::CreateAndTrackBatchUpdateHintsFetcher() { |
| DCHECK(hints_fetcher_factory_); |
| std::unique_ptr<HintsFetcher> hints_fetcher = |
| hints_fetcher_factory_->BuildInstance(); |
| HintsFetcher* hints_fetcher_ptr = hints_fetcher.get(); |
| batch_update_hints_fetchers_.Put(batch_update_hints_fetcher_request_id_++, |
| std::move(hints_fetcher)); |
| UMA_HISTOGRAM_COUNTS_100( |
| "OptimizationGuide.HintsManager.ConcurrentBatchUpdateFetches", |
| batch_update_hints_fetchers_.size()); |
| return std::make_pair(batch_update_hints_fetcher_request_id_, |
| hints_fetcher_ptr); |
| } |
| |
| void HintsManager::CleanUpBatchUpdateHintsFetcher(int32_t request_id) { |
| auto it = batch_update_hints_fetchers_.Peek(request_id); |
| if (it != batch_update_hints_fetchers_.end()) |
| batch_update_hints_fetchers_.Erase(it); |
| } |
| |
| void HintsManager::InvokeOnDemandHintsCallbackForURL( |
| const GURL& url, |
| const base::flat_set<proto::OptimizationType>& optimization_types, |
| OnDemandOptimizationGuideDecisionRepeatingCallback callback) { |
| base::flat_map<proto::OptimizationType, OptimizationGuideDecisionWithMetadata> |
| decisions = GetDecisionsWithCachedInformationForURLAndOptimizationTypes( |
| url, optimization_types); |
| callback.Run(url, decisions); |
| } |
| |
| void HintsManager::CanApplyOptimizationAsync( |
| const GURL& navigation_url, |
| proto::OptimizationType optimization_type, |
| OptimizationGuideDecisionCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| OptimizationMetadata metadata; |
| OptimizationTypeDecision type_decision = |
| CanApplyOptimization(navigation_url, optimization_type, &metadata); |
| OptimizationGuideDecision decision = |
| GetOptimizationGuideDecisionFromOptimizationTypeDecision(type_decision); |
| // It's possible that a hint that applies to |navigation_url| will come in |
| // later, so only run the callback if we are sure we can apply the decision. |
| if (decision == OptimizationGuideDecision::kTrue || |
| HasAllInformationForDecisionAvailable(navigation_url, |
| optimization_type)) { |
| base::UmaHistogramEnumeration( |
| "OptimizationGuide.ApplyDecisionAsync." + |
| GetStringNameForOptimizationType(optimization_type), |
| type_decision); |
| std::move(callback).Run(decision, metadata); |
| return; |
| } |
| |
| registered_callbacks_[navigation_url][optimization_type].push_back( |
| std::move(callback)); |
| } |
| |
| OptimizationTypeDecision HintsManager::CanApplyOptimization( |
| const GURL& navigation_url, |
| proto::OptimizationType optimization_type, |
| OptimizationMetadata* optimization_metadata) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| ScopedCanApplyOptimizationLogger scoped_logger(optimization_type, |
| navigation_url); |
| // Clear out optimization metadata if provided. |
| if (optimization_metadata) |
| *optimization_metadata = {}; |
| |
| // Ensure that developers register their opt types before asking the |
| // optimization guide for data for that type. |
| DCHECK(registered_optimization_types_.find(optimization_type) != |
| registered_optimization_types_.end()); |
| // If the type is not registered, we probably don't have a hint for it, so |
| // just return. |
| if (registered_optimization_types_.find(optimization_type) == |
| registered_optimization_types_.end()) { |
| scoped_logger.set_type_decision(OptimizationTypeDecision::kNoHintAvailable); |
| return OptimizationTypeDecision::kNoHintAvailable; |
| } |
| |
| // If the URL doesn't have a host, we cannot query the hint for it, so just |
| // return early. |
| if (!navigation_url.has_host()) { |
| scoped_logger.set_type_decision(OptimizationTypeDecision::kNoHintAvailable); |
| return OptimizationTypeDecision::kNoHintAvailable; |
| } |
| const auto& host = navigation_url.host(); |
| |
| // Check if the URL should be filtered out if we have an optimization filter |
| // for the type. |
| |
| // Check if we have an allowlist loaded into memory for it, and if we do, |
| // see if the URL matches anything in the filter. |
| if (allowlist_optimization_filters_.find(optimization_type) != |
| allowlist_optimization_filters_.end()) { |
| const auto type_decision = |
| allowlist_optimization_filters_[optimization_type]->Matches( |
| navigation_url) |
| ? OptimizationTypeDecision::kAllowedByOptimizationFilter |
| : OptimizationTypeDecision::kNotAllowedByOptimizationFilter; |
| scoped_logger.set_type_decision(type_decision); |
| return type_decision; |
| } |
| |
| // Check if we have a blocklist loaded into memory for it, and if we do, see |
| // if the URL matches anything in the filter. |
| if (blocklist_optimization_filters_.find(optimization_type) != |
| blocklist_optimization_filters_.end()) { |
| const auto type_decision = |
| blocklist_optimization_filters_[optimization_type]->Matches( |
| navigation_url) |
| ? OptimizationTypeDecision::kNotAllowedByOptimizationFilter |
| : OptimizationTypeDecision::kAllowedByOptimizationFilter; |
| scoped_logger.set_type_decision(type_decision); |
| return type_decision; |
| } |
| |
| // Check if we had an optimization filter for it, but it was not loaded into |
| // memory. |
| if (optimization_types_with_filter_.find(optimization_type) != |
| optimization_types_with_filter_.end()) { |
| scoped_logger.set_type_decision( |
| OptimizationTypeDecision::kHadOptimizationFilterButNotLoadedInTime); |
| return OptimizationTypeDecision::kHadOptimizationFilterButNotLoadedInTime; |
| } |
| |
| // First, check if the optimization type is allowlisted by a URL-keyed hint. |
| const proto::Hint* url_keyed_hint = |
| hint_cache_->GetURLKeyedHint(navigation_url); |
| if (url_keyed_hint) { |
| DCHECK_EQ(url_keyed_hint->page_hints_size(), 1); |
| if (url_keyed_hint->page_hints_size() > 0) { |
| if (IsOptimizationTypeAllowed( |
| url_keyed_hint->page_hints(0).allowlisted_optimizations(), |
| optimization_type, optimization_metadata)) { |
| scoped_logger.set_type_decision( |
| OptimizationTypeDecision::kAllowedByHint); |
| if (optimization_metadata && !optimization_metadata->empty()) |
| scoped_logger.set_has_metadata(); |
| return OptimizationTypeDecision::kAllowedByHint; |
| } |
| } |
| } |
| |
| // Check if we have a hint already loaded for this navigation. |
| const proto::Hint* loaded_hint = hint_cache_->GetHostKeyedHintIfLoaded(host); |
| if (!loaded_hint) { |
| if (hint_cache_->HasHint(host)) { |
| // If we do not have a hint already loaded and we do not have one in the |
| // cache, we do not know what to do with the URL so just return. |
| // Otherwise, we do have information, but we just do not know it yet. |
| if (features::ShouldPersistHintsToDisk()) { |
| scoped_logger.set_type_decision( |
| OptimizationTypeDecision::kHadHintButNotLoadedInTime); |
| return OptimizationTypeDecision::kHadHintButNotLoadedInTime; |
| } else { |
| scoped_logger.set_type_decision( |
| OptimizationTypeDecision::kNoHintAvailable); |
| return OptimizationTypeDecision::kNoHintAvailable; |
| } |
| } |
| |
| if (IsHintBeingFetchedForNavigation(navigation_url)) { |
| scoped_logger.set_type_decision( |
| OptimizationTypeDecision::kHintFetchStartedButNotAvailableInTime); |
| return OptimizationTypeDecision::kHintFetchStartedButNotAvailableInTime; |
| } |
| scoped_logger.set_type_decision(OptimizationTypeDecision::kNoHintAvailable); |
| return OptimizationTypeDecision::kNoHintAvailable; |
| } |
| |
| if (IsOptimizationTypeAllowed(loaded_hint->allowlisted_optimizations(), |
| optimization_type, optimization_metadata)) { |
| scoped_logger.set_type_decision(OptimizationTypeDecision::kAllowedByHint); |
| if (optimization_metadata && !optimization_metadata->empty()) |
| scoped_logger.set_has_metadata(); |
| return OptimizationTypeDecision::kAllowedByHint; |
| } |
| |
| const proto::PageHint* matched_page_hint = |
| loaded_hint ? FindPageHintForURL(navigation_url, loaded_hint) : nullptr; |
| if (!matched_page_hint) { |
| scoped_logger.set_type_decision( |
| OptimizationTypeDecision::kNotAllowedByHint); |
| return OptimizationTypeDecision::kNotAllowedByHint; |
| } |
| |
| bool is_allowed = |
| IsOptimizationTypeAllowed(matched_page_hint->allowlisted_optimizations(), |
| optimization_type, optimization_metadata); |
| const auto type_decision = is_allowed |
| ? OptimizationTypeDecision::kAllowedByHint |
| : OptimizationTypeDecision::kNotAllowedByHint; |
| scoped_logger.set_type_decision(type_decision); |
| if (optimization_metadata && !optimization_metadata->empty()) |
| scoped_logger.set_has_metadata(); |
| return type_decision; |
| } |
| |
| void HintsManager::PrepareToInvokeRegisteredCallbacks( |
| const GURL& navigation_url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (registered_callbacks_.find(navigation_url) == registered_callbacks_.end()) |
| return; |
| |
| LoadHintForHost( |
| navigation_url.host(), |
| base::BindOnce(&HintsManager::OnReadyToInvokeRegisteredCallbacks, |
| weak_ptr_factory_.GetWeakPtr(), navigation_url)); |
| } |
| |
| void HintsManager::OnReadyToInvokeRegisteredCallbacks( |
| const GURL& navigation_url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (registered_callbacks_.find(navigation_url) == |
| registered_callbacks_.end()) { |
| return; |
| } |
| |
| for (auto& opt_type_and_callbacks : |
| registered_callbacks_.at(navigation_url)) { |
| proto::OptimizationType opt_type = opt_type_and_callbacks.first; |
| |
| for (auto& callback : opt_type_and_callbacks.second) { |
| OptimizationMetadata metadata; |
| OptimizationTypeDecision type_decision = |
| CanApplyOptimization(navigation_url, opt_type, &metadata); |
| OptimizationGuideDecision decision = |
| GetOptimizationGuideDecisionFromOptimizationTypeDecision( |
| type_decision); |
| base::UmaHistogramEnumeration( |
| "OptimizationGuide.ApplyDecisionAsync." + |
| GetStringNameForOptimizationType(opt_type), |
| type_decision); |
| std::move(callback).Run(decision, metadata); |
| } |
| } |
| registered_callbacks_.erase(navigation_url); |
| } |
| |
| bool HintsManager::HasOptimizationTypeToFetchFor() { |
| if (registered_optimization_types_.empty()) |
| return false; |
| |
| for (const auto& optimization_type : registered_optimization_types_) { |
| if (optimization_types_with_filter_.find(optimization_type) == |
| optimization_types_with_filter_.end()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool HintsManager::IsAllowedToFetchNavigationHints(const GURL& url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!HasOptimizationTypeToFetchFor()) |
| return false; |
| |
| if (!IsUserPermittedToFetchFromRemoteOptimizationGuide(is_off_the_record_, |
| pref_service_)) { |
| return false; |
| } |
| DCHECK(!is_off_the_record_); |
| |
| return url.is_valid() && url.SchemeIsHTTPOrHTTPS(); |
| } |
| |
| void HintsManager::OnNavigationStartOrRedirect( |
| OptimizationGuideNavigationData* navigation_data, |
| base::OnceClosure callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| LoadHintForURL(navigation_data->navigation_url(), std::move(callback)); |
| |
| if (switches::DisableFetchingHintsAtNavigationStartForTesting()) { |
| return; |
| } |
| |
| MaybeFetchHintsForNavigation(navigation_data); |
| } |
| |
| void HintsManager::MaybeFetchHintsForNavigation( |
| OptimizationGuideNavigationData* navigation_data) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (registered_optimization_types_.empty()) |
| return; |
| |
| const GURL url = navigation_data->navigation_url(); |
| if (!IsAllowedToFetchNavigationHints(url)) |
| return; |
| |
| ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder |
| race_navigation_recorder(navigation_data); |
| |
| // We expect that if the URL is being fetched for, we have already run through |
| // the logic to decide if we also require fetching hints for the host. |
| if (IsHintBeingFetchedForNavigation(url)) { |
| race_navigation_recorder.set_race_attempt_status( |
| RaceNavigationFetchAttemptStatus:: |
| kRaceNavigationFetchAlreadyInProgress); |
| |
| // Just set the hints fetch start to the start of the navigation, so we can |
| // track whether the hint came back before commit or not. |
| navigation_data->set_hints_fetch_start(navigation_data->navigation_start()); |
| return; |
| } |
| |
| std::vector<std::string> hosts; |
| std::vector<GURL> urls; |
| if (!hint_cache_->HasHint(url.host())) { |
| hosts.push_back(url.host()); |
| race_navigation_recorder.set_race_attempt_status( |
| RaceNavigationFetchAttemptStatus::kRaceNavigationFetchHost); |
| } |
| |
| if (!hint_cache_->HasURLKeyedEntryForURL(url)) { |
| urls.push_back(url); |
| race_navigation_recorder.set_race_attempt_status( |
| RaceNavigationFetchAttemptStatus::kRaceNavigationFetchURL); |
| } |
| |
| if (hosts.empty() && urls.empty()) { |
| race_navigation_recorder.set_race_attempt_status( |
| RaceNavigationFetchAttemptStatus::kRaceNavigationFetchNotAttempted); |
| return; |
| } |
| |
| DCHECK(hints_fetcher_factory_); |
| auto it = page_navigation_hints_fetchers_.Put( |
| url, hints_fetcher_factory_->BuildInstance()); |
| |
| UMA_HISTOGRAM_COUNTS_100( |
| "OptimizationGuide.HintsManager.ConcurrentPageNavigationFetches", |
| page_navigation_hints_fetchers_.size()); |
| |
| MaybeLogGetHintRequestInfo(proto::CONTEXT_PAGE_NAVIGATION, |
| registered_optimization_types_, urls, hosts); |
| bool fetch_attempted = it->second->FetchOptimizationGuideServiceHints( |
| hosts, urls, registered_optimization_types_, |
| proto::CONTEXT_PAGE_NAVIGATION, application_locale_, |
| base::BindOnce(&HintsManager::OnPageNavigationHintsFetched, |
| weak_ptr_factory_.GetWeakPtr(), |
| navigation_data->GetWeakPtr(), url, |
| base::flat_set<GURL>(urls.begin(), urls.end()), |
| base::flat_set<std::string>(hosts.begin(), hosts.end()))); |
| if (fetch_attempted) { |
| navigation_data->set_hints_fetch_start(base::TimeTicks::Now()); |
| |
| if (!hosts.empty() && !urls.empty()) { |
| race_navigation_recorder.set_race_attempt_status( |
| RaceNavigationFetchAttemptStatus::kRaceNavigationFetchHostAndURL); |
| } |
| } else { |
| race_navigation_recorder.set_race_attempt_status( |
| RaceNavigationFetchAttemptStatus::kRaceNavigationFetchNotAttempted); |
| } |
| } |
| |
| void HintsManager::OnNavigationFinish( |
| const std::vector<GURL>& navigation_redirect_chain) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // The callbacks will be invoked when the fetch request comes back, so it |
| // will be cleaned up later. |
| for (const auto& url : navigation_redirect_chain) { |
| if (IsHintBeingFetchedForNavigation(url)) |
| continue; |
| |
| PrepareToInvokeRegisteredCallbacks(url); |
| } |
| } |
| |
| void HintsManager::OnDeferredStartup() { |
| if (features::ShouldDeferStartupActiveTabsHintsFetch()) |
| InitiateHintsFetchScheduling(); |
| } |
| |
| base::WeakPtr<OptimizationGuideStore> HintsManager::hint_store() { |
| return hint_cache_->hint_store(); |
| } |
| |
| HintCache* HintsManager::hint_cache() { |
| return hint_cache_.get(); |
| } |
| |
| PushNotificationManager* HintsManager::push_notification_manager() { |
| return push_notification_manager_.get(); |
| } |
| |
| HintsFetcherFactory* HintsManager::GetHintsFetcherFactory() { |
| return hints_fetcher_factory_.get(); |
| } |
| |
| bool HintsManager::HasAllInformationForDecisionAvailable( |
| const GURL& navigation_url, |
| proto::OptimizationType optimization_type) { |
| if (HasLoadedOptimizationAllowlist(optimization_type) || |
| HasLoadedOptimizationBlocklist(optimization_type)) { |
| // If we have an optimization filter for the optimization type, it is |
| // consulted instead of any hints that may be available. |
| return true; |
| } |
| |
| bool has_host_keyed_hint = hint_cache_->HasHint(navigation_url.host()); |
| const auto* host_keyed_hint = |
| hint_cache_->GetHostKeyedHintIfLoaded(navigation_url.host()); |
| if (has_host_keyed_hint && host_keyed_hint == nullptr) { |
| // If we have a host-keyed hint in the cache and it is not loaded, we do not |
| // have all information available, regardless of whether we can fetch hints |
| // or not. |
| return false; |
| } |
| |
| if (!IsAllowedToFetchNavigationHints(navigation_url)) { |
| // If we are not allowed to fetch hints for the navigation, we have all |
| // information available if the host-keyed hint we have has been loaded |
| // already or we don't have a hint available. |
| return host_keyed_hint != nullptr || !has_host_keyed_hint; |
| } |
| |
| if (IsHintBeingFetchedForNavigation(navigation_url)) { |
| // If a hint is being fetched for the navigation, then we do not have all |
| // information available yet. |
| return false; |
| } |
| |
| // If we are allowed to fetch hints for the navigation, we only have all |
| // information available for certain if we have attempted to get the URL-keyed |
| // hint and if the host-keyed hint is loaded. |
| return hint_cache_->HasURLKeyedEntryForURL(navigation_url) && |
| host_keyed_hint != nullptr; |
| } |
| |
| void HintsManager::ClearFetchedHints() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| hint_cache_->ClearFetchedHints(); |
| HintsFetcher::ClearHostsSuccessfullyFetched(pref_service_); |
| } |
| |
| void HintsManager::ClearHostKeyedHints() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| hint_cache_->ClearHostKeyedHints(); |
| HintsFetcher::ClearHostsSuccessfullyFetched(pref_service_); |
| } |
| |
| void HintsManager::AddHintForTesting( |
| const GURL& url, |
| proto::OptimizationType optimization_type, |
| const absl::optional<OptimizationMetadata>& metadata) { |
| std::unique_ptr<proto::Hint> hint = std::make_unique<proto::Hint>(); |
| hint->set_key(url.spec()); |
| proto::PageHint* page_hint = hint->add_page_hints(); |
| page_hint->set_page_pattern("*"); |
| proto::Optimization* optimization = |
| page_hint->add_allowlisted_optimizations(); |
| optimization->set_optimization_type(optimization_type); |
| if (!metadata) { |
| hint_cache_->AddHintForTesting(url, std::move(hint)); // IN-TEST |
| PrepareToInvokeRegisteredCallbacks(url); |
| return; |
| } |
| if (metadata->loading_predictor_metadata()) { |
| *optimization->mutable_loading_predictor_metadata() = |
| *metadata->loading_predictor_metadata(); |
| } else if (metadata->performance_hints_metadata()) { |
| *optimization->mutable_performance_hints_metadata() = |
| *metadata->performance_hints_metadata(); |
| } else if (metadata->public_image_metadata()) { |
| *optimization->mutable_public_image_metadata() = |
| *metadata->public_image_metadata(); |
| } else if (metadata->any_metadata()) { |
| *optimization->mutable_any_metadata() = *metadata->any_metadata(); |
| } else { |
| NOTREACHED(); |
| } |
| hint_cache_->AddHintForTesting(url, std::move(hint)); // IN-TEST |
| PrepareToInvokeRegisteredCallbacks(url); |
| } |
| |
| void HintsManager::RemoveFetchedEntriesByHintKeys( |
| base::OnceClosure on_success, |
| proto::KeyRepresentation key_representation, |
| const base::flat_set<std::string>& hint_keys) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Make sure the key representation is something that we expect. |
| switch (key_representation) { |
| case proto::KeyRepresentation::HOST: |
| case proto::KeyRepresentation::FULL_URL: |
| break; |
| default: |
| NOTREACHED(); |
| return; |
| } |
| |
| if (key_representation == proto::FULL_URL) { |
| base::flat_set<GURL> urls_to_remove; |
| base::flat_set<std::string> hosts_to_remove; |
| // It is possible that the hints may have upgraded from being HOST keyed to |
| // URL keyed on the server at any time. To protect against this, also remove |
| // the host of the GURL from storage. |
| // However, note that the opposite is not likely to happen since URL-keyed |
| // hints are not persisted to disk. |
| for (const std::string& url : hint_keys) { |
| GURL gurl(url); |
| if (!gurl.is_valid()) { |
| continue; |
| } |
| hosts_to_remove.insert(gurl.host()); |
| urls_to_remove.insert(gurl); |
| } |
| |
| // Also clear the HintFetcher's host pref. |
| for (const std::string& host : hosts_to_remove) { |
| HintsFetcher::ClearSingleFetchedHost(pref_service_, host); |
| } |
| |
| hint_cache_->RemoveHintsForURLs(urls_to_remove); |
| hint_cache_->RemoveHintsForHosts(std::move(on_success), hosts_to_remove); |
| return; |
| } |
| |
| // Also clear the HintFetcher's host pref. |
| for (const std::string& host : hint_keys) { |
| HintsFetcher::ClearSingleFetchedHost(pref_service_, host); |
| } |
| |
| hint_cache_->RemoveHintsForHosts(std::move(on_success), hint_keys); |
| } |
| |
| void HintsManager::PurgeFetchedEntries(base::OnceClosure on_success) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| ClearFetchedHints(); |
| std::move(on_success).Run(); |
| } |
| |
| } // namespace optimization_guide |