1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "components/precache/core/precache_database.h" 6 7 #include "base/bind.h" 8 #include "base/files/file_path.h" 9 #include "base/message_loop/message_loop.h" 10 #include "base/metrics/histogram.h" 11 #include "base/time/time.h" 12 #include "sql/connection.h" 13 #include "sql/transaction.h" 14 #include "url/gurl.h" 15 16 namespace { 17 18 // The number of days old that an entry in the precache URL table can be before 19 // it is considered "old" and is removed from the table. 20 const int kPrecacheHistoryExpiryPeriodDays = 60; 21 22 } // namespace 23 24 namespace precache { 25 26 PrecacheDatabase::PrecacheDatabase() : is_flush_posted_(false) { 27 // A PrecacheDatabase can be constructed on any thread. 28 thread_checker_.DetachFromThread(); 29 } 30 31 PrecacheDatabase::~PrecacheDatabase() { 32 // Since the PrecacheDatabase is refcounted, it will only be deleted if there 33 // are no references remaining to it, meaning that it is not in use. Thus, it 34 // is safe to delete it, regardless of what thread we are on. 35 thread_checker_.DetachFromThread(); 36 } 37 38 bool PrecacheDatabase::Init(const base::FilePath& db_path) { 39 DCHECK(thread_checker_.CalledOnValidThread()); 40 DCHECK(!db_); // Init must only be called once. 41 42 db_.reset(new sql::Connection()); 43 db_->set_histogram_tag("Precache"); 44 45 if (!db_->Open(db_path)) { 46 // Don't initialize the URL table if unable to access 47 // the database. 48 return false; 49 } 50 51 if (!precache_url_table_.Init(db_.get())) { 52 // Raze and close the database connection to indicate that it's not usable, 53 // and so that the database will be created anew next time, in case it's 54 // corrupted. 55 db_->RazeAndClose(); 56 return false; 57 } 58 return true; 59 } 60 61 void PrecacheDatabase::DeleteExpiredPrecacheHistory( 62 const base::Time& current_time) { 63 if (!IsDatabaseAccessible()) { 64 // Do nothing if unable to access the database. 65 return; 66 } 67 68 // Delete old precache history that has expired. 69 base::Time delete_end = current_time - base::TimeDelta::FromDays( 70 kPrecacheHistoryExpiryPeriodDays); 71 buffered_writes_.push_back( 72 base::Bind(&PrecacheURLTable::DeleteAllPrecachedBefore, 73 base::Unretained(&precache_url_table_), delete_end)); 74 75 Flush(); 76 } 77 78 void PrecacheDatabase::RecordURLPrecached(const GURL& url, 79 const base::Time& fetch_time, 80 int64 size, bool was_cached) { 81 if (!IsDatabaseAccessible()) { 82 // Don't track anything if unable to access the database. 83 return; 84 } 85 86 if (buffered_urls_.find(url.spec()) != buffered_urls_.end()) { 87 // If the URL for this fetch is in the write buffer, then flush the write 88 // buffer. 89 Flush(); 90 } 91 92 if (was_cached && !precache_url_table_.HasURL(url)) { 93 // Since the precache came from the cache, and there's no entry in the URL 94 // table for the URL, this means that the resource was already in the cache 95 // because of user browsing. Thus, this precache had no effect, so ignore 96 // it. 97 return; 98 } 99 100 if (!was_cached) { 101 // The precache only counts as overhead if it was downloaded over the 102 // network. 103 UMA_HISTOGRAM_COUNTS("Precache.DownloadedPrecacheMotivated", size); 104 } 105 106 // Use the URL table to keep track of URLs that are in the cache thanks to 107 // precaching. If a row for the URL already exists, than update the timestamp 108 // to |fetch_time|. 109 buffered_writes_.push_back( 110 base::Bind(&PrecacheURLTable::AddURL, 111 base::Unretained(&precache_url_table_), url, fetch_time)); 112 buffered_urls_.insert(url.spec()); 113 MaybePostFlush(); 114 } 115 116 void PrecacheDatabase::RecordURLFetched(const GURL& url, 117 const base::Time& fetch_time, 118 int64 size, bool was_cached, 119 bool is_connection_cellular) { 120 if (!IsDatabaseAccessible()) { 121 // Don't track anything if unable to access the database. 122 return; 123 } 124 125 if (buffered_urls_.find(url.spec()) != buffered_urls_.end()) { 126 // If the URL for this fetch is in the write buffer, then flush the write 127 // buffer. 128 Flush(); 129 } 130 131 if (was_cached && !precache_url_table_.HasURL(url)) { 132 // Ignore cache hits that precache can't take credit for. 133 return; 134 } 135 136 if (!was_cached) { 137 // The fetch was served over the network during user browsing, so count it 138 // as downloaded non-precache bytes. 139 UMA_HISTOGRAM_COUNTS("Precache.DownloadedNonPrecache", size); 140 if (is_connection_cellular) { 141 UMA_HISTOGRAM_COUNTS("Precache.DownloadedNonPrecache.Cellular", size); 142 } 143 } else { 144 // The fetch was served from the cache, and since there's an entry for this 145 // URL in the URL table, this means that the resource was served from the 146 // cache only because precaching put it there. Thus, precaching was helpful, 147 // so count the fetch as saved bytes. 148 UMA_HISTOGRAM_COUNTS("Precache.Saved", size); 149 if (is_connection_cellular) { 150 UMA_HISTOGRAM_COUNTS("Precache.Saved.Cellular", size); 151 } 152 } 153 154 // Since the resource has been fetched during user browsing, remove any record 155 // of that URL having been precached from the URL table, if any exists. 156 // The current fetch would have put this resource in the cache regardless of 157 // whether or not it was previously precached, so delete any record of that 158 // URL having been precached from the URL table. 159 buffered_writes_.push_back( 160 base::Bind(&PrecacheURLTable::DeleteURL, 161 base::Unretained(&precache_url_table_), url)); 162 buffered_urls_.insert(url.spec()); 163 MaybePostFlush(); 164 } 165 166 bool PrecacheDatabase::IsDatabaseAccessible() const { 167 DCHECK(thread_checker_.CalledOnValidThread()); 168 DCHECK(db_); 169 170 return db_->is_open(); 171 } 172 173 void PrecacheDatabase::Flush() { 174 DCHECK(thread_checker_.CalledOnValidThread()); 175 if (buffered_writes_.empty()) { 176 // Do nothing if there's nothing to flush. 177 DCHECK(buffered_urls_.empty()); 178 return; 179 } 180 181 if (IsDatabaseAccessible()) { 182 sql::Transaction transaction(db_.get()); 183 if (transaction.Begin()) { 184 for (std::vector<base::Closure>::const_iterator it = 185 buffered_writes_.begin(); 186 it != buffered_writes_.end(); ++it) { 187 it->Run(); 188 } 189 190 transaction.Commit(); 191 } 192 } 193 194 // Clear the buffer, even if the database is inaccessible or unable to begin a 195 // transaction. 196 buffered_writes_.clear(); 197 buffered_urls_.clear(); 198 } 199 200 void PrecacheDatabase::PostedFlush() { 201 DCHECK(thread_checker_.CalledOnValidThread()); 202 DCHECK(is_flush_posted_); 203 is_flush_posted_ = false; 204 Flush(); 205 } 206 207 void PrecacheDatabase::MaybePostFlush() { 208 DCHECK(thread_checker_.CalledOnValidThread()); 209 210 if (buffered_writes_.empty() || is_flush_posted_) { 211 // There's no point in posting a flush if there's nothing to be flushed or 212 // if a flush has already been posted. 213 return; 214 } 215 216 DCHECK(base::MessageLoop::current()); 217 // Post a delayed task to flush the buffer in 1 second, so that multiple 218 // database writes can be buffered up and flushed together in the same 219 // transaction. 220 base::MessageLoop::current()->PostDelayedTask( 221 FROM_HERE, base::Bind(&PrecacheDatabase::PostedFlush, 222 scoped_refptr<PrecacheDatabase>(this)), 223 base::TimeDelta::FromSeconds(1)); 224 is_flush_posted_ = true; 225 } 226 227 } // namespace precache 228