blob: c41be64450105fe56fc57aa1bc975f049c176ccb [file] [log] [blame]
// Copyright 2019 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/navigation_predictor/search_engine_preconnector.h"
#include <limits>
#include "base/functional/bind.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/time/default_tick_clock.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_features.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service_factory.h"
#include "chrome/browser/predictors/loading_predictor_config.h"
#include "chrome/browser/predictors/predictors_traffic_annotations.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/browser_context.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/features.h"
#include "net/base/reconnect_notifier.h"
namespace {
#if BUILDFLAG(IS_ANDROID)
const int kDefaultStartupDelayMs = 0;
const bool kDefaultSkipInBackground = false;
#else
const int kDefaultStartupDelayMs = 5000;
const bool kDefaultSkipInBackground = true;
#endif
constexpr int kPreconnectIntervalSec = 60;
constexpr int kPreconnectRetryDelayMs = 50;
} // namespace
namespace features {
// Feature to control preconnect to search.
BASE_FEATURE(kPreconnectFromKeyedService,
"PreconnectFromKeyedService",
base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kPreconnectToSearch,
"PreconnectToSearch",
base::FEATURE_ENABLED_BY_DEFAULT);
} // namespace features
WebContentVisibilityManager::WebContentVisibilityManager()
: tick_clock_(base::DefaultTickClock::GetInstance()) {}
WebContentVisibilityManager::~WebContentVisibilityManager() = default;
void WebContentVisibilityManager::OnWebContentsVisibilityChanged(
content::WebContents* web_contents,
bool is_in_foreground) {
visible_web_contents_.erase(web_contents);
last_web_contents_state_change_time_ = tick_clock_->NowTicks();
if (is_in_foreground) {
visible_web_contents_.insert(web_contents);
}
}
void WebContentVisibilityManager::OnWebContentsDestroyed(
content::WebContents* web_contents) {
visible_web_contents_.erase(web_contents);
last_web_contents_state_change_time_ = tick_clock_->NowTicks();
}
bool WebContentVisibilityManager::IsBrowserAppLikelyInForeground() const {
// If no web contents is in foreground, then allow a very short cool down
// period before considering app in background. This cooldown period is
// needed since when switching between the tabs, none of the web contents is
// in foreground for a very short period.
if (visible_web_contents_.empty() &&
tick_clock_->NowTicks() - last_web_contents_state_change_time_ >
base::Seconds(1)) {
return false;
}
return tick_clock_->NowTicks() - last_web_contents_state_change_time_ <=
base::Seconds(120);
}
void WebContentVisibilityManager::SetTickClockForTesting(
const base::TickClock* tick_clock) {
tick_clock_ = tick_clock;
}
bool SearchEnginePreconnector::ShouldBeEnabledAsKeyedService() {
static bool preconnect_from_keyed_service =
base::FeatureList::IsEnabled(features::kPreconnectFromKeyedService);
return preconnect_from_keyed_service;
}
bool SearchEnginePreconnector::ShouldBeEnabledForOffTheRecord() {
static bool enabled_for_otr = base::GetFieldTrialParamByFeatureAsBool(
features::kPreconnectFromKeyedService, "run_on_otr", false);
return enabled_for_otr;
}
bool SearchEnginePreconnector::SearchEnginePreconnect2Enabled() {
static bool preconnect2_enabled =
base::FeatureList::IsEnabled(net::features::kSearchEnginePreconnect2);
return preconnect2_enabled;
}
SearchEnginePreconnector::SearchEnginePreconnector(
content::BrowserContext* browser_context)
: browser_context_(browser_context) {
DCHECK(ShouldBeEnabledForOffTheRecord() ||
!browser_context_->IsOffTheRecord());
}
SearchEnginePreconnector::~SearchEnginePreconnector() = default;
void SearchEnginePreconnector::StopPreconnecting() {
preconnector_started_ = false;
timer_.Stop();
}
void SearchEnginePreconnector::StartPreconnecting(bool with_startup_delay) {
preconnector_started_ = true;
timer_.Stop();
if (with_startup_delay) {
StartPreconnectWithDelay(
base::Milliseconds(base::GetFieldTrialParamByFeatureAsInt(
features::kPreconnectToSearch, "startup_delay_ms",
kDefaultStartupDelayMs)),
PreconnectTriggerEvent::kInitialPreconnect);
return;
}
PreconnectDSE();
}
void SearchEnginePreconnector::PreconnectDSE() {
DCHECK(ShouldBeEnabledForOffTheRecord() ||
!browser_context_->IsOffTheRecord());
DCHECK(!timer_.IsRunning());
if (!base::FeatureList::IsEnabled(features::kPreconnectToSearch))
return;
// Don't preconnect unless the user allows search suggestions.
if (!Profile::FromBrowserContext(browser_context_)
->GetPrefs()
->GetBoolean(prefs::kSearchSuggestEnabled))
return;
GURL preconnect_url = GetDefaultSearchEngineOriginURL();
if (preconnect_url.scheme() != url::kHttpScheme &&
preconnect_url.scheme() != url::kHttpsScheme) {
return;
}
if (!preconnect_url.is_valid() || !preconnect_url.has_host()) {
return;
}
if (!predictors::IsPreconnectAllowed(
Profile::FromBrowserContext(browser_context_))) {
return;
}
const bool is_browser_app_likely_in_foreground =
IsBrowserAppLikelyInForeground();
base::UmaHistogramBoolean(
"NavigationPredictor.SearchEnginePreconnector."
"IsBrowserAppLikelyInForeground",
is_browser_app_likely_in_foreground);
std::optional<net::ConnectionKeepAliveConfig> keepalive_config;
mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient> observer;
if (SearchEnginePreconnect2Enabled()) {
keepalive_config = net::ConnectionKeepAliveConfig();
keepalive_config->idle_timeout_in_seconds =
net::features::kIdleTimeoutInSeconds.Get();
keepalive_config->ping_interval_in_seconds =
net::features::kPingIntervalInSeconds.Get();
keepalive_config->enable_connection_keep_alive = true;
keepalive_config->quic_connection_options =
net::features::kQuicConnectionOptions.Get();
if (!receiver_.is_bound()) {
observer = receiver_.BindNewPipeAndPassRemote();
receiver_.set_disconnect_handler(base::BindOnce(
&SearchEnginePreconnector::OnReconnectObserverPipeDisconnected,
base::Unretained(this)));
}
}
if (!base::GetFieldTrialParamByFeatureAsBool(features::kPreconnectToSearch,
"skip_in_background",
kDefaultSkipInBackground) ||
is_browser_app_likely_in_foreground) {
net::SchemefulSite schemeful_site(preconnect_url);
auto network_anonymziation_key =
net::NetworkAnonymizationKey::CreateSameSite(schemeful_site);
GetPreconnectManager().StartPreconnectUrl(
preconnect_url, /*allow_credentials=*/true, network_anonymziation_key,
predictors::kSearchEnginePreconnectTrafficAnnotation,
/*storage_partition_config=*/nullptr, std::move(keepalive_config),
std::move(observer));
}
// Periodically preconnect to the DSE. If the browser app is likely in
// background, we will reattempt preconnect later.
if (!SearchEnginePreconnect2Enabled()) {
StartPreconnectWithDelay(GetPreconnectInterval(),
PreconnectTriggerEvent::kPeriodicPreconnect);
}
last_preconnect_attempt_time_ = base::TimeTicks::Now();
}
GURL SearchEnginePreconnector::GetDefaultSearchEngineOriginURL() const {
auto* template_service = TemplateURLServiceFactory::GetForProfile(
Profile::FromBrowserContext(browser_context_));
if (!template_service)
return GURL();
const auto* search_provider = template_service->GetDefaultSearchProvider();
if (!search_provider || !search_provider->data().preconnect_to_search_url)
return GURL();
return search_provider->GenerateSearchURL({}).DeprecatedGetOriginAsURL();
}
base::TimeDelta SearchEnginePreconnector::GetPreconnectInterval() const {
if (!SearchEnginePreconnect2Enabled()) {
int preconnect_interval = base::GetFieldTrialParamByFeatureAsInt(
net::features::kSearchEnginePreconnectInterval, "preconnect_interval",
kPreconnectIntervalSec);
// Add an extra delay to make sure the preconnect has expired if it wasn't
// used.
return base::Seconds(preconnect_interval) +
base::Milliseconds(kPreconnectRetryDelayMs);
}
// If this is the first time failing, we should instantly retry, but we wait
// a very small amount of time since a closed connection would likely mean
// that there were something wrong in the connection.
// Otherwise, we backoff `kPreconnectRetryDelayMs` (currently 50 ms) * 2^n for
// the next preconnect attempt.
return std::min(
base::Milliseconds(kPreconnectRetryDelayMs) *
CalculateBackoffMultiplier(),
base::Seconds(net::features::kMaxPreconnectRetryInterval.Get()));
}
int32_t SearchEnginePreconnector::CalculateBackoffMultiplier() const {
return 1 << std::min(static_cast<int>(consecutive_connection_failure_),
std::numeric_limits<int32_t>::digits - 1);
}
bool SearchEnginePreconnector::IsShortSession() const {
CHECK(last_preconnect_attempt_time_.has_value());
if (is_short_session_for_testing_.has_value()) {
return is_short_session_for_testing_.value();
}
base::TimeDelta session_time =
base::TimeTicks::Now() - last_preconnect_attempt_time_.value();
// If the current session duration is shorter than the idle timeout, we
// consider the session to be short.
return session_time < net::features::kShortSessionThreshold.Get();
}
void SearchEnginePreconnector::StartPreconnectWithDelay(
base::TimeDelta delay,
PreconnectTriggerEvent event) {
RecordPreconnectAttemptHistogram(delay, event);
// Set/Reset the timer to fire after the specified `delay`.
timer_.Start(FROM_HERE, delay,
base::BindOnce(&SearchEnginePreconnector::PreconnectDSE,
base::Unretained(this)));
}
predictors::PreconnectManager&
SearchEnginePreconnector::GetPreconnectManager() {
if (!preconnect_manager_) {
preconnect_manager_ = std::make_unique<predictors::PreconnectManager>(
GetWeakPtr(), Profile::FromBrowserContext(browser_context_));
}
return *preconnect_manager_.get();
}
void SearchEnginePreconnector::OnWebContentsVisibilityChanged(
content::WebContents* web_contents,
bool is_in_foreground) {
WebContentVisibilityManager::OnWebContentsVisibilityChanged(web_contents,
is_in_foreground);
if (!SearchEnginePreconnect2Enabled()) {
return;
}
// Early stop when we know that the visibility change did not trigger
// foregrounding of the app and also when the preconnector is not started.
if (!IsBrowserAppLikelyInForeground() || !preconnector_started_) {
return;
}
// Stop the timer explicitly here so that we do not have any duplicate
// attempts.
timer_.Stop();
// Attempt reconnect again in case the visibility has changed after the last
// preconnect attempt so that we will preconnect sooner.
PreconnectDSE();
}
void SearchEnginePreconnector::OnSessionClosed() {
if (IsShortSession()) {
// If we have a short session, we consider that the session was closed due
// to an error, and will consider as a failed connection as well.
consecutive_connection_failure_++;
} else {
// If the last session was not short, then it must mean that the connection
// was successful. Reset the failure count.
base::UmaHistogramCounts1000(
"NavigationPredictor.SearchEnginePreconnector.ConsecutiveFailures",
consecutive_connection_failure_);
consecutive_connection_failure_ = 0;
}
StartPreconnectWithDelay(GetPreconnectInterval(),
PreconnectTriggerEvent::kSessionClosed);
}
void SearchEnginePreconnector::OnNetworkEvent(net::NetworkChangeEvent event) {
// If the network event is `Connected`, we attempt preconnect. Otherwise,
// we will ignore the events for now.
if (event == net::NetworkChangeEvent::kConnected) {
StartPreconnectWithDelay(base::Milliseconds(kPreconnectRetryDelayMs),
PreconnectTriggerEvent::kNetworkEvent);
}
}
void SearchEnginePreconnector::OnConnectionFailed() {
consecutive_connection_failure_++;
StartPreconnectWithDelay(GetPreconnectInterval(),
PreconnectTriggerEvent::kConnectionFailed);
}
void SearchEnginePreconnector::OnReconnectObserverPipeDisconnected() {
receiver_.reset();
// Only call `OnConnectionFailed` when the `timer_` is not running since we
// might already be waiting for reconnect attempt from other reasons.
if (!timer_.IsRunning()) {
OnConnectionFailed();
}
}
void SearchEnginePreconnector::RecordPreconnectAttemptHistogram(
base::TimeDelta delay,
PreconnectTriggerEvent event) {
base::UmaHistogramEnumeration(
"NavigationPredictor.SearchEnginePreconnector.TriggerEvent", event);
base::UmaHistogramLongTimes(
"NavigationPredictor.SearchEnginePreconnector.PreconnectDelay", delay);
if (last_preconnect_attempt_time_.has_value()) {
base::UmaHistogramLongTimes(
"NavigationPredictor.SearchEnginePreconnector."
"PreconnectAttemptInterval",
base::TimeTicks::Now() - last_preconnect_attempt_time_.value());
}
}