| // 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/logging.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/time/time.h" |
| #include "components/data_reduction_proxy/core/browser/data_reduction_proxy_settings.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/precache/core/proto/precache.pb.h" |
| #include "components/precache/core/proto/unfinished_work.pb.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/sync/driver/sync_service.h" |
| #include "components/variations/metrics_util.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" |
| #include "net/http/http_cache.h" |
| #include "net/url_request/url_request_context.h" |
| #include "net/url_request/url_request_context_getter.h" |
| |
| using content::BrowserThread; |
| |
| namespace precache { |
| |
| const char kPrecacheFieldTrialName[] = "Precache"; |
| const char kMinCacheSizeParam[] = "min_cache_size"; |
| |
| namespace { |
| |
| const char kPrecacheFieldTrialEnabledGroup[] = "Enabled"; |
| const char kPrecacheFieldTrialControlGroup[] = "Control"; |
| const char kConfigURLParam[] = "config_url"; |
| const char kManifestURLPrefixParam[] = "manifest_url_prefix"; |
| const char kDataReductionProxyParam[] = "disable_if_data_reduction_proxy"; |
| const size_t kNumTopHosts = 100; |
| |
| } // namespace |
| |
| size_t NumTopHosts() { |
| return kNumTopHosts; |
| } |
| |
| PrecacheManager::PrecacheManager( |
| content::BrowserContext* browser_context, |
| const syncer::SyncService* const sync_service, |
| const history::HistoryService* const history_service, |
| const data_reduction_proxy::DataReductionProxySettings* |
| data_reduction_proxy_settings, |
| Delegate* delegate, |
| const base::FilePath& db_path, |
| std::unique_ptr<PrecacheDatabase> precache_database) |
| : browser_context_(browser_context), |
| sync_service_(sync_service), |
| history_service_(history_service), |
| data_reduction_proxy_settings_(data_reduction_proxy_settings), |
| delegate_(delegate), |
| is_precaching_(false) { |
| precache_database_ = std::move(precache_database); |
| 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 { |
| bool disable_if_proxy = !variations::GetVariationParamValue( |
| kPrecacheFieldTrialName, kDataReductionProxyParam).empty(); |
| if (disable_if_proxy && |
| (!data_reduction_proxy_settings_ || |
| data_reduction_proxy_settings_->IsDataReductionProxyEnabled())) |
| return AllowedType::DISALLOWED; |
| |
| if (!(sync_service_ && sync_service_->IsEngineInitialized())) |
| return AllowedType::PENDING; |
| |
| // SyncService delegates to SyncPrefs, which must be called on the UI thread. |
| if (history_service_ && !sync_service_->IsLocalSyncEnabled() && |
| sync_service_->GetActiveDataTypes().Has(syncer::SESSIONS) && |
| !sync_service_->GetEncryptedDataTypes().Has(syncer::SESSIONS)) { |
| return AllowedType::ALLOWED; |
| } |
| |
| return AllowedType::DISALLOWED; |
| } |
| |
| void PrecacheManager::OnCacheBackendReceived(int net_error_code) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| if (net_error_code != net::OK) { |
| // Assume there is no cache. |
| cache_backend_ = nullptr; |
| OnCacheSizeReceived(0); |
| return; |
| } |
| DCHECK(cache_backend_); |
| int result = cache_backend_->CalculateSizeOfAllEntries(base::Bind( |
| &PrecacheManager::OnCacheSizeReceived, base::Unretained(this))); |
| if (result == net::ERR_IO_PENDING) { |
| // Wait for the callback. |
| } else if (result >= 0) { |
| // The result is the expected bytes already. |
| OnCacheSizeReceived(result); |
| } else { |
| // Error occurred. Couldn't get the size. Assume there is no cache. |
| OnCacheSizeReceived(0); |
| } |
| cache_backend_ = nullptr; |
| } |
| |
| void PrecacheManager::OnCacheSizeReceived(int cache_size_bytes) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&PrecacheManager::OnCacheSizeReceivedInUIThread, |
| base::Unretained(this), cache_size_bytes)); |
| } |
| |
| void PrecacheManager::OnCacheSizeReceivedInUIThread(int cache_size_bytes) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| UMA_HISTOGRAM_MEMORY_KB("Precache.CacheSize.AllEntries", |
| cache_size_bytes / 1024); |
| |
| if (cache_size_bytes < min_cache_size_bytes_) { |
| OnDone(); // Do not continue. |
| } else { |
| BrowserThread::PostTaskAndReplyWithResult( |
| BrowserThread::DB, FROM_HERE, |
| base::Bind(&PrecacheDatabase::GetUnfinishedWork, |
| base::Unretained(precache_database_.get())), |
| base::Bind(&PrecacheManager::OnGetUnfinishedWorkDone, AsWeakPtr())); |
| } |
| } |
| |
| void PrecacheManager::PrecacheIfCacheIsBigEnough( |
| scoped_refptr<net::URLRequestContextGetter> url_request_context_getter) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| CHECK(url_request_context_getter); |
| |
| // Continue with OnGetUnfinishedWorkDone only if the size of the cache is |
| // at least min_cache_size_bytes_. |
| // Class disk_cache::Backend does not expose its maximum size. However, caches |
| // are usually full, so we can use the size of all the entries stored in the |
| // cache (via CalculateSizeOfAllEntries) as a proxy of its maximum size. |
| net::URLRequestContext* context = |
| url_request_context_getter->GetURLRequestContext(); |
| if (!context) { |
| OnCacheSizeReceived(0); |
| return; |
| } |
| net::HttpTransactionFactory* factory = context->http_transaction_factory(); |
| if (!factory) { |
| OnCacheSizeReceived(0); |
| return; |
| } |
| net::HttpCache* cache = factory->GetCache(); |
| if (!cache) { |
| // There is no known cache. Assume that there is no cache. |
| // TODO(jamartin): I'm not sure this can be an actual posibility. Consider |
| // making this a CHECK(cache). |
| OnCacheSizeReceived(0); |
| return; |
| } |
| const int net_error_code = cache->GetBackend( |
| &cache_backend_, base::Bind(&PrecacheManager::OnCacheBackendReceived, |
| base::Unretained(this))); |
| if (net_error_code != net::ERR_IO_PENDING) { |
| // No need to wait for the callback. The callback hasn't been called with |
| // the appropriate code, so we call it directly. |
| OnCacheBackendReceived(net_error_code); |
| } |
| } |
| |
| 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; |
| |
| is_precaching_ = true; |
| BrowserThread::PostTask( |
| BrowserThread::DB, FROM_HERE, |
| base::Bind(&PrecacheDatabase::SetLastPrecacheTimestamp, |
| base::Unretained(precache_database_.get()), |
| base::Time::Now())); |
| |
| // Ignore boolean return value. In all documented failure cases, it sets the |
| // int to a reasonable value. |
| base::StringToInt(variations::GetVariationParamValue(kPrecacheFieldTrialName, |
| kMinCacheSizeParam), |
| &min_cache_size_bytes_); |
| if (min_cache_size_bytes_ <= 0) { |
| // Skip looking up the cache size, because it doesn't matter. |
| OnCacheSizeReceivedInUIThread(0); |
| return; |
| } |
| |
| scoped_refptr<net::URLRequestContextGetter> url_request_context_getter( |
| content::BrowserContext::GetDefaultStoragePartition(browser_context_) |
| ->GetURLRequestContext()); |
| if (!url_request_context_getter) { |
| OnCacheSizeReceivedInUIThread(0); |
| return; |
| } |
| |
| BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| base::Bind(&PrecacheManager::PrecacheIfCacheIsBigEnough, AsWeakPtr(), |
| std::move(url_request_context_getter))); |
| } |
| |
| void PrecacheManager::OnGetUnfinishedWorkDone( |
| std::unique_ptr<PrecacheUnfinishedWork> unfinished_work) { |
| // Reset progress on a prefetch that has taken too long to complete. |
| if (unfinished_work->has_start_time() && |
| base::Time::Now() - |
| base::Time::FromInternalValue(unfinished_work->start_time()) > |
| base::TimeDelta::FromHours(6)) { |
| PrecacheFetcher::RecordCompletionStatistics( |
| *unfinished_work, unfinished_work->top_host_size(), |
| unfinished_work->resource_size()); |
| unfinished_work.reset(new PrecacheUnfinishedWork); |
| } |
| // If this prefetch is new, set the start time. |
| if (!unfinished_work->has_start_time()) |
| unfinished_work->set_start_time(base::Time::Now().ToInternalValue()); |
| unfinished_work_ = std::move(unfinished_work); |
| bool needs_top_hosts = unfinished_work_->top_host_size() == 0; |
| |
| base::RecordAction(base::UserMetricsAction("Precache.Fetch.Begin")); |
| |
| if (IsInExperimentGroup()) { |
| 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. |
| if (needs_top_hosts) { |
| history_service_->TopHosts( |
| NumTopHosts(), |
| base::Bind(&PrecacheManager::OnHostsReceived, AsWeakPtr())); |
| } else { |
| InitializeAndStartFetcher(); |
| } |
| } else if (IsInControlGroup()) { |
| // Calculate TopHosts solely for metrics purposes. |
| if (needs_top_hosts) { |
| history_service_->TopHosts( |
| NumTopHosts(), |
| base::Bind(&PrecacheManager::OnHostsReceivedThenDone, AsWeakPtr())); |
| } else { |
| OnDone(); |
| } |
| } else { |
| if (PrecachingAllowed() != AllowedType::PENDING) { |
| // We are not waiting on the sync engine 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. |
| } |
| |
| OnDone(); |
| } |
| } |
| |
| void PrecacheManager::CancelPrecaching() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!is_precaching_) { |
| // Do nothing if precaching is not in progress. |
| return; |
| } |
| is_precaching_ = false; |
| // If cancellation occurs after StartPrecaching but before OnHostsReceived, |
| // is_precaching will be true, but the precache_fetcher_ will not yet be |
| // constructed. |
| if (precache_fetcher_) { |
| std::unique_ptr<PrecacheUnfinishedWork> unfinished_work = |
| precache_fetcher_->CancelPrecaching(); |
| if (unfinished_work) { |
| BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, |
| base::Bind(&PrecacheDatabase::SaveUnfinishedWork, |
| precache_database_->GetWeakPtr(), |
| base::Passed(&unfinished_work))); |
| } |
| // Destroying the |precache_fetcher_| will cancel any fetch in progress. |
| precache_fetcher_.reset(); |
| } |
| precache_completion_callback_.Reset(); |
| } |
| |
| bool PrecacheManager::IsPrecaching() const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| return is_precaching_; |
| } |
| |
| void PrecacheManager::UpdatePrecacheMetricsAndState( |
| const GURL& url, |
| const GURL& referrer, |
| const base::Time& fetch_time, |
| const net::HttpResponseInfo& info, |
| int64_t size, |
| bool is_user_traffic, |
| const base::Callback<void(base::Time)>& register_synthetic_trial) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| BrowserThread::PostTaskAndReplyWithResult( |
| BrowserThread::DB, FROM_HERE, |
| base::Bind(&PrecacheDatabase::GetLastPrecacheTimestamp, |
| base::Unretained(precache_database_.get())), |
| base::Bind(&PrecacheManager::RecordStatsForFetch, AsWeakPtr(), url, |
| referrer, fetch_time, info, size, register_synthetic_trial)); |
| |
| if (is_user_traffic && IsPrecaching()) |
| CancelPrecaching(); |
| } |
| |
| void PrecacheManager::RecordStatsForFetch( |
| const GURL& url, |
| const GURL& referrer, |
| const base::Time& fetch_time, |
| const net::HttpResponseInfo& info, |
| int64_t size, |
| const base::Callback<void(base::Time)>& register_synthetic_trial, |
| base::Time last_precache_time) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| register_synthetic_trial.Run(last_precache_time); |
| |
| 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, referrer.host(), fetch_time, info, size)); |
| } |
| |
| void PrecacheManager::RecordStatsForFetchInternal( |
| const GURL& url, |
| const std::string& referrer_host, |
| const base::Time& fetch_time, |
| const net::HttpResponseInfo& info, |
| int64_t size, |
| 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::RecordURLPrefetchMetrics, |
| base::Unretained(precache_database_.get()), info)); |
| } 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, fetch_time, |
| info, size, 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::OnManifestFetched(const std::string& host, |
| const PrecacheManifest& manifest) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (delegate_) |
| delegate_->OnManifestFetched(host, manifest); |
| } |
| |
| void PrecacheManager::OnHostsReceived( |
| const history::TopHostsList& host_counts) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| for (const auto& host_count : host_counts) { |
| TopHost* top_host = unfinished_work_->add_top_host(); |
| top_host->set_hostname(host_count.first); |
| top_host->set_visits(host_count.second); |
| } |
| InitializeAndStartFetcher(); |
| } |
| |
| void PrecacheManager::InitializeAndStartFetcher() { |
| if (!is_precaching_) { |
| // Don't start precaching if it was canceled while waiting for the list of |
| // hosts. |
| return; |
| } |
| // Start precaching. |
| precache_fetcher_.reset(new PrecacheFetcher( |
| content::BrowserContext::GetDefaultStoragePartition(browser_context_) |
| ->GetURLRequestContext(), |
| GURL(variations::GetVariationParamValue(kPrecacheFieldTrialName, |
| kConfigURLParam)), |
| variations::GetVariationParamValue(kPrecacheFieldTrialName, |
| kManifestURLPrefixParam), |
| std::move(unfinished_work_), |
| metrics::HashName( |
| base::FieldTrialList::FindFullName(kPrecacheFieldTrialName)), |
| precache_database_->GetWeakPtr(), |
| content::BrowserThread::GetTaskRunnerForThread( |
| content::BrowserThread::DB), |
| this)); |
| precache_fetcher_->Start(); |
| } |
| |
| void PrecacheManager::OnHostsReceivedThenDone( |
| const history::TopHostsList& host_counts) { |
| OnDone(); |
| } |
| |
| } // namespace precache |