blob: ceb9e1b8bb4f0043b96956ebbedd77b163cfddeb [file] [log] [blame]
// Copyright (c) 2012 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/net/predictor.h"
#include <algorithm>
#include <cmath>
#include <iterator>
#include <set>
#include <sstream>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/compiler_specific.h"
#include "base/containers/mru_cache.h"
#include "base/feature_list.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/io_thread.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/profile_io_data.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/resource_hints.h"
#include "net/base/address_list.h"
#include "net/base/completion_callback.h"
#include "net/base/host_port_pair.h"
#include "net/base/net_errors.h"
#include "net/http/transport_security_state.h"
#include "net/log/net_log.h"
#include "net/proxy/proxy_info.h"
#include "net/proxy/proxy_service.h"
#include "net/ssl/ssl_config_service.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"
using base::TimeDelta;
using content::BrowserThread;
namespace chrome_browser_net {
namespace {
const base::Feature kNetworkPrediction{"NetworkPrediction",
base::FEATURE_ENABLED_BY_DEFAULT};
} // namespace
// static
const int Predictor::kPredictorReferrerVersion = 2;
const double Predictor::kPreconnectWorthyExpectedValue = 0.8;
const double Predictor::kDNSPreresolutionWorthyExpectedValue = 0.1;
const double Predictor::kDiscardableExpectedValue = 0.05;
const size_t Predictor::kMaxSpeculativeParallelResolves = 3;
const int Predictor::kMaxUnusedSocketLifetimeSecondsWithoutAGet = 10;
// This number was obtained by the Net.Predictor.MRUIndex histogram on Canary
// and Dev channel (M53). The database size was initialized to 1000, and the
// histogram logged the index into the MRU in PrepareFrameSubresources. The
// results showed that 99% of all accesses used the first 100 elements, and
// 99.5% of all accesses used the first 170 elements.
const int Predictor::kMaxReferrers = 100;
// To control our congestion avoidance system, which discards a queue when
// resolutions are "taking too long," we need an expected resolution time.
// Common average is in the range of 300-500ms.
const int kExpectedResolutionTimeMs = 500;
const int Predictor::kTypicalSpeculativeGroupSize = 8;
const int Predictor::kMaxSpeculativeResolveQueueDelayMs =
(kExpectedResolutionTimeMs * Predictor::kTypicalSpeculativeGroupSize) /
Predictor::kMaxSpeculativeParallelResolves;
// The default value of the credentials flag when preconnecting.
static bool kAllowCredentialsOnPreconnectByDefault = true;
static int g_max_queueing_delay_ms =
Predictor::kMaxSpeculativeResolveQueueDelayMs;
static size_t g_max_parallel_resolves =
Predictor::kMaxSpeculativeParallelResolves;
// A version number for prefs that are saved. This should be incremented when
// we change the format so that we discard old data.
static const int kPredictorStartupFormatVersion = 1;
Predictor::Predictor(bool predictor_enabled)
: url_request_context_getter_(nullptr),
predictor_enabled_(predictor_enabled),
user_prefs_(nullptr),
profile_io_data_(nullptr),
num_pending_lookups_(0),
peak_pending_lookups_(0),
shutdown_(false),
max_concurrent_dns_lookups_(g_max_parallel_resolves),
max_dns_queue_delay_(
TimeDelta::FromMilliseconds(g_max_queueing_delay_ms)),
transport_security_state_(nullptr),
ssl_config_service_(nullptr),
proxy_service_(nullptr),
consecutive_omnibox_preconnect_count_(0),
referrers_(kMaxReferrers),
observer_(nullptr),
timed_cache_(new TimedCache(base::TimeDelta::FromSeconds(
kMaxUnusedSocketLifetimeSecondsWithoutAGet))),
ui_weak_factory_(new base::WeakPtrFactory<Predictor>(this)) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
Predictor::~Predictor() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(shutdown_);
}
// static
Predictor* Predictor::CreatePredictor(bool simple_shutdown) {
bool predictor_enabled = base::FeatureList::IsEnabled(kNetworkPrediction);
if (simple_shutdown)
return new SimplePredictor(predictor_enabled);
return new Predictor(predictor_enabled);
}
void Predictor::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterListPref(prefs::kDnsPrefetchingStartupList,
PrefRegistry::LOSSY_PREF);
registry->RegisterListPref(prefs::kDnsPrefetchingHostReferralList,
PrefRegistry::LOSSY_PREF);
}
// --------------------- Start UI methods. ------------------------------------
void Predictor::InitNetworkPredictor(PrefService* user_prefs,
IOThread* io_thread,
net::URLRequestContextGetter* getter,
ProfileIOData* profile_io_data) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
user_prefs_ = user_prefs;
url_request_context_getter_ = getter;
// Gather the list of hostnames to prefetch on startup.
std::vector<GURL> urls = GetPredictedUrlListAtStartup(user_prefs);
std::unique_ptr<base::ListValue> referral_list = base::WrapUnique(
user_prefs->GetList(prefs::kDnsPrefetchingHostReferralList)->DeepCopy());
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::Bind(&Predictor::FinalizeInitializationOnIOThread,
base::Unretained(this), urls,
base::Passed(std::move(referral_list)), io_thread,
profile_io_data));
}
void Predictor::AnticipateOmniboxUrl(const GURL& url, bool preconnectable) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!PredictorEnabled())
return;
if (!url.is_valid() || !url.has_host())
return;
if (!CanPreresolveAndPreconnect())
return;
std::string host = url.HostNoBrackets();
bool is_new_host_request = (host != last_omnibox_host_);
last_omnibox_host_ = host;
UrlInfo::ResolutionMotivation motivation(UrlInfo::OMNIBOX_MOTIVATED);
base::TimeTicks now = base::TimeTicks::Now();
if (preconnectable && !is_new_host_request) {
++consecutive_omnibox_preconnect_count_;
// The omnibox suggests a search URL (for which we can preconnect) after
// one or two characters are typed, even though such typing often (1 in
// 3?) becomes a real URL. This code waits till is has more evidence of a
// preconnectable URL (search URL) before forming a preconnection, so as
// to reduce the useless preconnect rate.
// Perchance this logic should be pushed back into the omnibox, where the
// actual characters typed, such as a space, can better forcast whether
// we need to search/preconnect or not. By waiting for at least 4
// characters in a row that have lead to a search proposal, we avoid
// preconnections for a prefix like "www." and we also wait until we have
// at least a 4 letter word to search for.
// Each character typed appears to induce 2 calls to AnticipateOmniboxUrl(),
// so we double 4 characters and limit at 8 requests.
// TODO(jar): Use an A/B test to optimize this.
const int kMinConsecutiveRequests = 8;
if (consecutive_omnibox_preconnect_count_ >= kMinConsecutiveRequests) {
// TODO(jar): Perhaps we should do a GET to leave the socket open in the
// pool. Currently, we just do a connect, which MAY be reset if we
// don't use it in 10 secondes!!! As a result, we may do more
// connections, and actually cost the server more than if we did a real
// get with a fake request (/gen_204 might be the good path on Google).
const int kMaxSearchKeepaliveSeconds(10);
if ((now - last_omnibox_preconnect_).InSeconds() <
kMaxSearchKeepaliveSeconds)
return; // We've done a preconnect recently.
last_omnibox_preconnect_ = now;
const int kConnectionsNeeded = 1;
PreconnectUrl(CanonicalizeUrl(url), GURL(), motivation,
kAllowCredentialsOnPreconnectByDefault, kConnectionsNeeded);
return; // Skip pre-resolution, since we'll open a connection.
}
} else {
consecutive_omnibox_preconnect_count_ = 0;
}
// Fall through and consider pre-resolution.
// Omnibox tends to call in pairs (just a few milliseconds apart), and we
// really don't need to keep resolving a name that often.
// TODO(jar): A/B tests could check for perf impact of the early returns.
if (!is_new_host_request) {
const int kMinPreresolveSeconds(10);
if (kMinPreresolveSeconds > (now - last_omnibox_preresolve_).InSeconds())
return;
}
last_omnibox_preresolve_ = now;
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::Bind(&Predictor::Resolve, base::Unretained(this),
CanonicalizeUrl(url), motivation));
}
void Predictor::PreconnectUrlAndSubresources(const GURL& url,
const GURL& first_party_for_cookies) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) ||
BrowserThread::CurrentlyOn(BrowserThread::IO));
if (!PredictorEnabled() || !url.is_valid() ||
!url.has_host())
return;
if (!CanPreresolveAndPreconnect())
return;
UrlInfo::ResolutionMotivation motivation(UrlInfo::EARLY_LOAD_MOTIVATED);
const int kConnectionsNeeded = 1;
PreconnectUrl(CanonicalizeUrl(url), first_party_for_cookies, motivation,
kConnectionsNeeded, kAllowCredentialsOnPreconnectByDefault);
PredictFrameSubresources(url.GetWithEmptyPath(), first_party_for_cookies);
}
std::vector<GURL> Predictor::GetPredictedUrlListAtStartup(
PrefService* user_prefs) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::vector<GURL> urls;
// Recall list of URLs we learned about during last session.
// This may catch secondary hostnames, pulled in by the homepages. It will
// also catch more of the "primary" home pages, since that was (presumably)
// rendered first (and will be rendered first this time too).
const base::ListValue* startup_list =
user_prefs->GetList(prefs::kDnsPrefetchingStartupList);
if (startup_list) {
base::ListValue::const_iterator it = startup_list->begin();
int format_version = -1;
if (it != startup_list->end() &&
(*it)->GetAsInteger(&format_version) &&
format_version == kPredictorStartupFormatVersion) {
++it;
for (; it != startup_list->end(); ++it) {
std::string url_spec;
if (!(*it)->GetAsString(&url_spec)) {
LOG(DFATAL);
break; // Format incompatibility.
}
GURL url(url_spec);
if (!url.has_host() || !url.has_scheme()) {
LOG(DFATAL);
break; // Format incompatibility.
}
urls.push_back(url);
}
}
}
// Prepare for any static home page(s) the user has in prefs. The user may
// have a LOT of tab's specified, so we may as well try to warm them all.
SessionStartupPref tab_start_pref =
SessionStartupPref::GetStartupPref(user_prefs);
if (SessionStartupPref::URLS == tab_start_pref.type) {
for (size_t i = 0; i < tab_start_pref.urls.size(); i++) {
GURL gurl = tab_start_pref.urls[i];
if (!gurl.is_valid() || gurl.SchemeIsFile() || gurl.host_piece().empty())
continue;
if (gurl.SchemeIsHTTPOrHTTPS())
urls.push_back(gurl.GetWithEmptyPath());
}
}
if (urls.empty())
urls.push_back(GURL("http://www.google.com:80"));
return urls;
}
void Predictor::DiscardAllResultsAndClearPrefsOnUIThread() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
base::Bind(&Predictor::DiscardAllResults,
io_weak_factory_->GetWeakPtr()));
ClearPrefsOnUIThread();
}
void Predictor::ClearPrefsOnUIThread() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
user_prefs_->ClearPref(prefs::kDnsPrefetchingStartupList);
user_prefs_->ClearPref(prefs::kDnsPrefetchingHostReferralList);
}
void Predictor::set_max_queueing_delay(int max_queueing_delay_ms) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
g_max_queueing_delay_ms = max_queueing_delay_ms;
}
void Predictor::set_max_parallel_resolves(size_t max_parallel_resolves) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
g_max_parallel_resolves = max_parallel_resolves;
}
void Predictor::ShutdownOnUIThread() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
ui_weak_factory_->InvalidateWeakPtrs();
BrowserThread::PostTask(
BrowserThread::IO,
FROM_HERE,
base::Bind(&Predictor::Shutdown, base::Unretained(this)));
}
// ---------------------- End UI methods. -------------------------------------
// --------------------- Start IO methods. ------------------------------------
void Predictor::Shutdown() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(!shutdown_);
shutdown_ = true;
}
void Predictor::DiscardAllResults() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Delete anything listed so far in this session that shows in about:dns.
referrers_.Clear();
// Try to delete anything in our work queue.
while (!work_queue_.IsEmpty()) {
// Emulate processing cycle as though host was not found.
GURL url = work_queue_.Pop();
UrlInfo* info = &results_[url];
DCHECK(info->HasUrl(url));
info->SetAssignedState();
info->SetNoSuchNameState();
}
// Now every result_ is either resolved, or is being resolved.
// Step through result_, recording names of all hosts that can't be erased.
// We can't erase anything being worked on.
Results assignees;
for (Results::iterator it = results_.begin(); results_.end() != it; ++it) {
GURL url(it->first);
UrlInfo* info = &it->second;
DCHECK(info->HasUrl(url));
if (info->is_assigned()) {
info->SetPendingDeleteState();
assignees[url] = *info;
}
}
DCHECK_LE(assignees.size(), max_concurrent_dns_lookups_);
results_.clear();
// Put back in the names being worked on.
for (Results::iterator it = assignees.begin(); assignees.end() != it; ++it) {
DCHECK(it->second.is_marked_to_delete());
results_[it->first] = it->second;
}
}
// Overloaded Resolve() to take a vector of names.
void Predictor::ResolveList(const std::vector<GURL>& urls,
UrlInfo::ResolutionMotivation motivation) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
for (std::vector<GURL>::const_iterator it = urls.begin(); it < urls.end();
++it) {
AppendToResolutionQueue(*it, motivation);
}
}
// Basic Resolve() takes an invidual name, and adds it
// to the queue.
void Predictor::Resolve(const GURL& url,
UrlInfo::ResolutionMotivation motivation) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!url.has_host())
return;
AppendToResolutionQueue(url, motivation);
}
void Predictor::LearnFromNavigation(const GURL& referring_url,
const GURL& target_url) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!PredictorEnabled() || !CanPreresolveAndPreconnect())
return;
DCHECK_EQ(referring_url, Predictor::CanonicalizeUrl(referring_url));
DCHECK_NE(referring_url, GURL::EmptyGURL());
DCHECK_EQ(target_url, Predictor::CanonicalizeUrl(target_url));
DCHECK_NE(target_url, GURL::EmptyGURL());
// Skip HSTS redirects to learn the true referrer.
GURL referring_url_with_hsts = GetHSTSRedirectOnIOThread(referring_url);
if (observer_)
observer_->OnLearnFromNavigation(referring_url_with_hsts, target_url);
// Peek here, as Get() occurs on the actual navigation. Note, Put is used here
// due to the fact that on a new navigation, it is unclear whether the URL
// will be a referrer to any subresource. That could result in bloating the
// database with empty entries.
Referrers::iterator it = referrers_.Peek(referring_url_with_hsts);
if (it == referrers_.end())
it = referrers_.Put(referring_url_with_hsts, Referrer());
it->second.SuggestHost(target_url);
}
//-----------------------------------------------------------------------------
// This section supports the about:dns page.
void Predictor::PredictorGetHtmlInfo(Predictor* predictor,
std::string* output) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
output->append("<html><head><title>About DNS</title>"
// We'd like the following no-cache... but it doesn't work.
// "<META HTTP-EQUIV=\"Pragma\" CONTENT=\"no-cache\">"
"</head><body>");
if (predictor && predictor->PredictorEnabled() &&
predictor->CanPreresolveAndPreconnect()) {
predictor->GetHtmlInfo(output);
} else {
output->append("DNS pre-resolution and TCP pre-connection is disabled.");
}
output->append("</body></html>");
}
void Predictor::GetHtmlReferrerLists(std::string* output) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (referrers_.empty())
return;
// TODO(jar): Remove any plausible JavaScript from names before displaying.
output->append("<br><table border>");
output->append(
"<tr><th>Host for Page</th>"
"<th>Page Load<br>Count</th>"
"<th>Subresource<br>Navigations</th>"
"<th>Subresource<br>PreConnects</th>"
"<th>Subresource<br>PreResolves</th>"
"<th>Expected<br>Connects</th>"
"<th>Subresource Spec</th></tr>");
for (Referrers::iterator it = referrers_.begin(); referrers_.end() != it;
++it) {
const Referrer& referrer = it->second;
bool first_set_of_futures = true;
for (Referrer::const_iterator future_url = referrer.begin();
future_url != referrer.end(); ++future_url) {
output->append("<tr align=right>");
if (first_set_of_futures) {
base::StringAppendF(
output, "<td rowspan=%d>%s</td><td rowspan=%d>%d</td>",
static_cast<int>(referrer.size()), it->first.spec().c_str(),
static_cast<int>(referrer.size()),
static_cast<int>(referrer.use_count()));
}
first_set_of_futures = false;
base::StringAppendF(output,
"<td>%d</td><td>%d</td><td>%d</td><td>%2.3f</td><td>%s</td></tr>",
static_cast<int>(future_url->second.navigation_count()),
static_cast<int>(future_url->second.preconnection_count()),
static_cast<int>(future_url->second.preresolution_count()),
static_cast<double>(future_url->second.subresource_use_rate()),
future_url->first.spec().c_str());
}
}
output->append("</table>");
}
void Predictor::GetHtmlInfo(std::string* output) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (initial_observer_.get())
initial_observer_->GetFirstResolutionsHtml(output);
// Show list of subresource predictions and stats.
GetHtmlReferrerLists(output);
// Local lists for calling UrlInfo
UrlInfo::UrlInfoTable name_not_found;
UrlInfo::UrlInfoTable name_preresolved;
// UrlInfo supports value semantics, so we can do a shallow copy.
for (Results::iterator it(results_.begin()); it != results_.end(); it++) {
if (it->second.was_nonexistent()) {
name_not_found.push_back(it->second);
continue;
}
if (!it->second.was_found())
continue; // Still being processed.
name_preresolved.push_back(it->second);
}
bool brief = false;
#ifdef NDEBUG
brief = true;
#endif // NDEBUG
// Call for display of each table, along with title.
UrlInfo::GetHtmlTable(name_preresolved,
"Preresolution DNS records performed for ", brief, output);
UrlInfo::GetHtmlTable(name_not_found,
"Preresolving DNS records revealed non-existence for ", brief, output);
}
// Iterating through a MRUCache goes through most recent first. Iterate
// backwards here so that adding items in order "Just Works" when deserializing.
void Predictor::SerializeReferrers(base::ListValue* referral_list) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(referral_list->empty());
referral_list->AppendInteger(kPredictorReferrerVersion);
for (Referrers::const_reverse_iterator it = referrers_.rbegin();
it != referrers_.rend(); ++it) {
// Create a list for each referer.
std::unique_ptr<base::ListValue> motivator(new base::ListValue);
motivator->AppendString(it->first.spec());
motivator->Append(it->second.Serialize());
referral_list->Append(std::move(motivator));
}
}
void Predictor::DeserializeReferrers(const base::ListValue& referral_list) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
int format_version = -1;
if (referral_list.GetSize() > 0 &&
referral_list.GetInteger(0, &format_version) &&
format_version == kPredictorReferrerVersion) {
for (size_t i = 1; i < referral_list.GetSize(); ++i) {
const base::ListValue* motivator;
if (!referral_list.GetList(i, &motivator)) {
NOTREACHED();
return;
}
std::string motivating_url_spec;
if (!motivator->GetString(0, &motivating_url_spec)) {
NOTREACHED();
return;
}
const base::Value* subresource_list;
if (!motivator->Get(1, &subresource_list)) {
NOTREACHED();
return;
}
referrers_.Put(GURL(motivating_url_spec), Referrer())
->second.Deserialize(*subresource_list);
}
}
}
void Predictor::DiscardInitialNavigationHistory() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (initial_observer_.get())
initial_observer_->DiscardInitialNavigationHistory();
}
void Predictor::FinalizeInitializationOnIOThread(
const std::vector<GURL>& startup_urls,
std::unique_ptr<base::ListValue> referral_list,
IOThread* io_thread,
ProfileIOData* profile_io_data) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
profile_io_data_ = profile_io_data;
initial_observer_.reset(new InitialObserver());
net::URLRequestContext* context =
url_request_context_getter_->GetURLRequestContext();
transport_security_state_ = context->transport_security_state();
ssl_config_service_ = context->ssl_config_service();
proxy_service_ = context->proxy_service();
// base::WeakPtrFactory instances need to be created and destroyed
// on the same thread. Initialize the IO thread weak factory now.
io_weak_factory_.reset(new base::WeakPtrFactory<Predictor>(this));
// Prefetch these hostnames on startup.
DnsPrefetchMotivatedList(startup_urls, UrlInfo::STARTUP_LIST_MOTIVATED);
DeserializeReferrers(*referral_list);
LogStartupMetrics();
}
//-----------------------------------------------------------------------------
// This section intermingles prefetch results with actual browser HTTP
// network activity. It supports calculating of the benefit of a prefetch, as
// well as recording what prefetched hostname resolutions might be potentially
// helpful during the next chrome-startup.
//-----------------------------------------------------------------------------
void Predictor::LearnAboutInitialNavigation(const GURL& url) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!PredictorEnabled() || nullptr == initial_observer_.get() ||
!CanPreresolveAndPreconnect()) {
return;
}
initial_observer_->Append(url, this);
}
// This API is only used in the browser process.
// It is called from an IPC message originating in the renderer. It currently
// includes both Page-Scan, and Link-Hover prefetching.
// TODO(jar): Separate out link-hover prefetching, and page-scan results.
void Predictor::DnsPrefetchList(const std::vector<std::string>& hostnames) {
// TODO(jar): Push GURL transport further back into renderer, but this will
// require a Webkit change in the observer :-/.
std::vector<GURL> urls;
for (std::vector<std::string>::const_iterator it = hostnames.begin();
it < hostnames.end(); ++it) {
urls.push_back(GURL("http://" + *it + ":80"));
}
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DnsPrefetchMotivatedList(urls, UrlInfo::PAGE_SCAN_MOTIVATED);
}
void Predictor::DnsPrefetchMotivatedList(
const std::vector<GURL>& urls,
UrlInfo::ResolutionMotivation motivation) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) ||
BrowserThread::CurrentlyOn(BrowserThread::IO));
if (!PredictorEnabled())
return;
if (!CanPreresolveAndPreconnect())
return;
if (BrowserThread::CurrentlyOn(BrowserThread::IO)) {
ResolveList(urls, motivation);
} else {
BrowserThread::PostTask(
BrowserThread::IO,
FROM_HERE,
base::Bind(&Predictor::ResolveList, base::Unretained(this),
urls, motivation));
}
}
//-----------------------------------------------------------------------------
// Functions to handle saving of hostnames from one session to the next, to
// expedite startup times.
void Predictor::SaveStateForNextStartup() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!PredictorEnabled())
return;
if (!CanPreresolveAndPreconnect())
return;
std::unique_ptr<base::ListValue> startup_list(new base::ListValue);
std::unique_ptr<base::ListValue> referral_list(new base::ListValue);
// Get raw pointers to pass to the first task. Ownership of the unique_ptrs
// will be passed to the reply task.
base::ListValue* startup_list_raw = startup_list.get();
base::ListValue* referral_list_raw = referral_list.get();
BrowserThread::PostTaskAndReply(
BrowserThread::IO, FROM_HERE,
base::Bind(&Predictor::WriteDnsPrefetchState,
io_weak_factory_->GetWeakPtr(), startup_list_raw,
referral_list_raw),
base::Bind(&Predictor::UpdatePrefsOnUIThread,
ui_weak_factory_->GetWeakPtr(),
base::Passed(std::move(startup_list)),
base::Passed(std::move(referral_list))));
}
void Predictor::UpdatePrefsOnUIThread(
std::unique_ptr<base::ListValue> startup_list,
std::unique_ptr<base::ListValue> referral_list) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
user_prefs_->Set(prefs::kDnsPrefetchingStartupList, *startup_list);
user_prefs_->Set(prefs::kDnsPrefetchingHostReferralList, *referral_list);
}
void Predictor::WriteDnsPrefetchState(base::ListValue* startup_list,
base::ListValue* referral_list) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (initial_observer_.get())
initial_observer_->GetInitialDnsResolutionList(startup_list);
SerializeReferrers(referral_list);
}
void Predictor::PreconnectUrl(const GURL& url,
const GURL& first_party_for_cookies,
UrlInfo::ResolutionMotivation motivation,
bool allow_credentials,
int count) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) ||
BrowserThread::CurrentlyOn(BrowserThread::IO));
if (BrowserThread::CurrentlyOn(BrowserThread::IO)) {
PreconnectUrlOnIOThread(url, first_party_for_cookies, motivation,
allow_credentials, count);
} else {
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::Bind(&Predictor::PreconnectUrlOnIOThread, base::Unretained(this),
url, first_party_for_cookies, motivation, allow_credentials,
count));
}
}
void Predictor::PreconnectUrlOnIOThread(
const GURL& original_url,
const GURL& first_party_for_cookies,
UrlInfo::ResolutionMotivation motivation,
bool allow_credentials,
int count) {
// Skip the HSTS redirect.
GURL url = GetHSTSRedirectOnIOThread(original_url);
// TODO(csharrison): The observer should only be notified after the null check
// for the ProfileIOData. The predictor tests should be fixed to allow for
// this, as they currently expect a callback with no getter.
if (observer_) {
observer_->OnPreconnectUrl(
url, first_party_for_cookies, motivation, count);
}
if (!profile_io_data_)
return;
// Translate the motivation from UrlRequest motivations to HttpRequest
// motivations.
net::HttpRequestInfo::RequestMotivation request_motivation =
net::HttpRequestInfo::NORMAL_MOTIVATION;
switch (motivation) {
case UrlInfo::OMNIBOX_MOTIVATED:
request_motivation = net::HttpRequestInfo::OMNIBOX_MOTIVATED;
break;
case UrlInfo::LEARNED_REFERAL_MOTIVATED:
request_motivation = net::HttpRequestInfo::PRECONNECT_MOTIVATED;
break;
case UrlInfo::MOUSE_OVER_MOTIVATED:
case UrlInfo::SELF_REFERAL_MOTIVATED:
case UrlInfo::EARLY_LOAD_MOTIVATED:
request_motivation = net::HttpRequestInfo::EARLY_LOAD_MOTIVATED;
break;
default:
// Other motivations should never happen here.
NOTREACHED();
break;
}
UMA_HISTOGRAM_ENUMERATION("Net.PreconnectMotivation", motivation,
UrlInfo::MAX_MOTIVATED);
content::PreconnectUrl(profile_io_data_->GetResourceContext(), url,
first_party_for_cookies, count, allow_credentials,
request_motivation);
}
void Predictor::PredictFrameSubresources(const GURL& url,
const GURL& first_party_for_cookies) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) ||
BrowserThread::CurrentlyOn(BrowserThread::IO));
if (!PredictorEnabled())
return;
if (!CanPreresolveAndPreconnect())
return;
DCHECK_EQ(url.GetWithEmptyPath(), url);
// Add one pass through the message loop to allow current navigation to
// proceed.
if (BrowserThread::CurrentlyOn(BrowserThread::IO)) {
PrepareFrameSubresources(url, first_party_for_cookies);
} else {
BrowserThread::PostTask(
BrowserThread::IO,
FROM_HERE,
base::Bind(&Predictor::PrepareFrameSubresources,
base::Unretained(this), url, first_party_for_cookies));
}
}
bool Predictor::CanPrefetchAndPrerender() const {
chrome_browser_net::NetworkPredictionStatus status;
if (BrowserThread::CurrentlyOn(BrowserThread::UI)) {
status = chrome_browser_net::CanPrefetchAndPrerenderUI(user_prefs_);
} else {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
status = chrome_browser_net::CanPrefetchAndPrerenderIO(profile_io_data_);
}
return status == chrome_browser_net::NetworkPredictionStatus::ENABLED;
}
bool Predictor::CanPreresolveAndPreconnect() const {
if (BrowserThread::CurrentlyOn(BrowserThread::UI)) {
return chrome_browser_net::CanPreresolveAndPreconnectUI(user_prefs_);
} else {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
return chrome_browser_net::CanPreresolveAndPreconnectIO(profile_io_data_);
}
}
enum SubresourceValue {
PRECONNECTION,
PRERESOLUTION,
TOO_NEW,
SUBRESOURCE_VALUE_MAX
};
void Predictor::PrepareFrameSubresources(const GURL& original_url,
const GURL& first_party_for_cookies) {
// Apply HSTS redirect early so it is taken into account when looking up
// subresources.
GURL url = GetHSTSRedirectOnIOThread(original_url);
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK_EQ(url.GetWithEmptyPath(), url);
DCHECK(PredictorEnabled());
// Peek here, and Get after logging the index into the MRU.
Referrers::iterator it = referrers_.Get(url);
if (referrers_.end() == it) {
// Only when we don't know anything about this url, make 2 connections
// available. We could do this completely via learning (by prepopulating
// the referrer_ list with this expected value), but it would swell the
// size of the list with all the "Leaf" nodes in the tree (nodes that don't
// load any subresources). If we learn about this resource, we will instead
// provide a more carefully estimated preconnection count.
PreconnectUrlOnIOThread(url, first_party_for_cookies,
UrlInfo::SELF_REFERAL_MOTIVATED,
kAllowCredentialsOnPreconnectByDefault, 2);
return;
}
Referrer* referrer = &(it->second);
referrer->IncrementUseCount();
const UrlInfo::ResolutionMotivation motivation =
UrlInfo::LEARNED_REFERAL_MOTIVATED;
for (std::map<GURL, ReferrerValue>::iterator future_url = referrer->begin();
future_url != referrer->end();) {
SubresourceValue evalution(TOO_NEW);
double connection_expectation = future_url->second.subresource_use_rate();
UMA_HISTOGRAM_CUSTOM_COUNTS("Net.PreconnectSubresourceExpectation",
static_cast<int>(connection_expectation * 100),
10, 5000, 50);
future_url->second.ReferrerWasObserved();
if (connection_expectation > kPreconnectWorthyExpectedValue) {
evalution = PRECONNECTION;
future_url->second.IncrementPreconnectionCount();
int count = static_cast<int>(std::ceil(connection_expectation));
if (url.host_piece() == future_url->first.host_piece())
++count;
PreconnectUrlOnIOThread(future_url->first, first_party_for_cookies,
motivation,
kAllowCredentialsOnPreconnectByDefault, count);
} else if (connection_expectation > kDNSPreresolutionWorthyExpectedValue) {
evalution = PRERESOLUTION;
future_url->second.preresolution_increment();
UrlInfo* queued_info = AppendToResolutionQueue(future_url->first,
motivation);
if (queued_info)
queued_info->SetReferringHostname(url);
}
// Remove future urls that are below the discardable threshold here. This is
// the only place where the future urls of a referrer are iterated through,
// so it is the most logical place for trimming.
if (connection_expectation < kDiscardableExpectedValue) {
future_url = referrer->erase(future_url);
} else {
++future_url;
}
UMA_HISTOGRAM_ENUMERATION("Net.PreconnectSubresourceEval", evalution,
SUBRESOURCE_VALUE_MAX);
}
// If the Referrer has no URLs associated with it, remove it from the map.
if (referrer->empty())
referrers_.Erase(it);
}
void Predictor::OnLookupFinished(const GURL& url, int result) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
LookupFinished(url, result == net::OK);
if (observer_)
observer_->OnDnsLookupFinished(url, result == net::OK);
DCHECK_GT(num_pending_lookups_, 0u);
num_pending_lookups_--;
StartSomeQueuedResolutions();
}
void Predictor::LookupFinished(const GURL& url, bool found) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
UrlInfo* info = &results_[url];
DCHECK(info->HasUrl(url));
if (info->is_marked_to_delete()) {
results_.erase(url);
} else {
if (found)
info->SetFoundState();
else
info->SetNoSuchNameState();
}
}
bool Predictor::WouldLikelyProxyURL(const GURL& url) {
if (!proxy_service_)
return false;
net::ProxyInfo info;
bool synchronous_success = proxy_service_->TryResolveProxySynchronously(
url, std::string(), &info, nullptr, net::BoundNetLog());
return synchronous_success && !info.is_direct();
}
UrlInfo* Predictor::AppendToResolutionQueue(
const GURL& url,
UrlInfo::ResolutionMotivation motivation) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(url.has_host());
if (shutdown_)
return nullptr;
UrlInfo* info = &results_[url];
info->SetUrl(url); // Initialize or DCHECK.
// TODO(jar): I need to discard names that have long since expired.
// Currently we only add to the domain map :-/
DCHECK(info->HasUrl(url));
if (!info->NeedsDnsUpdate()) {
info->DLogResultsStats("DNS PrefetchNotUpdated");
return nullptr;
}
if (WouldLikelyProxyURL(url)) {
info->DLogResultsStats("DNS PrefetchForProxiedRequest");
return nullptr;
}
info->SetQueuedState(motivation);
work_queue_.Push(url, motivation);
StartSomeQueuedResolutions();
return info;
}
bool Predictor::CongestionControlPerformed(UrlInfo* info) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Note: queue_duration is ONLY valid after we go to assigned state.
if (info->queue_duration() < max_dns_queue_delay_)
return false;
// We need to discard all entries in our queue, as we're keeping them waiting
// too long. By doing this, we'll have a chance to quickly service urgent
// resolutions, and not have a bogged down system.
while (true) {
info->RemoveFromQueue();
if (work_queue_.IsEmpty())
break;
info = &results_[work_queue_.Pop()];
info->SetAssignedState();
}
return true;
}
void Predictor::StartSomeQueuedResolutions() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
while (!work_queue_.IsEmpty() &&
num_pending_lookups_ < max_concurrent_dns_lookups_) {
const GURL url(work_queue_.Pop());
UrlInfo* info = &results_[url];
DCHECK(info->HasUrl(url));
info->SetAssignedState();
if (CongestionControlPerformed(info)) {
DCHECK(work_queue_.IsEmpty());
return;
}
int status =
content::PreresolveUrl(profile_io_data_->GetResourceContext(), url,
base::Bind(&Predictor::OnLookupFinished,
io_weak_factory_->GetWeakPtr(), url));
if (status == net::ERR_IO_PENDING) {
// Will complete asynchronously.
num_pending_lookups_++;
peak_pending_lookups_ =
std::max(peak_pending_lookups_, num_pending_lookups_);
} else {
// Completed synchronously (was already cached by HostResolver), or else
// there was (equivalently) some network error that prevents us from
// finding the name. Status net::OK means it was "found."
LookupFinished(url, status == net::OK);
}
}
}
GURL Predictor::GetHSTSRedirectOnIOThread(const GURL& url) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!transport_security_state_)
return url;
if (!url.SchemeIs("http"))
return url;
if (!transport_security_state_->ShouldUpgradeToSSL(url.host()))
return url;
url::Replacements<char> replacements;
const char kNewScheme[] = "https";
replacements.SetScheme(kNewScheme, url::Component(0, strlen(kNewScheme)));
return url.ReplaceComponents(replacements);
}
void Predictor::LogStartupMetrics() {
size_t total_bytes = 0;
for (const auto& referrer : referrers_) {
total_bytes += referrer.first.spec().size();
total_bytes += sizeof(Referrer);
for (const auto& subresource : referrer.second) {
total_bytes += subresource.first.spec().size();
total_bytes += sizeof(ReferrerValue);
}
}
UMA_HISTOGRAM_CUSTOM_COUNTS("Net.Predictor.Startup.DBSize", total_bytes, 1,
10 * 1000 * 1000, 50);
}
// ---------------------- End IO methods. -------------------------------------
//-----------------------------------------------------------------------------
bool Predictor::PredictorEnabled() const {
base::AutoLock lock(predictor_enabled_lock_);
return predictor_enabled_;
}
void Predictor::SetPredictorEnabledForTest(bool predictor_enabled) {
base::AutoLock lock(predictor_enabled_lock_);
predictor_enabled_ = predictor_enabled;
}
Predictor::HostNameQueue::HostNameQueue() {
}
Predictor::HostNameQueue::~HostNameQueue() {
}
void Predictor::HostNameQueue::Push(const GURL& url,
UrlInfo::ResolutionMotivation motivation) {
switch (motivation) {
case UrlInfo::STATIC_REFERAL_MOTIVATED:
case UrlInfo::LEARNED_REFERAL_MOTIVATED:
case UrlInfo::MOUSE_OVER_MOTIVATED:
rush_queue_.push(url);
break;
default:
background_queue_.push(url);
break;
}
}
bool Predictor::HostNameQueue::IsEmpty() const {
return rush_queue_.empty() && background_queue_.empty();
}
GURL Predictor::HostNameQueue::Pop() {
DCHECK(!IsEmpty());
std::queue<GURL> *queue(rush_queue_.empty() ? &background_queue_
: &rush_queue_);
GURL url(queue->front());
queue->pop();
return url;
}
//-----------------------------------------------------------------------------
// Member definitions for InitialObserver class.
Predictor::InitialObserver::InitialObserver() {
}
Predictor::InitialObserver::~InitialObserver() {
}
void Predictor::InitialObserver::Append(const GURL& url,
Predictor* predictor) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// TODO(rlp): Do we really need the predictor check here?
if (nullptr == predictor)
return;
if (kStartupResolutionCount <= first_navigations_.size())
return;
DCHECK(url.SchemeIsHTTPOrHTTPS());
DCHECK_EQ(url, Predictor::CanonicalizeUrl(url));
if (first_navigations_.find(url) == first_navigations_.end())
first_navigations_[url] = base::TimeTicks::Now();
}
void Predictor::InitialObserver::GetInitialDnsResolutionList(
base::ListValue* startup_list) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(startup_list);
DCHECK(startup_list->empty());
DCHECK_EQ(0u, startup_list->GetSize());
startup_list->AppendInteger(kPredictorStartupFormatVersion);
for (FirstNavigations::iterator it = first_navigations_.begin();
it != first_navigations_.end();
++it) {
DCHECK(it->first == Predictor::CanonicalizeUrl(it->first));
startup_list->AppendString(it->first.spec());
}
}
void Predictor::InitialObserver::GetFirstResolutionsHtml(
std::string* output) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
UrlInfo::UrlInfoTable resolution_list;
{
for (FirstNavigations::iterator it(first_navigations_.begin());
it != first_navigations_.end();
it++) {
UrlInfo info;
info.SetUrl(it->first);
info.set_time(it->second);
resolution_list.push_back(info);
}
}
UrlInfo::GetHtmlTable(resolution_list,
"Future startups will prefetch DNS records for ", false, output);
}
//-----------------------------------------------------------------------------
// Helper functions
//-----------------------------------------------------------------------------
// static
GURL Predictor::CanonicalizeUrl(const GURL& url) {
if (!url.has_host())
return GURL::EmptyGURL();
std::string scheme;
if (url.has_scheme()) {
scheme = url.scheme();
if (scheme != "http" && scheme != "https")
return GURL::EmptyGURL();
if (url.has_port())
return url.GetWithEmptyPath();
} else {
scheme = "http";
}
// If we omit a port, it will default to 80 or 443 as appropriate.
std::string colon_plus_port;
if (url.has_port())
colon_plus_port = ":" + url.port();
return GURL(scheme + "://" + url.host() + colon_plus_port);
}
void SimplePredictor::InitNetworkPredictor(
PrefService* user_prefs,
IOThread* io_thread,
net::URLRequestContextGetter* getter,
ProfileIOData* profile_io_data) {
// Empty function for unittests.
}
void SimplePredictor::ShutdownOnUIThread() {
SetShutdown(true);
}
bool SimplePredictor::CanPrefetchAndPrerender() const { return true; }
bool SimplePredictor::CanPreresolveAndPreconnect() const { return true; }
} // namespace chrome_browser_net