blob: 69444ac86028e41dc8dd041b9b912e610c51445f [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/preloading/prefetch/prefetch_canary_checker.h"
#include <cmath>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "content/browser/preloading/prefetch/prefetch_dns_prober.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/storage_partition.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/base/net_errors.h"
#include "services/network/public/cpp/network_connection_tracker.h"
#include "services/network/public/mojom/network_context.mojom.h"
#if BUILDFLAG(IS_ANDROID)
#include "net/android/network_library.h"
#include "net/base/network_interfaces.h"
#endif
namespace content {
namespace {
// The maximum number of canary checks to cache. Each entry corresponds to
// a network the user was on during a single Chrome session, and cache misses
// are cheap so there's no reason to use a large value.
const size_t kMaxCacheSize = 4;
const char kFinalResultHistogram[] = "PrefetchProxy.CanaryChecker.FinalState";
const char kTimeUntilSuccess[] = "PrefetchProxy.CanaryChecker.TimeUntilSuccess";
const char kTimeUntilFailure[] = "PrefetchProxy.CanaryChecker.TimeUntilFailure";
const char kAttemptsBeforeSuccessHistogram[] =
"PrefetchProxy.CanaryChecker.NumAttemptsBeforeSuccess";
const char kNetErrorHistogram[] = "PrefetchProxy.CanaryChecker.NetError";
const char kCacheEntryAgeHistogram[] =
"PrefetchProxy.CanaryChecker.CacheEntryAge";
const char kCacheLookupResult[] =
"PrefetchProxy.CanaryChecker.CacheLookupResult";
// These values are persisted to UMA logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class CanaryCheckLookupResult {
kSuccess = 0,
kFailure = 1,
kCacheMiss = 2,
kMaxValue = kCacheMiss,
};
// Please keep this up to date with logged histogram suffix
// |PrefetchProxy.CanaryChecker.Clients| in
// //tools/metrics/histograms/metadata/prefetch/histograms.xml.
std::string NameForClient(PrefetchCanaryChecker::CheckType name) {
switch (name) {
case PrefetchCanaryChecker::CheckType::kTLS:
return "TLS";
case PrefetchCanaryChecker::CheckType::kDNS:
return "DNS";
default:
NOTREACHED() << static_cast<int>(name);
}
}
std::string GenerateNetworkID(network::mojom::ConnectionType connection_type) {
std::string id = base::NumberToString(static_cast<int>(connection_type));
bool is_cellular =
network::NetworkConnectionTracker::IsConnectionCellular(connection_type);
if (is_cellular) {
// Don't care about cell connection type.
id = "cell";
}
// Further identify WiFi and cell connections. These calls are only supported
// for Android devices.
#if BUILDFLAG(IS_ANDROID)
if (connection_type == network::mojom::ConnectionType::CONNECTION_WIFI) {
return base::StringPrintf("%s,%s", id.c_str(), net::GetWifiSSID().c_str());
}
if (is_cellular) {
return base::StringPrintf(
"%s,%s", id.c_str(),
net::android::GetTelephonyNetworkOperator().c_str());
}
#endif
return id;
}
void UpdateCacheWithNetworkID(
network::NetworkConnectionTracker* network_connection_tracker,
base::OnceCallback<void(std::string key)> updateCallBack) {
base::OnceCallback<void(network::mojom::ConnectionType connection_type)>
connectionTypeCallback = base::BindOnce(
[](base::OnceCallback<void(std::string key)> updateCallBack,
network::mojom::ConnectionType connection_type) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(GenerateNetworkID, connection_type),
std::move(updateCallBack));
},
std::move(updateCallBack));
auto split = base::SplitOnceCallback(std::move(connectionTypeCallback));
network::mojom::ConnectionType connection_type =
network::mojom::ConnectionType::CONNECTION_UNKNOWN;
if (network_connection_tracker->GetConnectionType(&connection_type,
std::move(split.first))) {
std::move(split.second).Run(connection_type);
}
}
} // namespace
PrefetchCanaryChecker::RetryPolicy::RetryPolicy() = default;
PrefetchCanaryChecker::RetryPolicy::~RetryPolicy() = default;
PrefetchCanaryChecker::RetryPolicy::RetryPolicy(
PrefetchCanaryChecker::RetryPolicy const&) = default;
// static
std::unique_ptr<PrefetchCanaryChecker>
PrefetchCanaryChecker::MakePrefetchCanaryChecker(
BrowserContext* browser_context,
CheckType name,
const GURL& url,
const RetryPolicy& retry_policy,
const base::TimeDelta check_timeout,
base::TimeDelta revalidate_cache_after) {
if (!url.is_valid()) {
return nullptr;
}
return std::make_unique<PrefetchCanaryChecker>(browser_context, name, url,
retry_policy, check_timeout,
revalidate_cache_after);
}
PrefetchCanaryChecker::PrefetchCanaryChecker(
BrowserContext* browser_context,
CheckType name,
const GURL& url,
const RetryPolicy& retry_policy,
const base::TimeDelta check_timeout,
base::TimeDelta revalidate_cache_after)
: browser_context_(browser_context),
name_(NameForClient(name)),
url_(url),
retry_policy_(retry_policy),
backoff_entry_(&retry_policy_.backoff_policy),
check_timeout_(check_timeout),
revalidate_cache_after_(revalidate_cache_after),
cache_(kMaxCacheSize) {
// The NetworkConnectionTracker can only be used directly on the UI thread.
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
network_connection_tracker_ = GetNetworkConnectionTracker();
DCHECK(network_connection_tracker_);
}
PrefetchCanaryChecker::~PrefetchCanaryChecker() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
base::WeakPtr<PrefetchCanaryChecker> PrefetchCanaryChecker::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void PrefetchCanaryChecker::UpdateCacheEntry(
PrefetchCanaryChecker::CacheEntry entry,
std::string key) {
TRACE_EVENT0("loading", "PrefetchCanaryChecker::UpdateCacheEntry");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
latest_cache_key_ = key;
cache_.Put(key, entry);
}
void PrefetchCanaryChecker::UpdateCacheKey(std::string key) {
TRACE_EVENT0("loading", "PrefetchCanaryChecker::UpdateCacheKey");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
latest_cache_key_ = key;
}
void PrefetchCanaryChecker::OnCheckEnd(bool success) {
PrefetchCanaryChecker::CacheEntry entry;
entry.success = success;
entry.last_modified = base::Time::Now();
// We have the check result and we need to store it in the cache, keyed on
// the current network key. Getting the network key on Android can be slow
// so we do this asynchronously. Note that this is fundamentally racy: the
// network might have changed since we completed the check. Fortunately, the
// impact of using the wrong key is limited: we might simply filter probe when
// we don't have to or fail to filter probe when we should.
UpdateCacheWithNetworkID(
network_connection_tracker_,
base::BindOnce(&PrefetchCanaryChecker::UpdateCacheEntry, GetWeakPtr(),
entry));
DCHECK(time_when_set_active_.has_value());
base::TimeDelta active_time =
base::Time::Now() - time_when_set_active_.value();
if (success) {
base::Histogram::FactoryTimeGet(
AppendNameToHistogram(kTimeUntilSuccess),
base::Milliseconds(0) /* minimum */,
base::Milliseconds(30000) /* maximum */, 50 /* bucket_count */,
base::HistogramBase::kUmaTargetedHistogramFlag)
->Add(active_time.InMilliseconds());
} else {
base::Histogram::FactoryTimeGet(
AppendNameToHistogram(kTimeUntilFailure),
base::Milliseconds(0) /* minimum */,
base::Milliseconds(60000) /* maximum */, 50 /* bucket_count */,
base::HistogramBase::kUmaTargetedHistogramFlag)
->Add(active_time.InMilliseconds());
}
base::BooleanHistogram::FactoryGet(
AppendNameToHistogram(kFinalResultHistogram),
base::HistogramBase::kUmaTargetedHistogramFlag)
->Add(success);
ResetState();
}
void PrefetchCanaryChecker::ResetState() {
time_when_set_active_ = std::nullopt;
resolver_control_handle_.reset();
retry_timer_.reset();
timeout_timer_.reset();
backoff_entry_.Reset();
}
void PrefetchCanaryChecker::SendNowIfInactive() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (time_when_set_active_.has_value()) {
// We already have an active check.
return;
}
time_when_set_active_ = base::Time::Now();
StartDNSResolution(url_);
}
void PrefetchCanaryChecker::ProcessTimeout() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Cancel the pending resolving job. This will do nothing if resolving has
// already completed. Otherwise, the callback we registered (OnDNSResolved)
// will be called with the error code we pass here (net::ERR_TIMED_OUT).
resolver_control_handle_->Cancel(net::ERR_TIMED_OUT);
}
void PrefetchCanaryChecker::ProcessFailure(int net_error) {
TRACE_EVENT1("loading", "PrefetchCanaryChecker::ProcessFailure", "net_error",
net_error);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!retry_timer_ || !retry_timer_->IsRunning());
DCHECK(!timeout_timer_ || !timeout_timer_->IsRunning());
DCHECK(time_when_set_active_.has_value());
backoff_entry_.InformOfRequest(false);
base::UmaHistogramSparse(AppendNameToHistogram(kNetErrorHistogram),
std::abs(net_error));
if (retry_policy_.max_retries >=
static_cast<size_t>(backoff_entry_.failure_count())) {
base::TimeDelta interval = backoff_entry_.GetTimeUntilRelease();
retry_timer_ = std::make_unique<base::OneShotTimer>();
// base::Unretained is safe because |retry_timer_| is owned by this.
retry_timer_->Start(
FROM_HERE, interval,
base::BindOnce(&PrefetchCanaryChecker::StartDNSResolution,
base::Unretained(this), url_));
return;
}
OnCheckEnd(false);
}
void PrefetchCanaryChecker::ProcessSuccess() {
TRACE_EVENT0("loading", "PrefetchCanaryChecker::ProcessSuccess");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!retry_timer_ || !retry_timer_->IsRunning());
DCHECK(!timeout_timer_ || !timeout_timer_->IsRunning());
DCHECK(time_when_set_active_.has_value());
base::LinearHistogram::FactoryGet(
AppendNameToHistogram(kAttemptsBeforeSuccessHistogram), 1 /* minimum */,
25 /* maximum */, 25 /* bucket_count */,
base::HistogramBase::kUmaTargetedHistogramFlag)
// |failure_count| is zero when the first attempt is successful.
// Increase by one for more intuitive metrics.
->Add(backoff_entry_.failure_count() + 1);
OnCheckEnd(true);
}
std::optional<bool> PrefetchCanaryChecker::CanaryCheckSuccessful() {
TRACE_EVENT0("loading", "PrefetchCanaryChecker::CanaryCheckSuccessful");
std::optional<bool> result = LookupAndRunChecksIfNeeded();
CanaryCheckLookupResult result_enum;
if (!result.has_value()) {
result_enum = CanaryCheckLookupResult::kCacheMiss;
} else if (result.value()) {
result_enum = CanaryCheckLookupResult::kSuccess;
} else {
result_enum = CanaryCheckLookupResult::kFailure;
}
base::UmaHistogramEnumeration(AppendNameToHistogram(kCacheLookupResult),
result_enum);
return result;
}
// RunChecksIfNeeded is the public version of LookupAndRunChecksIfNeeded that
// doesn't return the lookup value, to force clients to use
// CanaryCheckSuccessful (which reports UMA) for lookups.
void PrefetchCanaryChecker::RunChecksIfNeeded() {
LookupAndRunChecksIfNeeded();
}
std::optional<bool> PrefetchCanaryChecker::LookupAndRunChecksIfNeeded() {
TRACE_EVENT0("loading", "PrefetchCanaryChecker::LookupAndRunChecksIfNeeded");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Asynchronously update the network cache key. On Android, getting the
// network cache key can be very slow, so we don't want to block the main
// thread.
UpdateCacheWithNetworkID(
network_connection_tracker_,
base::BindOnce(&PrefetchCanaryChecker::UpdateCacheKey, GetWeakPtr()));
// Assume the cache key has not changed since last time we checked it. Note
// that if we have never set latest_cache_key_, |it| will be cache_.end().
auto it = cache_.Get(latest_cache_key_);
if (it == cache_.end()) {
SendNowIfInactive();
return std::optional<bool>();
}
const PrefetchCanaryChecker::CacheEntry& entry = it->second;
base::TimeDelta cache_entry_age = base::Time::Now() - entry.last_modified;
base::LinearHistogram::FactoryTimeGet(
AppendNameToHistogram(kCacheEntryAgeHistogram),
base::Hours(0) /* minimum */, base::Hours(72) /* maximum */,
50 /* bucket_count */, base::HistogramBase::kUmaTargetedHistogramFlag)
->Add(cache_entry_age.InHours());
// Check if the cache entry should be revalidated because it has expired or
// cache_entry_age is negative because the clock was moved back.
if (cache_entry_age >= revalidate_cache_after_ ||
cache_entry_age.is_negative()) {
SendNowIfInactive();
}
return entry.success;
}
std::string PrefetchCanaryChecker::AppendNameToHistogram(
const std::string& histogram) const {
return base::StringPrintf("%s.%s", histogram.c_str(), name_.c_str());
}
void PrefetchCanaryChecker::StartDNSResolution(const GURL& url) {
TRACE_EVENT0("loading", "PrefetchCanaryChecker::StartDNSResolution");
net::NetworkAnonymizationKey nak =
net::IsolationInfo::CreateForInternalRequest(url::Origin::Create(url))
.network_anonymization_key();
network::mojom::ResolveHostParametersPtr resolve_host_parameters =
network::mojom::ResolveHostParameters::New();
resolve_host_parameters->initial_priority = net::RequestPriority::IDLE;
// Don't use DNS results cached at the user's device.
resolve_host_parameters->cache_usage =
network::mojom::ResolveHostParameters::CacheUsage::DISALLOWED;
// Allow cancelling the request.
resolver_control_handle_ = mojo::Remote<network::mojom::ResolveHostHandle>();
resolve_host_parameters->control_handle =
resolver_control_handle_.BindNewPipeAndPassReceiver();
mojo::PendingRemote<network::mojom::ResolveHostClient> client_remote;
mojo::MakeSelfOwnedReceiver(
std::make_unique<PrefetchDNSProber>(
base::BindOnce(&PrefetchCanaryChecker::OnDNSResolved, GetWeakPtr())),
client_remote.InitWithNewPipeAndPassReceiver());
// TODO(crbug.com/40235854): Consider passing a SchemeHostPort to trigger
// HTTPS DNS resource record query.
browser_context_->GetDefaultStoragePartition()
->GetNetworkContext()
->ResolveHost(network::mojom::HostResolverHost::NewHostPortPair(
net::HostPortPair::FromURL(url)),
nak, std::move(resolve_host_parameters),
std::move(client_remote));
timeout_timer_ = std::make_unique<base::OneShotTimer>();
// base::Unretained is safe because |timeout_timer_| is owned by this.
timeout_timer_->Start(FROM_HERE, check_timeout_,
base::BindOnce(&PrefetchCanaryChecker::ProcessTimeout,
base::Unretained(this)));
}
void PrefetchCanaryChecker::OnDNSResolved(
int net_error,
const net::AddressList& resolved_addresses) {
TRACE_EVENT0("loading", "PrefetchCanaryChecker::OnDNSResolved");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
timeout_timer_.reset();
resolver_control_handle_.reset();
bool successful = net_error == net::OK && !resolved_addresses.empty();
if (successful) {
ProcessSuccess();
} else {
ProcessFailure(net_error);
}
}
} // namespace content