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