blob: a5c157352095c6b4a77898cb3d1579617d35abb7 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// 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_functions.h"
#include "build/build_config.h"
#include "chrome/browser/after_startup_task_utils.h"
#include "chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.h"
#include "chrome/browser/predictors/lcp_critical_path_predictor/prewarm_http_disk_cache_manager.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 "content/public/browser/service_worker_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/origin_util.h"
#include "net/base/network_anonymization_key.h"
#include "services/network/public/cpp/request_destination.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/origin.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/android/radio_utils.h"
#include "base/power_monitor/power_monitor.h"
#endif // BUILDFLAG(IS_ANDROID)
namespace features {
// Don't preconnect on weak signal to save power.
BASE_FEATURE(kNoPreconnectToSearchOnWeakSignal,
"NoPreconnectToSearchOnWeakSignal",
base::FEATURE_DISABLED_BY_DEFAULT);
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::NetworkAnonymizationKey::CreateSameSite(
net::SchemefulSite(initial_origin)));
}
return !prediction->requests.empty();
}
bool IsPreconnectExpensive() {
#if BUILDFLAG(IS_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;
}
std::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
}
void MaybeWarmUpServiceWorker(const GURL& url, Profile* profile) {
if (!base::FeatureList::IsEnabled(
blink::features::kSpeculativeServiceWorkerWarmUp) ||
!blink::features::kSpeculativeServiceWorkerWarmUpFromLoadingPredictor
.Get()) {
return;
}
if (!profile) {
return;
}
content::StoragePartition* storage_partition =
profile->GetDefaultStoragePartition();
if (!storage_partition) {
return;
}
content::ServiceWorkerContext* service_worker_context =
storage_partition->GetServiceWorkerContext();
if (!service_worker_context) {
return;
}
if (!content::OriginCanAccessServiceWorkers(url)) {
return;
}
const blink::StorageKey key =
blink::StorageKey::CreateFirstParty(url::Origin::Create(url));
if (!service_worker_context->MaybeHasRegistrationForStorageKey(key)) {
return;
}
service_worker_context->WarmUpServiceWorker(url, key, base::DoNothing());
}
} // 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,
std::optional<PreconnectPrediction> preconnect_prediction) {
if (shutdown_)
return true;
// Prewarm disk cache before preconnecting network.
MaybePrewarmResources(url);
MaybeWarmUpServiceWorker(url, profile_);
if (origin == HintOrigin::OMNIBOX) {
// Omnibox hints are lightweight and need a special treatment.
HandleHintByOrigin(url, preconnectable, /*only_allow_https=*/false,
omnibox_preconnect_data_);
return true;
}
if (origin == HintOrigin::BOOKMARK_BAR) {
// Bookmark hints are lightweight and need a special treatment.
HandleHintByOrigin(url, /*preconnectable=*/true, /*only_allow_https=*/true,
bookmark_bar_preconnect_data_);
return true;
}
if (origin == HintOrigin::NEW_TAB_PAGE) {
// New Tab Page hints are lightweight and need a special treatment.
HandleHintByOrigin(url, /*preconnectable=*/true, /*only_allow_https=*/true,
new_tab_page_preconnect_data_);
return true;
}
PreconnectPrediction prediction;
bool has_local_preconnect_prediction = false;
if (origin == HintOrigin::OPTIMIZATION_GUIDE) {
CHECK(preconnect_prediction);
prediction = *preconnect_prediction;
} else {
CHECK(!preconnect_prediction);
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) {
// We are currently preconnecting using the local preconnect prediction.
// Do not proceed further.
return true;
}
// Try to preconnect to the |url| even if the predictor has no
// prediction.
AddInitialUrlToPreconnectPrediction(url, &prediction);
}
// LCPP: AutoPreconnectLCPOrigins experiment (crbug.com/1518996)
// Preconnect to LCPP predicted LCP origins in all platforms including those
// without optimization guide.
if (base::FeatureList::IsEnabled(
blink::features::kLCPPAutoPreconnectLcpOrigin)) {
std::optional<LcppStat> lcpp_stat =
resource_prefetch_predictor()->GetLcppStat(url);
if (lcpp_stat) {
auto network_anonymization_key =
net::NetworkAnonymizationKey::CreateSameSite(
net::SchemefulSite(url::Origin::Create(url)));
size_t count = 0;
for (const GURL& preconnect_origin :
PredictPreconnectableOrigins(*lcpp_stat)) {
prediction.requests.emplace_back(url::Origin::Create(preconnect_origin),
1, network_anonymization_key);
++count;
}
base::UmaHistogramCounts10000("Blink.LCPP.PreconnectPredictionCount",
count);
}
}
// LCPP: set fonts to be prefetched to prefetch_requests.
// TODO(crbug.com/40285959): make prefetch work for platforms without the
// optimization guide.
if (base::FeatureList::IsEnabled(blink::features::kLCPPFontURLPredictor) &&
blink::features::kLCPPFontURLPredictorEnablePrefetch.Get() &&
base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch) &&
features::kLoadingPredictorPrefetchSubresourceType.Get() ==
features::PrefetchSubresourceType::kAll) {
std::optional<LcppStat> lcpp_stat =
resource_prefetch_predictor()->GetLcppStat(url);
if (lcpp_stat) {
auto network_anonymization_key =
net::NetworkAnonymizationKey::CreateSameSite(
net::SchemefulSite(url::Origin::Create(url)));
size_t count = 0;
for (const GURL& font_url : PredictFetchedFontUrls(*lcpp_stat)) {
prediction.prefetch_requests.emplace_back(
font_url, network_anonymization_key,
network::mojom::RequestDestination::kFont);
++count;
}
base::UmaHistogramCounts1000("Blink.LCPP.PrefetchFontCount", count);
}
}
// 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));
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() {
CHECK(!shutdown_);
if (!preconnect_manager_) {
preconnect_manager_ =
std::make_unique<PreconnectManager>(GetWeakPtr(), profile_);
}
return preconnect_manager_.get();
}
PrefetchManager* LoadingPredictor::prefetch_manager() {
CHECK(base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch));
CHECK(!shutdown_);
if (!prefetch_manager_) {
prefetch_manager_ =
std::make_unique<PrefetchManager>(GetWeakPtr(), profile_);
}
return prefetch_manager_.get();
}
void LoadingPredictor::Shutdown() {
DCHECK(!shutdown_);
resource_prefetch_predictor_->Shutdown();
preconnect_manager_.reset();
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, 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) {
CHECK(!shutdown_);
if (!prediction.prefetch_requests.empty() &&
(AfterStartupTaskUtils::IsBrowserStartupComplete() ||
!base::FeatureList::IsEnabled(
features::kAvoidLoadingPredictorPrefetchDuringBrowserStartup))) {
CHECK(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);
}
bool LoadingPredictor::HandleHintByOrigin(const GURL& url,
bool preconnectable,
bool only_allow_https,
PreconnectData& preconnect_data) {
if (!url.is_valid() || !url.has_host() || !IsPreconnectAllowed(profile_) ||
(only_allow_https && url.scheme() != url::kHttpsScheme)) {
return false;
}
const url::Origin origin = url::Origin::Create(url);
// When constructing an Origin from a GURL results in an opaque origin, the
// resulting origin is guaranteed to be unique; trying to create another
// origin from the same URL will result in a different unique opaque origin,
// so any preconnect attempt would never be used anyway.
if (origin.opaque()) {
return false;
}
// Tracking whether this is a new origin request. If so, then
// preconnect/presolve immediately. If the origins are the same, then
// preconnect/presolve after a given threshold.
const bool is_new_origin = origin != preconnect_data.last_origin_;
preconnect_data.last_origin_ = origin;
const net::SchemefulSite site = net::SchemefulSite(origin);
const auto network_anonymization_key =
net::NetworkAnonymizationKey::CreateSameSite(site);
base::TimeTicks now = base::TimeTicks::Now();
if (preconnectable) {
if (is_new_origin || now - preconnect_data.last_preconnect_time_ >=
kMinDelayBetweenPreconnectRequests) {
preconnect_data.last_preconnect_time_ = now;
preconnect_manager()->StartPreconnectUrl(url, true,
network_anonymization_key);
}
return true;
}
if (is_new_origin || now - preconnect_data.last_preresolve_time_ >=
kMinDelayBetweenPreresolveRequests) {
preconnect_data.last_preresolve_time_ = now;
preconnect_manager()->StartPreresolveHost(url, network_anonymization_key);
return true;
}
return false;
}
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::NetworkAnonymizationKey& network_anonymization_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_anonymization_key);
}
void LoadingPredictor::MaybePrewarmResources(
const GURL& top_frame_main_resource_url) {
if (!base::FeatureList::IsEnabled(
blink::features::kHttpDiskCachePrewarming)) {
return;
}
if (shutdown_) {
return;
}
if (!top_frame_main_resource_url.is_valid() ||
!top_frame_main_resource_url.SchemeIsHTTPOrHTTPS()) {
return;
}
std::optional<LcppStat> lcpp_stat =
resource_prefetch_predictor()->GetLcppStat(top_frame_main_resource_url);
if (!lcpp_stat || !IsValidLcppStat(*lcpp_stat)) {
return;
}
if (!prewarm_http_disk_cache_manager_) {
prewarm_http_disk_cache_manager_ =
std::make_unique<PrewarmHttpDiskCacheManager>(
profile_->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess());
}
prewarm_http_disk_cache_manager_->MaybePrewarmResources(
top_frame_main_resource_url, PredictFetchedSubresourceUrls(*lcpp_stat));
}
} // namespace predictors