| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/predictors/loading_predictor.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "base/metrics/histogram_macros.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/predictors/loading_data_collector.h" |
| #include "chrome/browser/predictors/loading_stats_collector.h" |
| #include "chrome/browser/predictors/predictors_features.h" |
| #include "chrome/browser/predictors/resource_prefetch_predictor.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "net/base/network_isolation_key.h" |
| #include "url/origin.h" |
| |
| #if defined(OS_ANDROID) |
| #include "base/android/radio_utils.h" |
| #include "base/power_monitor/power_monitor.h" |
| #endif // defined(OS_ANDROID) |
| |
| namespace features { |
| |
| // Don't preconnect on weak signal to save power. |
| const base::Feature kNoPreconnectToSearchOnWeakSignal{ |
| "NoPreconnectToSearchOnWeakSignal", base::FEATURE_DISABLED_BY_DEFAULT}; |
| const base::Feature kNoNavigationPreconnectOnWeakSignal{ |
| "NoNavigationPreconnectOnWeakSignal", base::FEATURE_DISABLED_BY_DEFAULT}; |
| |
| } // namespace features |
| |
| namespace predictors { |
| |
| namespace { |
| |
| const base::TimeDelta kMinDelayBetweenPreresolveRequests = base::Seconds(60); |
| const base::TimeDelta kMinDelayBetweenPreconnectRequests = base::Seconds(10); |
| |
| // Returns true iff |prediction| is not empty. |
| bool AddInitialUrlToPreconnectPrediction(const GURL& initial_url, |
| PreconnectPrediction* prediction) { |
| url::Origin initial_origin = url::Origin::Create(initial_url); |
| // Open minimum 2 sockets to the main frame host to speed up the loading if a |
| // main page has a redirect to the same host. This is because there can be a |
| // race between reading the server redirect response and sending a new request |
| // while the connection is still in use. |
| static const int kMinSockets = 2; |
| |
| if (!prediction->requests.empty() && |
| prediction->requests.front().origin == initial_origin) { |
| prediction->requests.front().num_sockets = |
| std::max(prediction->requests.front().num_sockets, kMinSockets); |
| } else if (!initial_origin.opaque() && |
| (initial_origin.scheme() == url::kHttpScheme || |
| initial_origin.scheme() == url::kHttpsScheme)) { |
| prediction->requests.emplace( |
| prediction->requests.begin(), initial_origin, kMinSockets, |
| net::NetworkIsolationKey(initial_origin, initial_origin)); |
| } |
| |
| return !prediction->requests.empty(); |
| } |
| |
| bool IsPreconnectExpensive() { |
| #if defined(OS_ANDROID) |
| // Preconnecting is expensive while on battery power and cellular data and |
| // the radio signal is weak. |
| if ((base::PowerMonitor::IsInitialized() && |
| !base::PowerMonitor::IsOnBatteryPower()) || |
| (base::android::RadioUtils::GetConnectionType() != |
| base::android::RadioConnectionType::kCell)) { |
| return false; |
| } |
| |
| absl::optional<base::android::RadioSignalLevel> maybe_level = |
| base::android::RadioUtils::GetCellSignalLevel(); |
| return maybe_level.has_value() && |
| *maybe_level <= base::android::RadioSignalLevel::kModerate; |
| #else |
| return false; |
| #endif |
| } |
| |
| } // namespace |
| |
| LoadingPredictor::LoadingPredictor(const LoadingPredictorConfig& config, |
| Profile* profile) |
| : config_(config), |
| profile_(profile), |
| resource_prefetch_predictor_( |
| std::make_unique<ResourcePrefetchPredictor>(config, profile)), |
| stats_collector_(std::make_unique<LoadingStatsCollector>( |
| resource_prefetch_predictor_.get(), |
| config)), |
| loading_data_collector_(std::make_unique<LoadingDataCollector>( |
| resource_prefetch_predictor_.get(), |
| stats_collector_.get(), |
| config)) {} |
| |
| LoadingPredictor::~LoadingPredictor() { |
| DCHECK(shutdown_); |
| } |
| |
| bool LoadingPredictor::PrepareForPageLoad( |
| const GURL& url, |
| HintOrigin origin, |
| bool preconnectable, |
| absl::optional<PreconnectPrediction> preconnect_prediction) { |
| if (shutdown_) |
| return true; |
| |
| if (origin == HintOrigin::OMNIBOX) { |
| // Omnibox hints are lightweight and need a special treatment. |
| HandleOmniboxHint(url, preconnectable); |
| return true; |
| } |
| |
| PreconnectPrediction prediction; |
| bool has_local_preconnect_prediction = false; |
| if (features::ShouldUseLocalPredictions()) { |
| has_local_preconnect_prediction = |
| resource_prefetch_predictor_->PredictPreconnectOrigins(url, |
| &prediction); |
| } |
| if (active_hints_.find(url) != active_hints_.end() && |
| has_local_preconnect_prediction && !preconnect_prediction) { |
| // We are currently preconnecting using the local preconnect prediction. Do |
| // not proceed further. |
| return true; |
| } |
| |
| if (preconnect_prediction) { |
| // Overwrite the prediction if we were provided with a non-empty one. |
| prediction = *preconnect_prediction; |
| } else { |
| // Try to preconnect to the |url| even if the predictor has no |
| // prediction. |
| AddInitialUrlToPreconnectPrediction(url, &prediction); |
| } |
| |
| // Return early if we do not have any requests. |
| if (prediction.requests.empty() && prediction.prefetch_requests.empty()) |
| return false; |
| |
| ++total_hints_activated_; |
| active_hints_.emplace(url, base::TimeTicks::Now()); |
| if (IsPreconnectAllowed(profile_)) |
| MaybeAddPreconnect(url, std::move(prediction), origin); |
| return has_local_preconnect_prediction || preconnect_prediction; |
| } |
| |
| void LoadingPredictor::CancelPageLoadHint(const GURL& url) { |
| if (shutdown_) |
| return; |
| |
| CancelActiveHint(active_hints_.find(url)); |
| } |
| |
| void LoadingPredictor::StartInitialization() { |
| if (shutdown_) |
| return; |
| |
| resource_prefetch_predictor_->StartInitialization(); |
| } |
| |
| LoadingDataCollector* LoadingPredictor::loading_data_collector() { |
| return loading_data_collector_.get(); |
| } |
| |
| ResourcePrefetchPredictor* LoadingPredictor::resource_prefetch_predictor() { |
| return resource_prefetch_predictor_.get(); |
| } |
| |
| PreconnectManager* LoadingPredictor::preconnect_manager() { |
| if (shutdown_ || !IsPreconnectFeatureEnabled()) |
| return nullptr; |
| |
| if (!preconnect_manager_) { |
| preconnect_manager_ = |
| std::make_unique<PreconnectManager>(GetWeakPtr(), profile_); |
| } |
| |
| return preconnect_manager_.get(); |
| } |
| |
| PrefetchManager* LoadingPredictor::prefetch_manager() { |
| if (!base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch)) |
| return nullptr; |
| |
| if (shutdown_ || !IsPreconnectFeatureEnabled()) |
| return nullptr; |
| |
| if (!prefetch_manager_) { |
| prefetch_manager_ = |
| std::make_unique<PrefetchManager>(GetWeakPtr(), profile_); |
| } |
| |
| return prefetch_manager_.get(); |
| } |
| |
| void LoadingPredictor::Shutdown() { |
| DCHECK(!shutdown_); |
| resource_prefetch_predictor_->Shutdown(); |
| shutdown_ = true; |
| } |
| |
| bool LoadingPredictor::OnNavigationStarted(NavigationId navigation_id, |
| ukm::SourceId ukm_source_id, |
| const GURL& main_frame_url, |
| base::TimeTicks creation_time) { |
| if (shutdown_) |
| return true; |
| |
| loading_data_collector()->RecordStartNavigation( |
| navigation_id, ukm_source_id, main_frame_url, creation_time); |
| CleanupAbandonedHintsAndNavigations(navigation_id); |
| active_navigations_.emplace(navigation_id, |
| NavigationInfo{main_frame_url, creation_time}); |
| active_urls_to_navigations_[main_frame_url].insert(navigation_id); |
| return PrepareForPageLoad(main_frame_url, HintOrigin::NAVIGATION); |
| } |
| |
| void LoadingPredictor::OnNavigationFinished(NavigationId navigation_id, |
| const GURL& old_main_frame_url, |
| const GURL& new_main_frame_url, |
| bool is_error_page) { |
| if (shutdown_) |
| return; |
| |
| loading_data_collector()->RecordFinishNavigation( |
| navigation_id, old_main_frame_url, new_main_frame_url, is_error_page); |
| if (active_urls_to_navigations_.find(old_main_frame_url) != |
| active_urls_to_navigations_.end()) { |
| active_urls_to_navigations_[old_main_frame_url].erase(navigation_id); |
| if (active_urls_to_navigations_[old_main_frame_url].empty()) { |
| active_urls_to_navigations_.erase(old_main_frame_url); |
| } |
| } |
| active_navigations_.erase(navigation_id); |
| CancelPageLoadHint(old_main_frame_url); |
| } |
| |
| std::map<GURL, base::TimeTicks>::iterator LoadingPredictor::CancelActiveHint( |
| std::map<GURL, base::TimeTicks>::iterator hint_it) { |
| if (hint_it == active_hints_.end()) |
| return hint_it; |
| |
| const GURL& url = hint_it->first; |
| MaybeRemovePreconnect(url); |
| return active_hints_.erase(hint_it); |
| } |
| |
| void LoadingPredictor::CleanupAbandonedHintsAndNavigations( |
| NavigationId navigation_id) { |
| base::TimeTicks time_now = base::TimeTicks::Now(); |
| const base::TimeDelta max_navigation_age = |
| base::Seconds(config_.max_navigation_lifetime_seconds); |
| |
| // Hints. |
| for (auto it = active_hints_.begin(); it != active_hints_.end();) { |
| base::TimeDelta prefetch_age = time_now - it->second; |
| if (prefetch_age > max_navigation_age) { |
| // Will go to the last bucket in the duration reported in |
| // CancelActiveHint() meaning that the duration was unlimited. |
| it = CancelActiveHint(it); |
| } else { |
| ++it; |
| } |
| } |
| |
| // Navigations. |
| for (auto it = active_navigations_.begin(); |
| it != active_navigations_.end();) { |
| if ((it->first == navigation_id) || |
| (time_now - it->second.creation_time > max_navigation_age)) { |
| CancelActiveHint(active_hints_.find(it->second.main_frame_url)); |
| it = active_navigations_.erase(it); |
| } else { |
| ++it; |
| } |
| } |
| } |
| |
| void LoadingPredictor::MaybeAddPreconnect(const GURL& url, |
| PreconnectPrediction prediction, |
| HintOrigin origin) { |
| DCHECK(!shutdown_); |
| if (!prediction.prefetch_requests.empty()) { |
| DCHECK(base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch)); |
| prefetch_manager()->Start(url, std::move(prediction.prefetch_requests)); |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| features::kNoNavigationPreconnectOnWeakSignal) && |
| IsPreconnectExpensive()) { |
| return; |
| } |
| |
| if (!prediction.requests.empty()) |
| preconnect_manager()->Start(url, std::move(prediction.requests)); |
| } |
| |
| void LoadingPredictor::MaybeRemovePreconnect(const GURL& url) { |
| DCHECK(!shutdown_); |
| if (preconnect_manager_) |
| preconnect_manager_->Stop(url); |
| if (prefetch_manager_) |
| prefetch_manager_->Stop(url); |
| } |
| |
| void LoadingPredictor::HandleOmniboxHint(const GURL& url, bool preconnectable) { |
| if (!url.is_valid() || !url.has_host() || !IsPreconnectAllowed(profile_)) |
| return; |
| |
| url::Origin origin = url::Origin::Create(url); |
| bool is_new_origin = origin != last_omnibox_origin_; |
| last_omnibox_origin_ = origin; |
| net::NetworkIsolationKey network_isolation_key(origin, origin); |
| base::TimeTicks now = base::TimeTicks::Now(); |
| if (preconnectable) { |
| if (is_new_origin || now - last_omnibox_preconnect_time_ >= |
| kMinDelayBetweenPreconnectRequests) { |
| last_omnibox_preconnect_time_ = now; |
| preconnect_manager()->StartPreconnectUrl(url, true, |
| network_isolation_key); |
| } |
| return; |
| } |
| |
| if (is_new_origin || now - last_omnibox_preresolve_time_ >= |
| kMinDelayBetweenPreresolveRequests) { |
| last_omnibox_preresolve_time_ = now; |
| preconnect_manager()->StartPreresolveHost(url, network_isolation_key); |
| } |
| } |
| |
| void LoadingPredictor::PreconnectInitiated(const GURL& url, |
| const GURL& preconnect_url) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (shutdown_) |
| return; |
| |
| auto nav_id_set_it = active_urls_to_navigations_.find(url); |
| if (nav_id_set_it == active_urls_to_navigations_.end()) |
| return; |
| |
| for (const auto& nav_id : nav_id_set_it->second) |
| loading_data_collector_->RecordPreconnectInitiated(nav_id, preconnect_url); |
| } |
| |
| void LoadingPredictor::PreconnectFinished( |
| std::unique_ptr<PreconnectStats> stats) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (shutdown_) |
| return; |
| |
| DCHECK(stats); |
| active_hints_.erase(stats->url); |
| stats_collector_->RecordPreconnectStats(std::move(stats)); |
| } |
| |
| void LoadingPredictor::PrefetchInitiated(const GURL& url, |
| const GURL& prefetch_url) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (shutdown_) |
| return; |
| |
| auto nav_id_set_it = active_urls_to_navigations_.find(url); |
| if (nav_id_set_it == active_urls_to_navigations_.end()) |
| return; |
| |
| for (const auto& nav_id : nav_id_set_it->second) |
| loading_data_collector_->RecordPrefetchInitiated(nav_id, prefetch_url); |
| } |
| |
| void LoadingPredictor::PrefetchFinished(std::unique_ptr<PrefetchStats> stats) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (shutdown_) |
| return; |
| |
| active_hints_.erase(stats->url); |
| } |
| |
| void LoadingPredictor::PreconnectURLIfAllowed( |
| const GURL& url, |
| bool allow_credentials, |
| const net::NetworkIsolationKey& network_isolation_key) { |
| if (!url.is_valid() || !url.has_host() || !IsPreconnectAllowed(profile_)) |
| return; |
| |
| if (base::FeatureList::IsEnabled( |
| features::kNoPreconnectToSearchOnWeakSignal) && |
| IsPreconnectExpensive()) { |
| return; |
| } |
| |
| preconnect_manager()->StartPreconnectUrl(url, allow_credentials, |
| network_isolation_key); |
| } |
| |
| } // namespace predictors |