blob: 1d2ddb653b7742cb2d7257d3ea1aeaa37f5aba25 [file] [log] [blame]
// Copyright 2013 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 "components/precache/content/precache_manager.h"
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/history/core/browser/history_service.h"
#include "components/precache/core/precache_database.h"
#include "components/precache/core/precache_switches.h"
#include "components/prefs/pref_service.h"
#include "components/sync_driver/sync_service.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/network_change_notifier.h"
using content::BrowserThread;
namespace {
const char kPrecacheFieldTrialName[] = "Precache";
const char kPrecacheFieldTrialEnabledGroup[] = "Enabled";
const char kPrecacheFieldTrialControlGroup[] = "Control";
const char kConfigURLParam[] = "config_url";
const char kManifestURLPrefixParam[] = "manifest_url_prefix";
const size_t kNumTopHosts = 100;
} // namespace
namespace precache {
size_t NumTopHosts() {
return kNumTopHosts;
}
PrecacheManager::PrecacheManager(
content::BrowserContext* browser_context,
const sync_driver::SyncService* const sync_service,
const history::HistoryService* const history_service)
: browser_context_(browser_context),
sync_service_(sync_service),
history_service_(history_service),
precache_database_(new PrecacheDatabase()),
is_precaching_(false) {
base::FilePath db_path(browser_context_->GetPath().Append(
base::FilePath(FILE_PATH_LITERAL("PrecacheDatabase"))));
BrowserThread::PostTask(
BrowserThread::DB, FROM_HERE,
base::Bind(base::IgnoreResult(&PrecacheDatabase::Init),
base::Unretained(precache_database_.get()), db_path));
}
PrecacheManager::~PrecacheManager() {
// DeleteSoon posts a non-nestable task to the task runner, so any previously
// posted tasks that rely on an Unretained precache_database_ will finish
// before it is deleted.
BrowserThread::DeleteSoon(BrowserThread::DB, FROM_HERE,
precache_database_.release());
}
bool PrecacheManager::IsInExperimentGroup() const {
// Verify IsPrecachingAllowed() before calling FieldTrialList::FindFullName().
// This is because field trials are only assigned when requested. This allows
// us to create Control and Experiment groups that are limited to users for
// whom PrecachingAllowed() is true, thus accentuating the impact of
// precaching.
return IsPrecachingAllowed() &&
(base::StartsWith(
base::FieldTrialList::FindFullName(kPrecacheFieldTrialName),
kPrecacheFieldTrialEnabledGroup, base::CompareCase::SENSITIVE) ||
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnablePrecache));
}
bool PrecacheManager::IsInControlGroup() const {
// Verify IsPrecachingAllowed() before calling FindFullName(). See
// PrecacheManager::IsInExperimentGroup() for an explanation of why.
return IsPrecachingAllowed() &&
base::StartsWith(
base::FieldTrialList::FindFullName(kPrecacheFieldTrialName),
kPrecacheFieldTrialControlGroup, base::CompareCase::SENSITIVE);
}
bool PrecacheManager::IsPrecachingAllowed() const {
return PrecachingAllowed() == AllowedType::ALLOWED;
}
PrecacheManager::AllowedType PrecacheManager::PrecachingAllowed() const {
if (!(sync_service_ && sync_service_->IsBackendInitialized()))
return AllowedType::PENDING;
// SyncService delegates to SyncPrefs, which must be called on the UI thread.
if (history_service_ &&
sync_service_->GetActiveDataTypes().Has(syncer::SESSIONS) &&
!sync_service_->GetEncryptedDataTypes().Has(syncer::SESSIONS))
return AllowedType::ALLOWED;
return AllowedType::DISALLOWED;
}
void PrecacheManager::StartPrecaching(
const PrecacheCompletionCallback& precache_completion_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (is_precaching_) {
DLOG(WARNING) << "Cannot start precaching because precaching is already "
"in progress.";
return;
}
precache_completion_callback_ = precache_completion_callback;
if (IsInExperimentGroup()) {
is_precaching_ = true;
BrowserThread::PostTask(
BrowserThread::DB, FROM_HERE,
base::Bind(&PrecacheDatabase::DeleteExpiredPrecacheHistory,
base::Unretained(precache_database_.get()),
base::Time::Now()));
// Request NumTopHosts() top hosts. Note that PrecacheFetcher is further
// bound by the value of PrecacheConfigurationSettings.top_sites_count, as
// retrieved from the server.
history_service_->TopHosts(
NumTopHosts(),
base::Bind(&PrecacheManager::OnHostsReceived, AsWeakPtr()));
} else if (IsInControlGroup()) {
// Set is_precaching_ so that the longer delay is placed between calls to
// TopHosts.
is_precaching_ = true;
// Calculate TopHosts solely for metrics purposes.
history_service_->TopHosts(
NumTopHosts(),
base::Bind(&PrecacheManager::OnHostsReceivedThenDone, AsWeakPtr()));
} else {
if (PrecachingAllowed() != AllowedType::PENDING) {
// We are not waiting on the sync backend to be initialized. The user
// either is not in the field trial, or does not have sync enabled.
// Pretend that precaching started, so that the PrecacheServiceLauncher
// doesn't try to start it again.
is_precaching_ = true;
}
OnDone();
}
}
void PrecacheManager::CancelPrecaching() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!is_precaching_) {
// Do nothing if precaching is not in progress.
return;
}
is_precaching_ = false;
// Destroying the |precache_fetcher_| will cancel any fetch in progress.
precache_fetcher_.reset();
// Uninitialize the callback so that any scoped_refptrs in it are released.
precache_completion_callback_.Reset();
}
bool PrecacheManager::IsPrecaching() const {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
return is_precaching_;
}
void PrecacheManager::RecordStatsForFetch(const GURL& url,
const GURL& referrer,
const base::TimeDelta& latency,
const base::Time& fetch_time,
int64_t size,
bool was_cached) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (size == 0 || url.is_empty() || !url.SchemeIsHTTPOrHTTPS()) {
// Ignore empty responses, empty URLs, or URLs that aren't HTTP or HTTPS.
return;
}
if (!history_service_)
return;
history_service_->HostRankIfAvailable(
referrer,
base::Bind(&PrecacheManager::RecordStatsForFetchInternal, AsWeakPtr(),
url, latency, fetch_time, size, was_cached));
}
void PrecacheManager::RecordStatsForFetchInternal(
const GURL& url,
const base::TimeDelta& latency,
const base::Time& fetch_time,
int64_t size,
bool was_cached,
int host_rank) {
if (is_precaching_) {
// Assume that precache is responsible for all requests made while
// precaching is currently in progress.
// TODO(sclittle): Make PrecacheFetcher explicitly mark precache-motivated
// fetches, and use that to determine whether or not a fetch was motivated
// by precaching.
BrowserThread::PostTask(
BrowserThread::DB, FROM_HERE,
base::Bind(&PrecacheDatabase::RecordURLPrefetch,
base::Unretained(precache_database_.get()), url, latency,
fetch_time, size, was_cached));
} else {
bool is_connection_cellular =
net::NetworkChangeNotifier::IsConnectionCellular(
net::NetworkChangeNotifier::GetConnectionType());
BrowserThread::PostTask(
BrowserThread::DB, FROM_HERE,
base::Bind(&PrecacheDatabase::RecordURLNonPrefetch,
base::Unretained(precache_database_.get()), url, latency,
fetch_time, size, was_cached, host_rank,
is_connection_cellular));
}
}
void PrecacheManager::ClearHistory() {
// PrecacheDatabase::ClearHistory must run after PrecacheDatabase::Init has
// finished. Using PostNonNestableTask guarantees this, by definition. See
// base::SequencedTaskRunner for details.
BrowserThread::PostNonNestableTask(
BrowserThread::DB, FROM_HERE,
base::Bind(&PrecacheDatabase::ClearHistory,
base::Unretained(precache_database_.get())));
}
void PrecacheManager::Shutdown() {
CancelPrecaching();
}
void PrecacheManager::OnDone() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
precache_fetcher_.reset();
// Run completion callback if not null. It's null if the client is in the
// Control group and CancelPrecaching is called before TopHosts computation
// finishes.
if (!precache_completion_callback_.is_null()) {
precache_completion_callback_.Run(!is_precaching_);
// Uninitialize the callback so that any scoped_refptrs in it are released.
precache_completion_callback_.Reset();
}
is_precaching_ = false;
}
void PrecacheManager::OnHostsReceived(
const history::TopHostsList& host_counts) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!is_precaching_) {
// Don't start precaching if it was canceled while waiting for the list of
// hosts.
return;
}
std::vector<std::string> hosts;
for (const auto& host_count : host_counts)
hosts.push_back(host_count.first);
// Start precaching.
precache_fetcher_.reset(new PrecacheFetcher(
hosts,
content::BrowserContext::GetDefaultStoragePartition(browser_context_)->
GetURLRequestContext(),
GURL(variations::GetVariationParamValue(
kPrecacheFieldTrialName, kConfigURLParam)),
variations::GetVariationParamValue(
kPrecacheFieldTrialName, kManifestURLPrefixParam),
this));
precache_fetcher_->Start();
}
void PrecacheManager::OnHostsReceivedThenDone(
const history::TopHostsList& host_counts) {
OnDone();
}
} // namespace precache