1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/optimization_guide/optimization_guide_hints_manager.h"
6 
7 #include <string>
8 #include <utility>
9 
10 #include "base/bind.h"
11 #include "base/callback_helpers.h"
12 #include "base/command_line.h"
13 #include "base/logging.h"
14 #include "base/metrics/histogram_functions.h"
15 #include "base/metrics/histogram_macros.h"
16 #include "base/metrics/histogram_macros_local.h"
17 #include "base/notreached.h"
18 #include "base/rand_util.h"
19 #include "base/sequenced_task_runner.h"
20 #include "base/task/post_task.h"
21 #include "base/task/thread_pool.h"
22 #include "base/task_runner_util.h"
23 #include "base/time/default_clock.h"
24 #include "chrome/browser/browser_process.h"
25 #include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h"
26 #include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service_factory.h"
27 #include "chrome/browser/optimization_guide/optimization_guide_navigation_data.h"
28 #include "chrome/browser/optimization_guide/optimization_guide_permissions_util.h"
29 #include "chrome/browser/optimization_guide/optimization_guide_web_contents_observer.h"
30 #include "chrome/browser/profiles/profile.h"
31 #include "components/google/core/common/google_util.h"
32 #include "components/optimization_guide/bloom_filter.h"
33 #include "components/optimization_guide/hint_cache.h"
34 #include "components/optimization_guide/hints_component_util.h"
35 #include "components/optimization_guide/hints_fetcher_factory.h"
36 #include "components/optimization_guide/hints_processing_util.h"
37 #include "components/optimization_guide/optimization_filter.h"
38 #include "components/optimization_guide/optimization_guide_constants.h"
39 #include "components/optimization_guide/optimization_guide_decider.h"
40 #include "components/optimization_guide/optimization_guide_enums.h"
41 #include "components/optimization_guide/optimization_guide_features.h"
42 #include "components/optimization_guide/optimization_guide_prefs.h"
43 #include "components/optimization_guide/optimization_guide_service.h"
44 #include "components/optimization_guide/optimization_guide_store.h"
45 #include "components/optimization_guide/optimization_guide_switches.h"
46 #include "components/optimization_guide/optimization_guide_util.h"
47 #include "components/optimization_guide/optimization_metadata.h"
48 #include "components/optimization_guide/proto/models.pb.h"
49 #include "components/optimization_guide/top_host_provider.h"
50 #include "components/prefs/pref_service.h"
51 #include "components/prefs/scoped_user_pref_update.h"
52 #include "content/public/browser/browser_thread.h"
53 #include "content/public/browser/navigation_handle.h"
54 #include "services/metrics/public/cpp/ukm_builders.h"
55 #include "services/metrics/public/cpp/ukm_recorder.h"
56 #include "services/metrics/public/cpp/ukm_source.h"
57 #include "services/metrics/public/cpp/ukm_source_id.h"
58 #include "services/network/public/cpp/shared_url_loader_factory.h"
59 
60 namespace {
61 
62 // The component version used with a manual config. This ensures that any hint
63 // component received from the OptimizationGuideService on a subsequent startup
64 // will have a newer version than it.
65 constexpr char kManualConfigComponentVersion[] = "0.0.0";
66 
67 // Delay until successfully fetched hints should be updated by requesting from
68 // the remote Optimization Guide Service.
69 constexpr base::TimeDelta kUpdateFetchedHintsDelay =
70     base::TimeDelta::FromHours(24);
71 
72 // Provides a random time delta in seconds between |kFetchRandomMinDelay| and
73 // |kFetchRandomMaxDelay|.
RandomFetchDelay()74 base::TimeDelta RandomFetchDelay() {
75   constexpr int kFetchRandomMinDelaySecs = 30;
76   constexpr int kFetchRandomMaxDelaySecs = 60;
77   return base::TimeDelta::FromSeconds(
78       base::RandInt(kFetchRandomMinDelaySecs, kFetchRandomMaxDelaySecs));
79 }
80 
MaybeRunUpdateClosure(base::OnceClosure update_closure)81 void MaybeRunUpdateClosure(base::OnceClosure update_closure) {
82   if (update_closure)
83     std::move(update_closure).Run();
84 }
85 
86 // Returns whether the particular component version can be processed, and if it
87 // can be, locks the semaphore (in the form of a pref) to signal that the
88 // processing of this particular version has started.
CanProcessComponentVersion(PrefService * pref_service,const base::Version & version)89 bool CanProcessComponentVersion(PrefService* pref_service,
90                                 const base::Version& version) {
91   DCHECK(version.IsValid());
92 
93   const std::string previous_attempted_version_string = pref_service->GetString(
94       optimization_guide::prefs::kPendingHintsProcessingVersion);
95   if (!previous_attempted_version_string.empty()) {
96     const base::Version previous_attempted_version =
97         base::Version(previous_attempted_version_string);
98     if (!previous_attempted_version.IsValid()) {
99       DLOG(ERROR) << "Bad contents in hints processing pref";
100       // Clear pref for fresh start next time.
101       pref_service->ClearPref(
102           optimization_guide::prefs::kPendingHintsProcessingVersion);
103       return false;
104     }
105     if (previous_attempted_version.CompareTo(version) == 0) {
106       // Previously attempted same version without completion.
107       return false;
108     }
109   }
110 
111   // Write config version to pref.
112   pref_service->SetString(
113       optimization_guide::prefs::kPendingHintsProcessingVersion,
114       version.GetString());
115   return true;
116 }
117 
118 // Returns whether |optimization_type| is whitelisted by |optimizations|. If
119 // it is whitelisted, this will return true and |optimization_metadata| will be
120 // populated with the metadata provided by the hint, if applicable. If
121 // |page_hint| is not provided or |optimization_type| is not whitelisted, this
122 // will return false.
IsOptimizationTypeAllowed(const google::protobuf::RepeatedPtrField<optimization_guide::proto::Optimization> & optimizations,optimization_guide::proto::OptimizationType optimization_type,optimization_guide::OptimizationMetadata * optimization_metadata,base::Optional<uint64_t> * tuning_version)123 bool IsOptimizationTypeAllowed(
124     const google::protobuf::RepeatedPtrField<
125         optimization_guide::proto::Optimization>& optimizations,
126     optimization_guide::proto::OptimizationType optimization_type,
127     optimization_guide::OptimizationMetadata* optimization_metadata,
128     base::Optional<uint64_t>* tuning_version) {
129   DCHECK(tuning_version);
130   *tuning_version = base::nullopt;
131 
132   for (const auto& optimization : optimizations) {
133     if (optimization_type != optimization.optimization_type())
134       continue;
135 
136     if (optimization.has_tuning_version()) {
137       *tuning_version = optimization.tuning_version();
138 
139       if (optimization.tuning_version() == UINT64_MAX) {
140         // UINT64_MAX is the sentinel value indicating that the optimization
141         // should not be served and was only added to the list for metrics
142         // purposes.
143         return false;
144       }
145     }
146 
147     // We found an optimization that can be applied. Populate optimization
148     // metadata if applicable and return.
149     if (optimization_metadata) {
150       switch (optimization.metadata_case()) {
151         case optimization_guide::proto::Optimization::kPreviewsMetadata:
152           optimization_metadata->set_previews_metadata(
153               optimization.previews_metadata());
154           break;
155         case optimization_guide::proto::Optimization::kPerformanceHintsMetadata:
156           optimization_metadata->set_performance_hints_metadata(
157               optimization.performance_hints_metadata());
158           break;
159         case optimization_guide::proto::Optimization::kPublicImageMetadata:
160           optimization_metadata->set_public_image_metadata(
161               optimization.public_image_metadata());
162           break;
163         case optimization_guide::proto::Optimization::kLoadingPredictorMetadata:
164           optimization_metadata->set_loading_predictor_metadata(
165               optimization.loading_predictor_metadata());
166           break;
167         case optimization_guide::proto::Optimization::kAnyMetadata:
168           optimization_metadata->set_any_metadata(optimization.any_metadata());
169           break;
170         case optimization_guide::proto::Optimization::METADATA_NOT_SET:
171           // Some optimization types do not have metadata, make sure we do not
172           // DCHECK.
173           break;
174       }
175     }
176     return true;
177   }
178 
179   return false;
180 }
181 
182 // Logs an OptimizationAutotuning event for the navigation with |navigation_id|,
183 // if |navigation_id| and |tuning_version| are non-null.
MaybeLogOptimizationAutotuningUKMForNavigation(base::Optional<int64_t> navigation_id,optimization_guide::proto::OptimizationType optimization_type,base::Optional<int64_t> tuning_version)184 void MaybeLogOptimizationAutotuningUKMForNavigation(
185     base::Optional<int64_t> navigation_id,
186     optimization_guide::proto::OptimizationType optimization_type,
187     base::Optional<int64_t> tuning_version) {
188   if (!navigation_id || !tuning_version) {
189     // Only log if we can correlate the tuning event with a navigation.
190     return;
191   }
192 
193   ukm::SourceId ukm_source_id =
194       ukm::ConvertToSourceId(*navigation_id, ukm::SourceIdType::NAVIGATION_ID);
195   ukm::builders::OptimizationGuideAutotuning builder(ukm_source_id);
196   builder.SetOptimizationType(optimization_type)
197       .SetTuningVersion(*tuning_version)
198       .Record(ukm::UkmRecorder::Get());
199 }
200 
201 // Util class for recording whether a hints fetch race against the current
202 // navigation was attempted. The result is recorded when it goes out of scope
203 // and its destructor is called.
204 class ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder {
205  public:
ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder(content::NavigationHandle * navigation_handle)206   explicit ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder(
207       content::NavigationHandle* navigation_handle)
208       : race_attempt_status_(
209             optimization_guide::RaceNavigationFetchAttemptStatus::kUnknown),
210         navigation_data_(
211             OptimizationGuideNavigationData::GetFromNavigationHandle(
212                 navigation_handle)) {}
213 
~ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder()214   ~ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder() {
215     DCHECK_NE(race_attempt_status_,
216               optimization_guide::RaceNavigationFetchAttemptStatus::kUnknown);
217     DCHECK_NE(
218         race_attempt_status_,
219         optimization_guide::RaceNavigationFetchAttemptStatus::
220             kDeprecatedRaceNavigationFetchNotAttemptedTooManyConcurrentFetches);
221     base::UmaHistogramEnumeration(
222         "OptimizationGuide.HintsManager.RaceNavigationFetchAttemptStatus",
223         race_attempt_status_);
224     if (navigation_data_)
225       navigation_data_->set_hints_fetch_attempt_status(race_attempt_status_);
226   }
227 
set_race_attempt_status(optimization_guide::RaceNavigationFetchAttemptStatus race_attempt_status)228   void set_race_attempt_status(
229       optimization_guide::RaceNavigationFetchAttemptStatus
230           race_attempt_status) {
231     race_attempt_status_ = race_attempt_status;
232   }
233 
234  private:
235   optimization_guide::RaceNavigationFetchAttemptStatus race_attempt_status_;
236   OptimizationGuideNavigationData* navigation_data_;
237 };
238 
239 // Returns true if the optimization type should be ignored when is newly
240 // registered as the optimization type is likely launched.
ShouldIgnoreNewlyRegisteredOptimizationType(optimization_guide::proto::OptimizationType optimization_type)241 bool ShouldIgnoreNewlyRegisteredOptimizationType(
242     optimization_guide::proto::OptimizationType optimization_type) {
243   switch (optimization_type) {
244     case optimization_guide::proto::NOSCRIPT:
245     case optimization_guide::proto::RESOURCE_LOADING:
246     case optimization_guide::proto::LITE_PAGE_REDIRECT:
247     case optimization_guide::proto::DEFER_ALL_SCRIPT:
248       return true;
249     default:
250       return false;
251   }
252   return false;
253 }
254 
255 }  // namespace
256 
OptimizationGuideHintsManager(const std::vector<optimization_guide::proto::OptimizationType> & optimization_types_at_initialization,optimization_guide::OptimizationGuideService * optimization_guide_service,Profile * profile,const base::FilePath & profile_path,PrefService * pref_service,leveldb_proto::ProtoDatabaseProvider * database_provider,optimization_guide::TopHostProvider * top_host_provider,scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)257 OptimizationGuideHintsManager::OptimizationGuideHintsManager(
258     const std::vector<optimization_guide::proto::OptimizationType>&
259         optimization_types_at_initialization,
260     optimization_guide::OptimizationGuideService* optimization_guide_service,
261     Profile* profile,
262     const base::FilePath& profile_path,
263     PrefService* pref_service,
264     leveldb_proto::ProtoDatabaseProvider* database_provider,
265     optimization_guide::TopHostProvider* top_host_provider,
266     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
267     : optimization_guide_service_(optimization_guide_service),
268       background_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
269           {base::MayBlock(), base::TaskPriority::BEST_EFFORT})),
270       profile_(profile),
271       pref_service_(pref_service),
272       hint_cache_(std::make_unique<optimization_guide::HintCache>(
273           optimization_guide::features::ShouldPersistHintsToDisk()
274               ? std::make_unique<optimization_guide::OptimizationGuideStore>(
275                     database_provider,
276                     profile_path.AddExtensionASCII(
277                         optimization_guide::kOptimizationGuideHintStore),
278                     background_task_runner_)
279               : nullptr,
280           optimization_guide::features::MaxHostKeyedHintCacheSize())),
281       page_navigation_hints_fetchers_(
282           optimization_guide::features::MaxConcurrentPageNavigationFetches()),
283       hints_fetcher_factory_(
284           std::make_unique<optimization_guide::HintsFetcherFactory>(
285               url_loader_factory,
286               optimization_guide::features::
287                   GetOptimizationGuideServiceGetHintsURL(),
288               pref_service)),
289       external_app_packages_approved_for_fetch_(
290           optimization_guide::features::
291               ExternalAppPackageNamesApprovedForFetch()),
292       top_host_provider_(top_host_provider),
293       clock_(base::DefaultClock::GetInstance()) {
294   DCHECK(optimization_guide_service_);
295 
296   RegisterOptimizationTypes(optimization_types_at_initialization);
297 
298   g_browser_process->network_quality_tracker()
299       ->AddEffectiveConnectionTypeObserver(this);
300 
301   hint_cache_->Initialize(
302       optimization_guide::switches::
303           ShouldPurgeOptimizationGuideStoreOnStartup(),
304       base::BindOnce(&OptimizationGuideHintsManager::OnHintCacheInitialized,
305                      ui_weak_ptr_factory_.GetWeakPtr()));
306 
307   NavigationPredictorKeyedService* navigation_predictor_service =
308       NavigationPredictorKeyedServiceFactory::GetForProfile(profile_);
309   navigation_predictor_service->AddObserver(this);
310 }
311 
~OptimizationGuideHintsManager()312 OptimizationGuideHintsManager::~OptimizationGuideHintsManager() {
313   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
314 
315   optimization_guide_service_->RemoveObserver(this);
316   g_browser_process->network_quality_tracker()
317       ->RemoveEffectiveConnectionTypeObserver(this);
318 
319   NavigationPredictorKeyedService* navigation_predictor_service =
320       NavigationPredictorKeyedServiceFactory::GetForProfile(profile_);
321   navigation_predictor_service->RemoveObserver(this);
322 }
323 
Shutdown()324 void OptimizationGuideHintsManager::Shutdown() {
325   optimization_guide_service_->RemoveObserver(this);
326   g_browser_process->network_quality_tracker()
327       ->RemoveEffectiveConnectionTypeObserver(this);
328 }
329 
OnHintsComponentAvailable(const optimization_guide::HintsComponentInfo & info)330 void OptimizationGuideHintsManager::OnHintsComponentAvailable(
331     const optimization_guide::HintsComponentInfo& info) {
332   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
333 
334   // Check for if hint component is disabled. This check is needed because the
335   // optimization guide still registers with the service as an observer for
336   // components as a signal during testing.
337   if (optimization_guide::switches::IsHintComponentProcessingDisabled()) {
338     MaybeRunUpdateClosure(std::move(next_update_closure_));
339     return;
340   }
341 
342   if (!CanProcessComponentVersion(pref_service_, info.version)) {
343     optimization_guide::RecordProcessHintsComponentResult(
344         optimization_guide::ProcessHintsComponentResult::
345             kFailedFinishProcessing);
346     MaybeRunUpdateClosure(std::move(next_update_closure_));
347     return;
348   }
349 
350   std::unique_ptr<optimization_guide::StoreUpdateData> update_data =
351       hint_cache_->MaybeCreateUpdateDataForComponentHints(info.version);
352 
353   // Processes the hints from the newly available component on a background
354   // thread, providing a StoreUpdateData for component update from the hint
355   // cache, so that each hint within the component can be moved into it. In the
356   // case where the component's version is not newer than the optimization guide
357   // store's component version, StoreUpdateData will be a nullptr and hint
358   // processing will be skipped. After PreviewsHints::Create() returns the newly
359   // created PreviewsHints, it is initialized in UpdateHints() on the UI thread.
360   base::PostTaskAndReplyWithResult(
361       background_task_runner_.get(), FROM_HERE,
362       base::BindOnce(&OptimizationGuideHintsManager::ProcessHintsComponent,
363                      base::Unretained(this), info,
364                      registered_optimization_types_, std::move(update_data)),
365       base::BindOnce(&OptimizationGuideHintsManager::UpdateComponentHints,
366                      ui_weak_ptr_factory_.GetWeakPtr(),
367                      std::move(next_update_closure_)));
368 
369   // Only replace hints component info if it is not the same - otherwise we will
370   // destruct the object and it will be invalid later.
371   if (!hints_component_info_ ||
372       hints_component_info_->version.CompareTo(info.version) != 0) {
373     hints_component_info_.emplace(info.version, info.path);
374   }
375 }
376 
377 std::unique_ptr<optimization_guide::StoreUpdateData>
ProcessHintsComponent(const optimization_guide::HintsComponentInfo & info,const base::flat_set<optimization_guide::proto::OptimizationType> & registered_optimization_types,std::unique_ptr<optimization_guide::StoreUpdateData> update_data)378 OptimizationGuideHintsManager::ProcessHintsComponent(
379     const optimization_guide::HintsComponentInfo& info,
380     const base::flat_set<optimization_guide::proto::OptimizationType>&
381         registered_optimization_types,
382     std::unique_ptr<optimization_guide::StoreUpdateData> update_data) {
383   DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
384 
385   optimization_guide::ProcessHintsComponentResult out_result;
386   std::unique_ptr<optimization_guide::proto::Configuration> config =
387       optimization_guide::ProcessHintsComponent(info, &out_result);
388   if (!config) {
389     optimization_guide::RecordProcessHintsComponentResult(out_result);
390     return nullptr;
391   }
392 
393   ProcessOptimizationFilters(config->optimization_allowlists(),
394                              config->optimization_blacklists(),
395                              registered_optimization_types);
396 
397   // TODO(crbug/1112500): Figure out what to do with component hints if there
398   // isn't a persistent store. Right now, it doesn't really matter since there
399   // aren't hints sent down via the component, but we need to figure out
400   // threading since these hints are now stored in memory prior to being
401   // persisted.
402   if (update_data) {
403     bool did_process_hints = hint_cache_->ProcessAndCacheHints(
404         config->mutable_hints(), update_data.get());
405     optimization_guide::RecordProcessHintsComponentResult(
406         did_process_hints
407             ? optimization_guide::ProcessHintsComponentResult::kSuccess
408             : optimization_guide::ProcessHintsComponentResult::
409                   kProcessedNoHints);
410   } else {
411     optimization_guide::RecordProcessHintsComponentResult(
412         optimization_guide::ProcessHintsComponentResult::
413             kSkippedProcessingHints);
414   }
415 
416   return update_data;
417 }
418 
ProcessOptimizationFilters(const google::protobuf::RepeatedPtrField<optimization_guide::proto::OptimizationFilter> & allowlist_optimization_filters,const google::protobuf::RepeatedPtrField<optimization_guide::proto::OptimizationFilter> & blocklist_optimization_filters,const base::flat_set<optimization_guide::proto::OptimizationType> & registered_optimization_types)419 void OptimizationGuideHintsManager::ProcessOptimizationFilters(
420     const google::protobuf::RepeatedPtrField<
421         optimization_guide::proto::OptimizationFilter>&
422         allowlist_optimization_filters,
423     const google::protobuf::RepeatedPtrField<
424         optimization_guide::proto::OptimizationFilter>&
425         blocklist_optimization_filters,
426     const base::flat_set<optimization_guide::proto::OptimizationType>&
427         registered_optimization_types) {
428   DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
429   base::AutoLock lock(optimization_filters_lock_);
430 
431   optimization_types_with_filter_.clear();
432   allowlist_optimization_filters_.clear();
433   blocklist_optimization_filters_.clear();
434   ProcessOptimizationFilterSet(allowlist_optimization_filters,
435                                /*is_allowlist=*/true,
436                                registered_optimization_types);
437   ProcessOptimizationFilterSet(blocklist_optimization_filters,
438                                /*is_allowlist=*/false,
439                                registered_optimization_types);
440 }
441 
ProcessOptimizationFilterSet(const google::protobuf::RepeatedPtrField<optimization_guide::proto::OptimizationFilter> & filters,bool is_allowlist,const base::flat_set<optimization_guide::proto::OptimizationType> & registered_optimization_types)442 void OptimizationGuideHintsManager::ProcessOptimizationFilterSet(
443     const google::protobuf::RepeatedPtrField<
444         optimization_guide::proto::OptimizationFilter>& filters,
445     bool is_allowlist,
446     const base::flat_set<optimization_guide::proto::OptimizationType>&
447         registered_optimization_types) {
448   DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
449 
450   for (const auto& filter : filters) {
451     if (filter.optimization_type() !=
452         optimization_guide::proto::TYPE_UNSPECIFIED) {
453       optimization_types_with_filter_.insert(filter.optimization_type());
454     }
455 
456     // Do not put anything in memory that we don't have registered.
457     if (registered_optimization_types.find(filter.optimization_type()) ==
458         registered_optimization_types.end()) {
459       continue;
460     }
461 
462     optimization_guide::RecordOptimizationFilterStatus(
463         filter.optimization_type(),
464         optimization_guide::OptimizationFilterStatus::kFoundServerFilterConfig);
465 
466     // Do not parse duplicate optimization filters.
467     if (allowlist_optimization_filters_.find(filter.optimization_type()) !=
468             allowlist_optimization_filters_.end() ||
469         blocklist_optimization_filters_.find(filter.optimization_type()) !=
470             blocklist_optimization_filters_.end()) {
471       optimization_guide::RecordOptimizationFilterStatus(
472           filter.optimization_type(),
473           optimization_guide::OptimizationFilterStatus::
474               kFailedServerFilterDuplicateConfig);
475       continue;
476     }
477 
478     // Parse optimization filter.
479     optimization_guide::OptimizationFilterStatus status;
480     std::unique_ptr<optimization_guide::OptimizationFilter>
481         optimization_filter =
482             optimization_guide::ProcessOptimizationFilter(filter, &status);
483     if (optimization_filter) {
484       if (is_allowlist) {
485         allowlist_optimization_filters_.insert(
486             {filter.optimization_type(), std::move(optimization_filter)});
487       } else {
488         blocklist_optimization_filters_.insert(
489             {filter.optimization_type(), std::move(optimization_filter)});
490       }
491     }
492     optimization_guide::RecordOptimizationFilterStatus(
493         filter.optimization_type(), status);
494   }
495 }
496 
OnHintCacheInitialized()497 void OptimizationGuideHintsManager::OnHintCacheInitialized() {
498   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
499 
500   // Check if there is a valid hint proto given on the command line first. We
501   // don't normally expect one, but if one is provided then use that and do not
502   // register as an observer as the opt_guide service.
503   std::unique_ptr<optimization_guide::proto::Configuration> manual_config =
504       optimization_guide::switches::ParseComponentConfigFromCommandLine();
505   if (manual_config) {
506     std::unique_ptr<optimization_guide::StoreUpdateData> update_data =
507         hint_cache_->MaybeCreateUpdateDataForComponentHints(
508             base::Version(kManualConfigComponentVersion));
509     hint_cache_->ProcessAndCacheHints(manual_config->mutable_hints(),
510                                       update_data.get());
511     // Allow |UpdateComponentHints| to block startup so that the first
512     // navigation gets the hints when a command line hint proto is provided.
513     UpdateComponentHints(base::DoNothing(), std::move(update_data));
514 
515     // Process any optimization filters passed via command line on the
516     // background thread.
517     if (manual_config->optimization_allowlists_size() > 0 ||
518         manual_config->optimization_blacklists_size() > 0) {
519       background_task_runner_->PostTask(
520           FROM_HERE,
521           base::BindOnce(
522               &OptimizationGuideHintsManager::ProcessOptimizationFilters,
523               base::Unretained(this), manual_config->optimization_allowlists(),
524               manual_config->optimization_blacklists(),
525               registered_optimization_types_));
526     }
527   }
528 
529   // If the store is available, clear all hint state so newly registered types
530   // can have their hints immediately included in hint fetches.
531   if (hint_cache_->IsHintStoreAvailable() && should_clear_hints_for_new_type_) {
532     ClearHostKeyedHints();
533     should_clear_hints_for_new_type_ = false;
534   }
535 
536   // Register as an observer regardless of hint proto override usage. This is
537   // needed as a signal during testing.
538   optimization_guide_service_->AddObserver(this);
539 }
540 
UpdateComponentHints(base::OnceClosure update_closure,std::unique_ptr<optimization_guide::StoreUpdateData> update_data)541 void OptimizationGuideHintsManager::UpdateComponentHints(
542     base::OnceClosure update_closure,
543     std::unique_ptr<optimization_guide::StoreUpdateData> update_data) {
544   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
545 
546   // If we get here, the hints have been processed correctly.
547   pref_service_->ClearPref(
548       optimization_guide::prefs::kPendingHintsProcessingVersion);
549 
550   if (update_data) {
551     hint_cache_->UpdateComponentHints(
552         std::move(update_data),
553         base::BindOnce(&OptimizationGuideHintsManager::OnComponentHintsUpdated,
554                        ui_weak_ptr_factory_.GetWeakPtr(),
555                        std::move(update_closure),
556                        /* hints_updated=*/true));
557   } else {
558     OnComponentHintsUpdated(std::move(update_closure), /*hints_updated=*/false);
559   }
560 }
561 
OnComponentHintsUpdated(base::OnceClosure update_closure,bool hints_updated)562 void OptimizationGuideHintsManager::OnComponentHintsUpdated(
563     base::OnceClosure update_closure,
564     bool hints_updated) {
565   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
566 
567   // Record the result of updating the hints. This is used as a signal for the
568   // hints being fully processed in testing.
569   LOCAL_HISTOGRAM_BOOLEAN(
570       optimization_guide::kComponentHintsUpdatedResultHistogramString,
571       hints_updated);
572 
573   MaybeScheduleTopHostsHintsFetch();
574 
575   MaybeRunUpdateClosure(std::move(update_closure));
576 }
577 
ListenForNextUpdateForTesting(base::OnceClosure next_update_closure)578 void OptimizationGuideHintsManager::ListenForNextUpdateForTesting(
579     base::OnceClosure next_update_closure) {
580   DCHECK(!next_update_closure_)
581       << "Only one update closure is supported at a time";
582   next_update_closure_ = std::move(next_update_closure);
583 }
584 
SetHintsFetcherFactoryForTesting(std::unique_ptr<optimization_guide::HintsFetcherFactory> hints_fetcher_factory)585 void OptimizationGuideHintsManager::SetHintsFetcherFactoryForTesting(
586     std::unique_ptr<optimization_guide::HintsFetcherFactory>
587         hints_fetcher_factory) {
588   hints_fetcher_factory_ = std::move(hints_fetcher_factory);
589 }
590 
SetClockForTesting(const base::Clock * clock)591 void OptimizationGuideHintsManager::SetClockForTesting(
592     const base::Clock* clock) {
593   clock_ = clock;
594 }
595 
MaybeScheduleTopHostsHintsFetch()596 void OptimizationGuideHintsManager::MaybeScheduleTopHostsHintsFetch() {
597   if (!top_host_provider_ ||
598       !IsUserPermittedToFetchFromRemoteOptimizationGuide(profile_)) {
599     return;
600   }
601 
602   if (!optimization_guide::features::ShouldBatchUpdateHintsForTopHosts())
603     return;
604 
605   if (optimization_guide::switches::ShouldOverrideFetchHintsTimer()) {
606     SetLastHintsFetchAttemptTime(clock_->Now());
607     FetchTopHostsHints();
608   } else if (!top_hosts_hints_fetch_timer_.IsRunning()) {
609     // Only Schedule this is the time is not already running.
610     ScheduleTopHostsHintsFetch();
611   }
612 }
613 
ScheduleTopHostsHintsFetch()614 void OptimizationGuideHintsManager::ScheduleTopHostsHintsFetch() {
615   DCHECK(!top_hosts_hints_fetch_timer_.IsRunning());
616 
617   const base::TimeDelta time_until_update_time =
618       hint_cache_->GetFetchedHintsUpdateTime() - clock_->Now();
619   base::TimeDelta fetcher_delay;
620   if (time_until_update_time <= base::TimeDelta()) {
621     // Fetched hints in the store should be updated and an attempt has not
622     // been made in last |kUpdateFetchedHintsDelay|.
623     SetLastHintsFetchAttemptTime(clock_->Now());
624     top_hosts_hints_fetch_timer_.Start(
625         FROM_HERE, RandomFetchDelay(), this,
626         &OptimizationGuideHintsManager::FetchTopHostsHints);
627   } else {
628     // If the fetched hints in the store are still up-to-date, set a timer
629     // for when the hints need to be updated.
630     fetcher_delay = time_until_update_time;
631     top_hosts_hints_fetch_timer_.Start(
632         FROM_HERE, fetcher_delay, this,
633         &OptimizationGuideHintsManager::ScheduleTopHostsHintsFetch);
634   }
635 }
636 
FetchTopHostsHints()637 void OptimizationGuideHintsManager::FetchTopHostsHints() {
638   DCHECK(top_host_provider_);
639 
640   if (registered_optimization_types_.empty())
641     return;
642 
643   std::vector<std::string> top_hosts = top_host_provider_->GetTopHosts();
644   if (top_hosts.empty())
645     return;
646 
647   if (!batch_update_hints_fetcher_) {
648     DCHECK(hints_fetcher_factory_);
649     batch_update_hints_fetcher_ = hints_fetcher_factory_->BuildInstance();
650   }
651 
652   batch_update_hints_fetcher_->FetchOptimizationGuideServiceHints(
653       top_hosts, std::vector<GURL>{}, registered_optimization_types_,
654       optimization_guide::proto::CONTEXT_BATCH_UPDATE,
655       base::BindOnce(
656           &OptimizationGuideHintsManager::OnTopHostsHintsFetched,
657           ui_weak_ptr_factory_.GetWeakPtr(),
658           base::flat_set<std::string>(top_hosts.begin(), top_hosts.end())));
659 }
660 
OnTopHostsHintsFetched(const base::flat_set<std::string> & hosts_fetched,base::Optional<std::unique_ptr<optimization_guide::proto::GetHintsResponse>> get_hints_response)661 void OptimizationGuideHintsManager::OnTopHostsHintsFetched(
662     const base::flat_set<std::string>& hosts_fetched,
663     base::Optional<std::unique_ptr<optimization_guide::proto::GetHintsResponse>>
664         get_hints_response) {
665   if (!get_hints_response)
666     return;
667 
668   hint_cache_->UpdateFetchedHints(
669       std::move(*get_hints_response), clock_->Now() + kUpdateFetchedHintsDelay,
670       hosts_fetched,
671       /*urls_fetched=*/{},
672       base::BindOnce(
673           &OptimizationGuideHintsManager::OnFetchedTopHostsHintsStored,
674           ui_weak_ptr_factory_.GetWeakPtr()));
675 }
676 
OnPageNavigationHintsFetched(base::WeakPtr<OptimizationGuideNavigationData> navigation_data_weak_ptr,const base::Optional<GURL> & navigation_url,const base::flat_set<GURL> & page_navigation_urls_requested,const base::flat_set<std::string> & page_navigation_hosts_requested,base::Optional<std::unique_ptr<optimization_guide::proto::GetHintsResponse>> get_hints_response)677 void OptimizationGuideHintsManager::OnPageNavigationHintsFetched(
678     base::WeakPtr<OptimizationGuideNavigationData> navigation_data_weak_ptr,
679     const base::Optional<GURL>& navigation_url,
680     const base::flat_set<GURL>& page_navigation_urls_requested,
681     const base::flat_set<std::string>& page_navigation_hosts_requested,
682     base::Optional<std::unique_ptr<optimization_guide::proto::GetHintsResponse>>
683         get_hints_response) {
684   if (!get_hints_response.has_value() || !get_hints_response.value()) {
685     if (navigation_url) {
686       CleanUpFetcherForNavigation(*navigation_url);
687       PrepareToInvokeRegisteredCallbacks(*navigation_url);
688     }
689     return;
690   }
691 
692   hint_cache_->UpdateFetchedHints(
693       std::move(*get_hints_response), clock_->Now() + kUpdateFetchedHintsDelay,
694       page_navigation_hosts_requested, page_navigation_urls_requested,
695       base::BindOnce(
696           &OptimizationGuideHintsManager::OnFetchedPageNavigationHintsStored,
697           ui_weak_ptr_factory_.GetWeakPtr(), navigation_data_weak_ptr,
698           navigation_url, page_navigation_hosts_requested));
699 }
700 
OnFetchedTopHostsHintsStored()701 void OptimizationGuideHintsManager::OnFetchedTopHostsHintsStored() {
702   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
703   LOCAL_HISTOGRAM_BOOLEAN("OptimizationGuide.FetchedHints.Stored", true);
704 
705   if (!optimization_guide::features::ShouldPersistHintsToDisk()) {
706     // If we aren't persisting hints to disk, there's no point in purging
707     // hints from disk or starting a new fetch since at this point we should
708     // just be fetching everything on page navigation and only storing
709     // in-memory.
710     return;
711   }
712 
713   hint_cache_->PurgeExpiredFetchedHints();
714 
715   top_hosts_hints_fetch_timer_.Stop();
716   top_hosts_hints_fetch_timer_.Start(
717       FROM_HERE, hint_cache_->GetFetchedHintsUpdateTime() - clock_->Now(), this,
718       &OptimizationGuideHintsManager::ScheduleTopHostsHintsFetch);
719 }
720 
OnFetchedPageNavigationHintsStored(base::WeakPtr<OptimizationGuideNavigationData> navigation_data_weak_ptr,const base::Optional<GURL> & navigation_url,const base::flat_set<std::string> & page_navigation_hosts_requested)721 void OptimizationGuideHintsManager::OnFetchedPageNavigationHintsStored(
722     base::WeakPtr<OptimizationGuideNavigationData> navigation_data_weak_ptr,
723     const base::Optional<GURL>& navigation_url,
724     const base::flat_set<std::string>& page_navigation_hosts_requested) {
725   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
726 
727   if (navigation_data_weak_ptr) {
728     navigation_data_weak_ptr->set_hints_fetch_end(base::TimeTicks::Now());
729   }
730 
731   if (navigation_url) {
732     CleanUpFetcherForNavigation(*navigation_url);
733     PrepareToInvokeRegisteredCallbacks(*navigation_url);
734   }
735 }
736 
IsHintBeingFetchedForNavigation(const GURL & navigation_url)737 bool OptimizationGuideHintsManager::IsHintBeingFetchedForNavigation(
738     const GURL& navigation_url) {
739   return page_navigation_hints_fetchers_.Get(navigation_url) !=
740          page_navigation_hints_fetchers_.end();
741 }
742 
CleanUpFetcherForNavigation(const GURL & navigation_url)743 void OptimizationGuideHintsManager::CleanUpFetcherForNavigation(
744     const GURL& navigation_url) {
745   auto it = page_navigation_hints_fetchers_.Peek(navigation_url);
746   if (it != page_navigation_hints_fetchers_.end())
747     page_navigation_hints_fetchers_.Erase(it);
748 }
749 
GetLastHintsFetchAttemptTime() const750 base::Time OptimizationGuideHintsManager::GetLastHintsFetchAttemptTime() const {
751   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
752   return base::Time::FromDeltaSinceWindowsEpoch(
753       base::TimeDelta::FromMicroseconds(pref_service_->GetInt64(
754           optimization_guide::prefs::kHintsFetcherLastFetchAttempt)));
755 }
756 
SetLastHintsFetchAttemptTime(base::Time last_attempt_time)757 void OptimizationGuideHintsManager::SetLastHintsFetchAttemptTime(
758     base::Time last_attempt_time) {
759   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
760   pref_service_->SetInt64(
761       optimization_guide::prefs::kHintsFetcherLastFetchAttempt,
762       last_attempt_time.ToDeltaSinceWindowsEpoch().InMicroseconds());
763 }
764 
LoadHintForNavigation(content::NavigationHandle * navigation_handle,base::OnceClosure callback)765 void OptimizationGuideHintsManager::LoadHintForNavigation(
766     content::NavigationHandle* navigation_handle,
767     base::OnceClosure callback) {
768   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
769 
770   const auto& url = navigation_handle->GetURL();
771   if (!url.has_host()) {
772     std::move(callback).Run();
773     return;
774   }
775 
776   LoadHintForHost(url.host(), std::move(callback));
777 }
778 
LoadHintForHost(const std::string & host,base::OnceClosure callback)779 void OptimizationGuideHintsManager::LoadHintForHost(
780     const std::string& host,
781     base::OnceClosure callback) {
782   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
783 
784   hint_cache_->LoadHint(
785       host,
786       base::BindOnce(&OptimizationGuideHintsManager::OnHintLoaded,
787                      ui_weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
788 }
789 
IsGoogleURL(const GURL & url) const790 bool OptimizationGuideHintsManager::IsGoogleURL(const GURL& url) const {
791   return google_util::IsGoogleHostname(url.host(),
792                                        google_util::DISALLOW_SUBDOMAIN);
793 }
794 
IsAllowedToFetchForNavigationPrediction(const base::Optional<NavigationPredictorKeyedService::Prediction> prediction) const795 bool OptimizationGuideHintsManager::IsAllowedToFetchForNavigationPrediction(
796     const base::Optional<NavigationPredictorKeyedService::Prediction>
797         prediction) const {
798   if (!prediction)
799     return false;
800 
801   if (prediction->prediction_source() ==
802       NavigationPredictorKeyedService::PredictionSource::
803           kAnchorElementsParsedFromWebPage) {
804     const base::Optional<GURL> source_document_url =
805         prediction->source_document_url();
806     if (!source_document_url || source_document_url->is_empty())
807       return false;
808 
809     // We only extract next predicted navigations from Google URLs.
810     return IsGoogleURL(*source_document_url);
811   }
812 
813   if (prediction->prediction_source() ==
814       NavigationPredictorKeyedService::PredictionSource::kExternalAndroidApp) {
815     if (external_app_packages_approved_for_fetch_.empty())
816       return false;
817 
818     const base::Optional<std::vector<std::string>> external_app_packages_name =
819         prediction->external_app_packages_name();
820     if (!external_app_packages_name || external_app_packages_name->empty())
821       return false;
822 
823     for (const auto& package_name : *external_app_packages_name) {
824       if (external_app_packages_approved_for_fetch_.find(package_name) ==
825           external_app_packages_approved_for_fetch_.end())
826         return false;
827     }
828     // If we get here, all apps have been approved for fetching.
829     return true;
830   }
831 
832   return false;
833 }
834 
OnPredictionUpdated(const base::Optional<NavigationPredictorKeyedService::Prediction> prediction)835 void OptimizationGuideHintsManager::OnPredictionUpdated(
836     const base::Optional<NavigationPredictorKeyedService::Prediction>
837         prediction) {
838   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
839 
840   if (!IsAllowedToFetchForNavigationPrediction(prediction))
841     return;
842 
843   // Extract the target hosts and URLs. Use a flat set to remove duplicates.
844   // |target_hosts_serialized| is the ordered list of non-duplicate hosts.
845   // TODO(sophiechang): See if we can make this logic simpler.
846   base::flat_set<std::string> target_hosts;
847   std::vector<std::string> target_hosts_serialized;
848   std::vector<GURL> target_urls;
849   for (const auto& url : prediction->sorted_predicted_urls()) {
850     if (!IsAllowedToFetchNavigationHints(url))
851       continue;
852 
853     // Insert the host to |target_hosts|. The host is inserted to
854     // |target_hosts_serialized| only if it was not a duplicate insertion to
855     // |target_hosts|.
856     std::pair<base::flat_set<std::string>::iterator, bool> insert_result =
857         target_hosts.insert(url.host());
858     if (insert_result.second)
859       target_hosts_serialized.push_back(url.host());
860 
861     // Ensure that the 2 data structures remain synchronized.
862     DCHECK_EQ(target_hosts.size(), target_hosts_serialized.size());
863 
864     if (!hint_cache_->HasURLKeyedEntryForURL(url))
865       target_urls.push_back(url);
866   }
867 
868   if (target_hosts.empty() && target_urls.empty())
869     return;
870 
871   if (!batch_update_hints_fetcher_) {
872     DCHECK(hints_fetcher_factory_);
873     batch_update_hints_fetcher_ = hints_fetcher_factory_->BuildInstance();
874   }
875 
876   // Use the batch update hints fetcher for fetches off the SRP since we are
877   // not fetching for the current navigation, even though we are fetching using
878   // the page navigation context. However, since we do want to load the hints
879   // returned, we pass this through to the page navigation callback.
880   batch_update_hints_fetcher_->FetchOptimizationGuideServiceHints(
881       target_hosts_serialized, target_urls, registered_optimization_types_,
882       optimization_guide::proto::CONTEXT_BATCH_UPDATE,
883       base::BindOnce(
884           &OptimizationGuideHintsManager::OnPageNavigationHintsFetched,
885           ui_weak_ptr_factory_.GetWeakPtr(), nullptr, base::nullopt,
886           target_urls, target_hosts));
887 }
888 
OnHintLoaded(base::OnceClosure callback,const optimization_guide::proto::Hint * loaded_hint) const889 void OptimizationGuideHintsManager::OnHintLoaded(
890     base::OnceClosure callback,
891     const optimization_guide::proto::Hint* loaded_hint) const {
892   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
893 
894   // Record the result of loading a hint. This is used as a signal for testing.
895   LOCAL_HISTOGRAM_BOOLEAN(optimization_guide::kLoadedHintLocalHistogramString,
896                           loaded_hint);
897 
898   // Run the callback now that the hint is loaded. This is used as a signal by
899   // tests.
900   std::move(callback).Run();
901 }
902 
RegisterOptimizationTypes(const std::vector<optimization_guide::proto::OptimizationType> & optimization_types)903 void OptimizationGuideHintsManager::RegisterOptimizationTypes(
904     const std::vector<optimization_guide::proto::OptimizationType>&
905         optimization_types) {
906   bool should_load_new_optimization_filter = false;
907 
908   DictionaryPrefUpdate previously_registered_opt_types(
909       pref_service_,
910       optimization_guide::prefs::kPreviouslyRegisteredOptimizationTypes);
911   for (const auto optimization_type : optimization_types) {
912     if (optimization_type == optimization_guide::proto::TYPE_UNSPECIFIED)
913       continue;
914 
915     if (registered_optimization_types_.find(optimization_type) !=
916         registered_optimization_types_.end()) {
917       continue;
918     }
919     registered_optimization_types_.insert(optimization_type);
920 
921     base::Optional<double> value = previously_registered_opt_types->FindBoolKey(
922         optimization_guide::proto::OptimizationType_Name(optimization_type));
923     if (!value) {
924       if (!ShouldIgnoreNewlyRegisteredOptimizationType(optimization_type))
925         should_clear_hints_for_new_type_ = true;
926       previously_registered_opt_types->SetBoolKey(
927           optimization_guide::proto::OptimizationType_Name(optimization_type),
928           true);
929     }
930 
931     if (!should_load_new_optimization_filter) {
932       base::AutoLock lock(optimization_filters_lock_);
933       if (optimization_types_with_filter_.find(optimization_type) !=
934           optimization_types_with_filter_.end()) {
935         should_load_new_optimization_filter = true;
936       }
937     }
938   }
939 
940   // If the store is available, clear all hint state so newly registered types
941   // can have their hints immediately included in hint fetches.
942   if (hint_cache_->IsHintStoreAvailable() && should_clear_hints_for_new_type_) {
943     ClearHostKeyedHints();
944     should_clear_hints_for_new_type_ = false;
945   }
946 
947   if (should_load_new_optimization_filter) {
948     if (optimization_guide::switches::IsHintComponentProcessingDisabled()) {
949       std::unique_ptr<optimization_guide::proto::Configuration> manual_config =
950           optimization_guide::switches::ParseComponentConfigFromCommandLine();
951       if (manual_config->optimization_allowlists_size() > 0 ||
952           manual_config->optimization_blacklists_size() > 0) {
953         // Process any optimization filters passed via command line on the
954         // background thread.
955         background_task_runner_->PostTask(
956             FROM_HERE,
957             base::BindOnce(
958                 &OptimizationGuideHintsManager::ProcessOptimizationFilters,
959                 base::Unretained(this),
960                 manual_config->optimization_allowlists(),
961                 manual_config->optimization_blacklists(),
962                 registered_optimization_types_));
963       }
964     } else {
965       DCHECK(hints_component_info_);
966       OnHintsComponentAvailable(*hints_component_info_);
967     }
968   } else {
969     MaybeRunUpdateClosure(std::move(next_update_closure_));
970   }
971 }
972 
HasLoadedOptimizationAllowlist(optimization_guide::proto::OptimizationType optimization_type)973 bool OptimizationGuideHintsManager::HasLoadedOptimizationAllowlist(
974     optimization_guide::proto::OptimizationType optimization_type) {
975   base::AutoLock lock(optimization_filters_lock_);
976 
977   return allowlist_optimization_filters_.find(optimization_type) !=
978          allowlist_optimization_filters_.end();
979 }
980 
HasLoadedOptimizationBlocklist(optimization_guide::proto::OptimizationType optimization_type)981 bool OptimizationGuideHintsManager::HasLoadedOptimizationBlocklist(
982     optimization_guide::proto::OptimizationType optimization_type) {
983   base::AutoLock lock(optimization_filters_lock_);
984 
985   return blocklist_optimization_filters_.find(optimization_type) !=
986          blocklist_optimization_filters_.end();
987 }
988 
CanApplyOptimizationAsync(const GURL & navigation_url,const base::Optional<int64_t> & navigation_id,optimization_guide::proto::OptimizationType optimization_type,optimization_guide::OptimizationGuideDecisionCallback callback)989 void OptimizationGuideHintsManager::CanApplyOptimizationAsync(
990     const GURL& navigation_url,
991     const base::Optional<int64_t>& navigation_id,
992     optimization_guide::proto::OptimizationType optimization_type,
993     optimization_guide::OptimizationGuideDecisionCallback callback) {
994   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
995 
996   optimization_guide::OptimizationMetadata metadata;
997   optimization_guide::OptimizationTypeDecision type_decision =
998       CanApplyOptimization(navigation_url, navigation_id, optimization_type,
999                            &metadata);
1000   optimization_guide::OptimizationGuideDecision decision = optimization_guide::
1001       GetOptimizationGuideDecisionFromOptimizationTypeDecision(type_decision);
1002   // It's possible that a hint that applies to |navigation_url| will come in
1003   // later, so only run the callback if we are sure we can apply the decision.
1004   if (decision == optimization_guide::OptimizationGuideDecision::kTrue ||
1005       HasAllInformationForDecisionAvailable(navigation_url,
1006                                             optimization_type)) {
1007     base::UmaHistogramEnumeration(
1008         "OptimizationGuide.ApplyDecisionAsync." +
1009             optimization_guide::GetStringNameForOptimizationType(
1010                 optimization_type),
1011         type_decision);
1012     std::move(callback).Run(decision, metadata);
1013     return;
1014   }
1015 
1016   registered_callbacks_[navigation_url][optimization_type].push_back(
1017       std::make_pair(navigation_id, std::move(callback)));
1018 }
1019 
1020 optimization_guide::OptimizationTypeDecision
CanApplyOptimization(const GURL & navigation_url,const base::Optional<int64_t> & navigation_id,optimization_guide::proto::OptimizationType optimization_type,optimization_guide::OptimizationMetadata * optimization_metadata)1021 OptimizationGuideHintsManager::CanApplyOptimization(
1022     const GURL& navigation_url,
1023     const base::Optional<int64_t>& navigation_id,
1024     optimization_guide::proto::OptimizationType optimization_type,
1025     optimization_guide::OptimizationMetadata* optimization_metadata) {
1026   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1027 
1028   // Clear out optimization metadata if provided.
1029   if (optimization_metadata)
1030     *optimization_metadata = {};
1031 
1032   // If the type is not registered, we probably don't have a hint for it, so
1033   // just return.
1034   if (registered_optimization_types_.find(optimization_type) ==
1035       registered_optimization_types_.end()) {
1036     return optimization_guide::OptimizationTypeDecision::kNoHintAvailable;
1037   }
1038 
1039   // If the URL doesn't have a host, we cannot query the hint for it, so just
1040   // return early.
1041   if (!navigation_url.has_host())
1042     return optimization_guide::OptimizationTypeDecision::kNoHintAvailable;
1043   const auto& host = navigation_url.host();
1044 
1045   // Check if the URL should be filtered out if we have an optimization filter
1046   // for the type.
1047   {
1048     base::AutoLock lock(optimization_filters_lock_);
1049 
1050     // Check if we have an allowlist loaded into memory for it, and if we do,
1051     // see if the URL matches anything in the filter.
1052     if (allowlist_optimization_filters_.find(optimization_type) !=
1053         allowlist_optimization_filters_.end()) {
1054       return allowlist_optimization_filters_[optimization_type]->Matches(
1055                  navigation_url)
1056                  ? optimization_guide::OptimizationTypeDecision::
1057                        kAllowedByOptimizationFilter
1058                  : optimization_guide::OptimizationTypeDecision::
1059                        kNotAllowedByOptimizationFilter;
1060     }
1061 
1062     // Check if we have a blocklist loaded into memory for it, and if we do, see
1063     // if the URL matches anything in the filter.
1064     if (blocklist_optimization_filters_.find(optimization_type) !=
1065         blocklist_optimization_filters_.end()) {
1066       return blocklist_optimization_filters_[optimization_type]->Matches(
1067                  navigation_url)
1068                  ? optimization_guide::OptimizationTypeDecision::
1069                        kNotAllowedByOptimizationFilter
1070                  : optimization_guide::OptimizationTypeDecision::
1071                        kAllowedByOptimizationFilter;
1072     }
1073 
1074     // Check if we had an optimization filter for it, but it was not loaded into
1075     // memory.
1076     if (optimization_types_with_filter_.find(optimization_type) !=
1077         optimization_types_with_filter_.end()) {
1078       return optimization_guide::OptimizationTypeDecision::
1079           kHadOptimizationFilterButNotLoadedInTime;
1080     }
1081   }
1082 
1083   base::Optional<uint64_t> tuning_version;
1084 
1085   // First, check if the optimization type is whitelisted by a URL-keyed hint.
1086   const optimization_guide::proto::Hint* url_keyed_hint =
1087       hint_cache_->GetURLKeyedHint(navigation_url);
1088   if (url_keyed_hint) {
1089     DCHECK_EQ(url_keyed_hint->page_hints_size(), 1);
1090     if (url_keyed_hint->page_hints_size() > 0) {
1091       bool is_allowed = IsOptimizationTypeAllowed(
1092           url_keyed_hint->page_hints(0).whitelisted_optimizations(),
1093           optimization_type, optimization_metadata, &tuning_version);
1094       if (is_allowed || tuning_version) {
1095         MaybeLogOptimizationAutotuningUKMForNavigation(
1096             navigation_id, optimization_type, tuning_version);
1097         return is_allowed ? optimization_guide::OptimizationTypeDecision::
1098                                 kAllowedByHint
1099                           : optimization_guide::OptimizationTypeDecision::
1100                                 kNotAllowedByHint;
1101       }
1102     }
1103   }
1104 
1105   // Check if we have a hint already loaded for this navigation.
1106   const optimization_guide::proto::Hint* loaded_hint =
1107       hint_cache_->GetHostKeyedHintIfLoaded(host);
1108   if (!loaded_hint) {
1109     if (hint_cache_->HasHint(host)) {
1110       // If we do not have a hint already loaded and we do not have one in the
1111       // cache, we do not know what to do with the URL so just return.
1112       // Otherwise, we do have information, but we just do not know it yet.
1113       if (optimization_guide::features::ShouldPersistHintsToDisk()) {
1114         return optimization_guide::OptimizationTypeDecision::
1115             kHadHintButNotLoadedInTime;
1116       } else {
1117         return optimization_guide::OptimizationTypeDecision::kNoHintAvailable;
1118       }
1119     }
1120 
1121     if (IsHintBeingFetchedForNavigation(navigation_url)) {
1122       return optimization_guide::OptimizationTypeDecision::
1123           kHintFetchStartedButNotAvailableInTime;
1124     }
1125 
1126     return optimization_guide::OptimizationTypeDecision::kNoHintAvailable;
1127   }
1128 
1129   bool is_allowed = IsOptimizationTypeAllowed(
1130       loaded_hint->whitelisted_optimizations(), optimization_type,
1131       optimization_metadata, &tuning_version);
1132   if (is_allowed || tuning_version) {
1133     MaybeLogOptimizationAutotuningUKMForNavigation(
1134         navigation_id, optimization_type, tuning_version);
1135     return is_allowed
1136                ? optimization_guide::OptimizationTypeDecision::kAllowedByHint
1137                : optimization_guide::OptimizationTypeDecision::
1138                      kNotAllowedByHint;
1139   }
1140 
1141   const optimization_guide::proto::PageHint* matched_page_hint =
1142       loaded_hint
1143           ? optimization_guide::FindPageHintForURL(navigation_url, loaded_hint)
1144           : nullptr;
1145   if (!matched_page_hint)
1146     return optimization_guide::OptimizationTypeDecision::kNotAllowedByHint;
1147 
1148   is_allowed = IsOptimizationTypeAllowed(
1149       matched_page_hint->whitelisted_optimizations(), optimization_type,
1150       optimization_metadata, &tuning_version);
1151   MaybeLogOptimizationAutotuningUKMForNavigation(
1152       navigation_id, optimization_type, tuning_version);
1153   return is_allowed
1154              ? optimization_guide::OptimizationTypeDecision::kAllowedByHint
1155              : optimization_guide::OptimizationTypeDecision::kNotAllowedByHint;
1156 }
1157 
PrepareToInvokeRegisteredCallbacks(const GURL & navigation_url)1158 void OptimizationGuideHintsManager::PrepareToInvokeRegisteredCallbacks(
1159     const GURL& navigation_url) {
1160   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1161 
1162   if (registered_callbacks_.find(navigation_url) == registered_callbacks_.end())
1163     return;
1164 
1165   LoadHintForHost(
1166       navigation_url.host(),
1167       base::BindOnce(
1168           &OptimizationGuideHintsManager::OnReadyToInvokeRegisteredCallbacks,
1169           ui_weak_ptr_factory_.GetWeakPtr(), navigation_url));
1170 }
1171 
OnReadyToInvokeRegisteredCallbacks(const GURL & navigation_url)1172 void OptimizationGuideHintsManager::OnReadyToInvokeRegisteredCallbacks(
1173     const GURL& navigation_url) {
1174   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1175 
1176   if (registered_callbacks_.find(navigation_url) ==
1177       registered_callbacks_.end()) {
1178     return;
1179   }
1180 
1181   for (auto& opt_type_and_callbacks :
1182        registered_callbacks_.at(navigation_url)) {
1183     optimization_guide::proto::OptimizationType opt_type =
1184         opt_type_and_callbacks.first;
1185 
1186     for (auto& navigation_id_and_callback : opt_type_and_callbacks.second) {
1187       base::Optional<int64_t> navigation_id = navigation_id_and_callback.first;
1188       optimization_guide::OptimizationMetadata metadata;
1189       optimization_guide::OptimizationTypeDecision type_decision =
1190           CanApplyOptimization(navigation_url, navigation_id, opt_type,
1191                                &metadata);
1192       optimization_guide::OptimizationGuideDecision decision =
1193           optimization_guide::
1194               GetOptimizationGuideDecisionFromOptimizationTypeDecision(
1195                   type_decision);
1196       base::UmaHistogramEnumeration(
1197           "OptimizationGuide.ApplyDecisionAsync." +
1198               optimization_guide::GetStringNameForOptimizationType(opt_type),
1199           type_decision);
1200       std::move(navigation_id_and_callback.second).Run(decision, metadata);
1201     }
1202   }
1203   registered_callbacks_.erase(navigation_url);
1204 }
1205 
OnEffectiveConnectionTypeChanged(net::EffectiveConnectionType effective_connection_type)1206 void OptimizationGuideHintsManager::OnEffectiveConnectionTypeChanged(
1207     net::EffectiveConnectionType effective_connection_type) {
1208   current_effective_connection_type_ = effective_connection_type;
1209 }
1210 
HasOptimizationTypeToFetchFor()1211 bool OptimizationGuideHintsManager::HasOptimizationTypeToFetchFor() {
1212   if (registered_optimization_types_.empty())
1213     return false;
1214 
1215   base::AutoLock lock(optimization_filters_lock_);
1216   for (const auto& optimization_type : registered_optimization_types_) {
1217     if (optimization_types_with_filter_.find(optimization_type) ==
1218         optimization_types_with_filter_.end()) {
1219       return true;
1220     }
1221   }
1222   return false;
1223 }
1224 
IsAllowedToFetchNavigationHints(const GURL & url)1225 bool OptimizationGuideHintsManager::IsAllowedToFetchNavigationHints(
1226     const GURL& url) {
1227   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1228 
1229   if (!HasOptimizationTypeToFetchFor())
1230     return false;
1231 
1232   if (!IsUserPermittedToFetchFromRemoteOptimizationGuide(profile_))
1233     return false;
1234 
1235   if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS())
1236     return false;
1237 
1238   base::Optional<net::EffectiveConnectionType> ect_max_threshold =
1239       optimization_guide::features::
1240           GetMaxEffectiveConnectionTypeForNavigationHintsFetch();
1241   // If the threshold is unavailable, return early since there is no safe way to
1242   // proceed.
1243   if (!ect_max_threshold.has_value())
1244     return false;
1245 
1246   if (current_effective_connection_type_ <
1247           net::EFFECTIVE_CONNECTION_TYPE_SLOW_2G ||
1248       current_effective_connection_type_ > ect_max_threshold.value()) {
1249     return false;
1250   }
1251 
1252   return true;
1253 }
1254 
OnNavigationStartOrRedirect(content::NavigationHandle * navigation_handle,base::OnceClosure callback)1255 void OptimizationGuideHintsManager::OnNavigationStartOrRedirect(
1256     content::NavigationHandle* navigation_handle,
1257     base::OnceClosure callback) {
1258   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1259 
1260   if (optimization_guide::switches::
1261           DisableFetchingHintsAtNavigationStartForTesting()) {
1262     return;
1263   }
1264 
1265   LoadHintForNavigation(navigation_handle, std::move(callback));
1266 
1267   MaybeFetchHintsForNavigation(navigation_handle);
1268 }
1269 
MaybeFetchHintsForNavigation(content::NavigationHandle * navigation_handle)1270 void OptimizationGuideHintsManager::MaybeFetchHintsForNavigation(
1271     content::NavigationHandle* navigation_handle) {
1272   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1273 
1274   if (registered_optimization_types_.empty())
1275     return;
1276 
1277   const GURL url = navigation_handle->GetURL();
1278   if (!IsAllowedToFetchNavigationHints(url))
1279     return;
1280 
1281   ScopedHintsManagerRaceNavigationHintsFetchAttemptRecorder
1282       race_navigation_recorder(navigation_handle);
1283 
1284   // We expect that if the URL is being fetched for, we have already run through
1285   // the logic to decide if we also require fetching hints for the host.
1286   if (IsHintBeingFetchedForNavigation(url)) {
1287     race_navigation_recorder.set_race_attempt_status(
1288         optimization_guide::RaceNavigationFetchAttemptStatus::
1289             kRaceNavigationFetchAlreadyInProgress);
1290     return;
1291   }
1292 
1293   std::vector<std::string> hosts;
1294   std::vector<GURL> urls;
1295   if (!hint_cache_->HasHint(url.host())) {
1296     hosts.push_back(url.host());
1297     race_navigation_recorder.set_race_attempt_status(
1298         optimization_guide::RaceNavigationFetchAttemptStatus::
1299             kRaceNavigationFetchHost);
1300   }
1301 
1302   if (!hint_cache_->HasURLKeyedEntryForURL(url)) {
1303     urls.push_back(url);
1304     race_navigation_recorder.set_race_attempt_status(
1305         optimization_guide::RaceNavigationFetchAttemptStatus::
1306             kRaceNavigationFetchURL);
1307   }
1308 
1309   if (hosts.empty() && urls.empty()) {
1310     race_navigation_recorder.set_race_attempt_status(
1311         optimization_guide::RaceNavigationFetchAttemptStatus::
1312             kRaceNavigationFetchNotAttempted);
1313     return;
1314   }
1315 
1316   DCHECK(hints_fetcher_factory_);
1317   auto it = page_navigation_hints_fetchers_.Put(
1318       url, hints_fetcher_factory_->BuildInstance());
1319 
1320   UMA_HISTOGRAM_COUNTS_100(
1321       "OptimizationGuide.HintsManager.ConcurrentPageNavigationFetches",
1322       page_navigation_hints_fetchers_.size());
1323 
1324   OptimizationGuideNavigationData* navigation_data =
1325       OptimizationGuideNavigationData::GetFromNavigationHandle(
1326           navigation_handle);
1327   navigation_data->set_hints_fetch_start(base::TimeTicks::Now());
1328   it->second->FetchOptimizationGuideServiceHints(
1329       hosts, urls, registered_optimization_types_,
1330       optimization_guide::proto::CONTEXT_PAGE_NAVIGATION,
1331       base::BindOnce(
1332           &OptimizationGuideHintsManager::OnPageNavigationHintsFetched,
1333           ui_weak_ptr_factory_.GetWeakPtr(), navigation_data->GetWeakPtr(), url,
1334           base::flat_set<GURL>({url}),
1335           base::flat_set<std::string>({url.host()})));
1336 
1337   if (!hosts.empty() && !urls.empty()) {
1338     race_navigation_recorder.set_race_attempt_status(
1339         optimization_guide::RaceNavigationFetchAttemptStatus::
1340             kRaceNavigationFetchHostAndURL);
1341   }
1342 }
1343 
OnNavigationFinish(const std::vector<GURL> & navigation_redirect_chain)1344 void OptimizationGuideHintsManager::OnNavigationFinish(
1345     const std::vector<GURL>& navigation_redirect_chain) {
1346   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1347 
1348   // The callbacks will be invoked when the fetch request comes back, so it
1349   // will be cleaned up later.
1350   for (const auto& url : navigation_redirect_chain) {
1351     if (IsHintBeingFetchedForNavigation(url))
1352       continue;
1353 
1354     PrepareToInvokeRegisteredCallbacks(url);
1355   }
1356 }
1357 
HasAllInformationForDecisionAvailable(const GURL & navigation_url,optimization_guide::proto::OptimizationType optimization_type)1358 bool OptimizationGuideHintsManager::HasAllInformationForDecisionAvailable(
1359     const GURL& navigation_url,
1360     optimization_guide::proto::OptimizationType optimization_type) {
1361   if (HasLoadedOptimizationAllowlist(optimization_type) ||
1362       HasLoadedOptimizationBlocklist(optimization_type)) {
1363     // If we have an optimization filter for the optimization type, it is
1364     // consulted instead of any hints that may be available.
1365     return true;
1366   }
1367 
1368   bool has_host_keyed_hint = hint_cache_->HasHint(navigation_url.host());
1369   const auto* host_keyed_hint =
1370       hint_cache_->GetHostKeyedHintIfLoaded(navigation_url.host());
1371   if (has_host_keyed_hint && host_keyed_hint == nullptr) {
1372     // If we have a host-keyed hint in the cache and it is not loaded, we do not
1373     // have all information available, regardless of whether we can fetch hints
1374     // or not.
1375     return false;
1376   }
1377 
1378   if (!IsAllowedToFetchNavigationHints(navigation_url)) {
1379     // If we are not allowed to fetch hints for the navigation, we have all
1380     // information available if the host-keyed hint we have has been loaded
1381     // already or we don't have a hint available.
1382     return host_keyed_hint != nullptr || !has_host_keyed_hint;
1383   }
1384 
1385   if (IsHintBeingFetchedForNavigation(navigation_url)) {
1386     // If a hint is being fetched for the navigation, then we do not have all
1387     // information available yet.
1388     return false;
1389   }
1390 
1391   // If we are allowed to fetch hints for the navigation, we only have all
1392   // information available for certain if we have attempted to get the URL-keyed
1393   // hint and if the host-keyed hint is loaded.
1394   return hint_cache_->HasURLKeyedEntryForURL(navigation_url) &&
1395          host_keyed_hint != nullptr;
1396 }
1397 
ClearFetchedHints()1398 void OptimizationGuideHintsManager::ClearFetchedHints() {
1399   hint_cache_->ClearFetchedHints();
1400   optimization_guide::HintsFetcher::ClearHostsSuccessfullyFetched(
1401       pref_service_);
1402 }
1403 
ClearHostKeyedHints()1404 void OptimizationGuideHintsManager::ClearHostKeyedHints() {
1405   hint_cache_->ClearHostKeyedHints();
1406   optimization_guide::HintsFetcher::ClearHostsSuccessfullyFetched(
1407       pref_service_);
1408 }
1409 
AddHintForTesting(const GURL & url,optimization_guide::proto::OptimizationType optimization_type,const base::Optional<optimization_guide::OptimizationMetadata> & metadata)1410 void OptimizationGuideHintsManager::AddHintForTesting(
1411     const GURL& url,
1412     optimization_guide::proto::OptimizationType optimization_type,
1413     const base::Optional<optimization_guide::OptimizationMetadata>& metadata) {
1414   std::unique_ptr<optimization_guide::proto::Hint> hint =
1415       std::make_unique<optimization_guide::proto::Hint>();
1416   hint->set_key(url.spec());
1417   optimization_guide::proto::PageHint* page_hint = hint->add_page_hints();
1418   page_hint->set_page_pattern("*");
1419   optimization_guide::proto::Optimization* optimization =
1420       page_hint->add_whitelisted_optimizations();
1421   optimization->set_optimization_type(optimization_type);
1422   if (!metadata) {
1423     hint_cache_->AddHintForTesting(url, std::move(hint));
1424     PrepareToInvokeRegisteredCallbacks(url);
1425     return;
1426   }
1427   if (metadata->previews_metadata()) {
1428     *optimization->mutable_previews_metadata() = *metadata->previews_metadata();
1429   } else if (metadata->loading_predictor_metadata()) {
1430     *optimization->mutable_loading_predictor_metadata() =
1431         *metadata->loading_predictor_metadata();
1432   } else if (metadata->performance_hints_metadata()) {
1433     *optimization->mutable_performance_hints_metadata() =
1434         *metadata->performance_hints_metadata();
1435   } else if (metadata->public_image_metadata()) {
1436     *optimization->mutable_public_image_metadata() =
1437         *metadata->public_image_metadata();
1438   } else if (metadata->any_metadata()) {
1439     *optimization->mutable_any_metadata() = *metadata->any_metadata();
1440   } else {
1441     NOTREACHED();
1442   }
1443   hint_cache_->AddHintForTesting(url, std::move(hint));
1444   PrepareToInvokeRegisteredCallbacks(url);
1445 }
1446 
OverrideTargetDecisionForTesting(optimization_guide::proto::OptimizationTarget optimization_target,optimization_guide::OptimizationGuideDecision optimization_guide_decision)1447 void OptimizationGuideHintsManager::OverrideTargetDecisionForTesting(
1448     optimization_guide::proto::OptimizationTarget optimization_target,
1449     optimization_guide::OptimizationGuideDecision optimization_guide_decision) {
1450   if (optimization_target !=
1451       optimization_guide::proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD) {
1452     return;
1453   }
1454 
1455   // Manipulate ECTs to effectively change the target decision.
1456   switch (optimization_guide_decision) {
1457     case optimization_guide::OptimizationGuideDecision::kTrue:
1458       current_effective_connection_type_ =
1459           net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_SLOW_2G;
1460       break;
1461     case optimization_guide::OptimizationGuideDecision::kFalse:
1462       current_effective_connection_type_ =
1463           net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_4G;
1464       break;
1465     case optimization_guide::OptimizationGuideDecision::kUnknown:
1466       // No way to override for |kUnknown|. Should not be used in tests.
1467       NOTREACHED();
1468       break;
1469   }
1470 }
1471