// 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/core/precache_database.h" #include "base/bind.h" #include "base/files/file_path.h" #include "base/message_loop/message_loop.h" #include "base/metrics/histogram.h" #include "base/time/time.h" #include "sql/connection.h" #include "sql/transaction.h" #include "url/gurl.h" namespace { // The number of days old that an entry in the precache URL table can be before // it is considered "old" and is removed from the table. const int kPrecacheHistoryExpiryPeriodDays = 60; } // namespace namespace precache { PrecacheDatabase::PrecacheDatabase() : is_flush_posted_(false) { // A PrecacheDatabase can be constructed on any thread. thread_checker_.DetachFromThread(); } PrecacheDatabase::~PrecacheDatabase() { // Since the PrecacheDatabase is refcounted, it will only be deleted if there // are no references remaining to it, meaning that it is not in use. Thus, it // is safe to delete it, regardless of what thread we are on. thread_checker_.DetachFromThread(); } bool PrecacheDatabase::Init(const base::FilePath& db_path) { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(!db_); // Init must only be called once. db_.reset(new sql::Connection()); db_->set_histogram_tag("Precache"); if (!db_->Open(db_path)) { // Don't initialize the URL table if unable to access // the database. return false; } if (!precache_url_table_.Init(db_.get())) { // Raze and close the database connection to indicate that it's not usable, // and so that the database will be created anew next time, in case it's // corrupted. db_->RazeAndClose(); return false; } return true; } void PrecacheDatabase::DeleteExpiredPrecacheHistory( const base::Time& current_time) { if (!IsDatabaseAccessible()) { // Do nothing if unable to access the database. return; } // Delete old precache history that has expired. base::Time delete_end = current_time - base::TimeDelta::FromDays( kPrecacheHistoryExpiryPeriodDays); buffered_writes_.push_back( base::Bind(&PrecacheURLTable::DeleteAllPrecachedBefore, base::Unretained(&precache_url_table_), delete_end)); Flush(); } void PrecacheDatabase::RecordURLPrecached(const GURL& url, const base::Time& fetch_time, int64 size, bool was_cached) { if (!IsDatabaseAccessible()) { // Don't track anything if unable to access the database. return; } if (buffered_urls_.find(url.spec()) != buffered_urls_.end()) { // If the URL for this fetch is in the write buffer, then flush the write // buffer. Flush(); } if (was_cached && !precache_url_table_.HasURL(url)) { // Since the precache came from the cache, and there's no entry in the URL // table for the URL, this means that the resource was already in the cache // because of user browsing. Thus, this precache had no effect, so ignore // it. return; } if (!was_cached) { // The precache only counts as overhead if it was downloaded over the // network. UMA_HISTOGRAM_COUNTS("Precache.DownloadedPrecacheMotivated", static_cast(size)); } // Use the URL table to keep track of URLs that are in the cache thanks to // precaching. If a row for the URL already exists, than update the timestamp // to |fetch_time|. buffered_writes_.push_back( base::Bind(&PrecacheURLTable::AddURL, base::Unretained(&precache_url_table_), url, fetch_time)); buffered_urls_.insert(url.spec()); MaybePostFlush(); } void PrecacheDatabase::RecordURLFetched(const GURL& url, const base::Time& fetch_time, int64 size, bool was_cached, bool is_connection_cellular) { if (!IsDatabaseAccessible()) { // Don't track anything if unable to access the database. return; } if (buffered_urls_.find(url.spec()) != buffered_urls_.end()) { // If the URL for this fetch is in the write buffer, then flush the write // buffer. Flush(); } if (was_cached && !precache_url_table_.HasURL(url)) { // Ignore cache hits that precache can't take credit for. return; } base::HistogramBase::Sample size_sample = static_cast(size); if (!was_cached) { // The fetch was served over the network during user browsing, so count it // as downloaded non-precache bytes. UMA_HISTOGRAM_COUNTS("Precache.DownloadedNonPrecache", size_sample); if (is_connection_cellular) { UMA_HISTOGRAM_COUNTS("Precache.DownloadedNonPrecache.Cellular", size_sample); } } else { // The fetch was served from the cache, and since there's an entry for this // URL in the URL table, this means that the resource was served from the // cache only because precaching put it there. Thus, precaching was helpful, // so count the fetch as saved bytes. UMA_HISTOGRAM_COUNTS("Precache.Saved", size_sample); if (is_connection_cellular) { UMA_HISTOGRAM_COUNTS("Precache.Saved.Cellular", size_sample); } } // Since the resource has been fetched during user browsing, remove any record // of that URL having been precached from the URL table, if any exists. // The current fetch would have put this resource in the cache regardless of // whether or not it was previously precached, so delete any record of that // URL having been precached from the URL table. buffered_writes_.push_back( base::Bind(&PrecacheURLTable::DeleteURL, base::Unretained(&precache_url_table_), url)); buffered_urls_.insert(url.spec()); MaybePostFlush(); } bool PrecacheDatabase::IsDatabaseAccessible() const { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(db_); return db_->is_open(); } void PrecacheDatabase::Flush() { DCHECK(thread_checker_.CalledOnValidThread()); if (buffered_writes_.empty()) { // Do nothing if there's nothing to flush. DCHECK(buffered_urls_.empty()); return; } if (IsDatabaseAccessible()) { sql::Transaction transaction(db_.get()); if (transaction.Begin()) { for (std::vector::const_iterator it = buffered_writes_.begin(); it != buffered_writes_.end(); ++it) { it->Run(); } transaction.Commit(); } } // Clear the buffer, even if the database is inaccessible or unable to begin a // transaction. buffered_writes_.clear(); buffered_urls_.clear(); } void PrecacheDatabase::PostedFlush() { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(is_flush_posted_); is_flush_posted_ = false; Flush(); } void PrecacheDatabase::MaybePostFlush() { DCHECK(thread_checker_.CalledOnValidThread()); if (buffered_writes_.empty() || is_flush_posted_) { // There's no point in posting a flush if there's nothing to be flushed or // if a flush has already been posted. return; } DCHECK(base::MessageLoop::current()); // Post a delayed task to flush the buffer in 1 second, so that multiple // database writes can be buffered up and flushed together in the same // transaction. base::MessageLoop::current()->PostDelayedTask( FROM_HERE, base::Bind(&PrecacheDatabase::PostedFlush, scoped_refptr(this)), base::TimeDelta::FromSeconds(1)); is_flush_posted_ = true; } } // namespace precache