// Copyright (c) 2009 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/thumbnail_store.h" #include #include #include "app/gfx/codec/jpeg_codec.h" #include "app/sql/statement.h" #include "app/sql/transaction.h" #include "base/basictypes.h" #include "base/file_util.h" #include "base/md5.h" #include "base/string_util.h" #include "base/thread.h" #include "base/values.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/history/history_notifications.h" #include "chrome/browser/profile.h" #include "chrome/common/pref_service.h" #include "googleurl/src/gurl.h" #include "third_party/skia/include/core/SkBitmap.h" ThumbnailStore::ThumbnailStore() : cache_(NULL), hs_(NULL), url_blacklist_(NULL), disk_data_loaded_(false) { } ThumbnailStore::~ThumbnailStore() { // Ensure that shutdown was called. DCHECK(hs_ == NULL); } void ThumbnailStore::Init(const FilePath& db_name, Profile* profile) { // Load thumbnails already in the database. g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &ThumbnailStore::InitializeFromDB, db_name, MessageLoop::current())); // Take ownership of a reference to the HistoryService. hs_ = profile->GetHistoryService(Profile::EXPLICIT_ACCESS); // Store a pointer to a persistent table of blacklisted URLs. url_blacklist_ = profile->GetPrefs()-> GetMutableDictionary(prefs::kNTPMostVisitedURLsBlacklist); DCHECK(url_blacklist_); // Get the list of most visited URLs and redirect information from the // HistoryService. most_visited_urls_.reset(new MostVisitedMap); timer_.Start(base::TimeDelta::FromSeconds(kUpdateIntervalSecs), this, &ThumbnailStore::UpdateURLData); UpdateURLData(); // Register to get notified when the history is cleared. registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED, Source(profile)); } bool ThumbnailStore::SetPageThumbnail(const GURL& url, const SkBitmap& thumbnail, const ThumbnailScore& score, bool fetch_redirects) { if (!cache_.get()) return false; if (!ShouldStoreThumbnailForURL(url) || (cache_->find(url) != cache_->end() && !ShouldReplaceThumbnailWith((*cache_)[url].score_, score))) return true; base::TimeTicks encode_start = base::TimeTicks::Now(); // Encode the SkBitmap to jpeg. scoped_refptr jpeg_data = new RefCountedBytes; SkAutoLockPixels thumbnail_lock(thumbnail); bool encoded = gfx::JPEGCodec::Encode( reinterpret_cast(thumbnail.getAddr32(0, 0)), gfx::JPEGCodec::FORMAT_BGRA, thumbnail.width(), thumbnail.height(), static_cast(thumbnail.rowBytes()), 90, &jpeg_data->data); base::TimeDelta delta = base::TimeTicks::Now() - encode_start; HISTOGRAM_TIMES("Thumbnail.Encode", delta); if (!encoded) return false; // Update the cache_ with the new thumbnail. (*cache_)[url] = CacheEntry(jpeg_data, score, true); // Get redirects for this URL. if (fetch_redirects) { hs_->QueryRedirectsTo(url, &consumer_, NewCallback(this, &ThumbnailStore::OnRedirectsForURLAvailable)); } return true; } bool ThumbnailStore::GetPageThumbnail( const GURL& url, RefCountedBytes** data) { if (!cache_.get() || IsURLBlacklisted(url)) return false; // Look up the |url| in the redirect list to find the final destination // which is the key into the |cache_|. history::RedirectMap::iterator it = redirect_urls_->find(url); if (it != redirect_urls_->end()) { // Return the first available thumbnail starting at the end of the // redirect list. history::RedirectList::reverse_iterator rit; for (rit = it->second->data.rbegin(); rit != it->second->data.rend(); ++rit) { if (cache_->find(*rit) != cache_->end()) { *data = (*cache_)[*rit].data_.get(); (*data)->AddRef(); return true; } } } // TODO(meelapshah) bug 14643: check past redirect lists if (cache_->find(url) == cache_->end()) return false; *data = (*cache_)[url].data_.get(); (*data)->AddRef(); return true; } void ThumbnailStore::Shutdown() { // We must release our reference to the HistoryService here to prevent // shutdown issues. Please refer to the comment in HistoryService::Cleanup // for details. hs_ = NULL; // De-register for notifications. registrar_.RemoveAll(); // Stop the timer to ensure that UpdateURLData is not called during shutdown. timer_.Stop(); // Write the cache to disk. This will schedule the disk operations to be run // on the file_thread. Note that Join() does not need to be called with the // file_thread because when the disk operation is scheduled, it will hold a // reference to |this| keeping this object alive. CleanCacheData(); } void ThumbnailStore::OnRedirectsForURLAvailable( HistoryService::Handle handle, GURL url, bool success, history::RedirectList* redirects) { if (!success) return; DCHECK(redirect_urls_.get()); // If A -> B -> C is a redirect chain, then this function would be called // with url=C and redirects = {B, A}. This is entered into the RedirectMap as // A => {B -> C}. if (redirects->empty()) { (*redirect_urls_)[url] = new RefCountedVector; } else { const GURL start_url = redirects->back(); std::reverse(redirects->begin(), redirects->end() - 1); *(redirects->end() - 1) = url; (*redirect_urls_)[start_url] = new RefCountedVector(*redirects); } } history::RedirectMap::iterator ThumbnailStore::GetRedirectIteratorForURL( const GURL& url) const { for (history::RedirectMap::iterator it = redirect_urls_->begin(); it != redirect_urls_->end(); ++it) { if (it->first == url || (!it->second->data.empty() && it->second->data.back() == url)) return it; } return redirect_urls_->end(); } void ThumbnailStore::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { if (type.value != NotificationType::HISTORY_URLS_DELETED) { NOTREACHED(); return; } Details url_details(details); // If all history was cleared, clear all of our data and reset the update // timer. if (url_details->all_history) { most_visited_urls_->clear(); redirect_urls_->clear(); cache_->clear(); timer_.Reset(); } } void ThumbnailStore::NotifyThumbnailStoreReady() { NotificationService::current()->Notify( NotificationType::THUMBNAIL_STORE_READY, Source(this), NotificationService::NoDetails()); } void ThumbnailStore::UpdateURLData() { DCHECK(url_blacklist_); int result_count = ThumbnailStore::kMaxCacheSize + url_blacklist_->size(); hs_->QueryTopURLsAndRedirects(result_count, &consumer_, NewCallback(this, &ThumbnailStore::OnURLDataAvailable)); } void ThumbnailStore::OnURLDataAvailable(HistoryService::Handle handle, bool success, std::vector* urls, history::RedirectMap* redirects) { if (!success) return; DCHECK(urls); DCHECK(redirects); // Each element of |urls| is the start of a redirect chain. When thumbnails // are stored from TabContents, the tails of the redirect chains are // associated with the image. Since SetPageThumbnail is called frequently, we // look up the tail end of each element in |urls| and insert that into the // MostVisitedMap. This way SetPageThumbnail can more quickly check if a // given url is in the most visited list. most_visited_urls_->clear(); for (size_t i = 0; i < urls->size(); ++i) { history::RedirectMap::iterator it = redirects->find(urls->at(i)); if (it->second->data.empty()) (*most_visited_urls_)[urls->at(i)] = GURL(); else (*most_visited_urls_)[it->second->data.back()] = urls->at(i); } redirect_urls_.reset(new history::RedirectMap(*redirects)); if (IsReady()) NotifyThumbnailStoreReady(); CleanCacheData(); } void ThumbnailStore::CleanCacheData() { if (!cache_.get()) return; scoped_refptr > urls_to_delete = new RefCountedVector; Cache* data_to_save = new Cache; // CommitCacheToDB will delete this // Iterate the cache, storing urls to be deleted and dirty cache entries to // be written to disk. for (Cache::iterator cache_it = cache_->begin(); cache_it != cache_->end();) { history::RedirectMap::iterator redirect_it = GetRedirectIteratorForURL(cache_it->first); const GURL* url = redirect_it == redirect_urls_->end() ? NULL : &redirect_it->first; // If this URL is blacklisted or not in the most visited list, mark it for // deletion. Otherwise, if the cache entry is dirty, mark it to be written // to disk. if (url == NULL || IsURLBlacklisted(*url) || !IsPopular(*url)) { urls_to_delete->data.push_back(cache_it->first); cache_->erase(cache_it++); } else { if (cache_it->second.dirty_) { data_to_save->insert(*cache_it); cache_it->second.dirty_ = false; } ++cache_it; } } g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &ThumbnailStore::CommitCacheToDB, urls_to_delete, data_to_save)); } void ThumbnailStore::CommitCacheToDB( scoped_refptr > urls_to_delete, Cache* data) { scoped_ptr data_to_save(data); if (!db_.is_open()) return; base::TimeTicks db_start = base::TimeTicks::Now(); sql::Transaction transaction(&db_); if (!transaction.Begin()) return; // Delete old thumbnails. if (urls_to_delete.get()) { for (std::vector::iterator it = urls_to_delete->data.begin(); it != urls_to_delete->data.end(); ++it) { sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, "DELETE FROM thumbnails WHERE url=?")); if (!statement) return; statement.BindString(0, it->spec()); if (!statement.Run()) NOTREACHED(); } } // Update cached thumbnails. if (data_to_save.get()) { for (Cache::iterator it = data_to_save->begin(); it != data_to_save->end(); ++it) { sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, "INSERT OR REPLACE INTO thumbnails " "(url, boring_score, good_clipping, " "at_top, time_taken, data) " "VALUES (?,?,?,?,?,?)")); statement.BindString(0, it->first.spec()); statement.BindDouble(1, it->second.score_.boring_score); statement.BindBool(2, it->second.score_.good_clipping); statement.BindBool(3, it->second.score_.at_top); statement.BindInt64(4, it->second.score_.time_at_snapshot.ToInternalValue()); statement.BindBlob(5, &it->second.data_->data[0], static_cast(it->second.data_->data.size())); if (!statement.Run()) DLOG(WARNING) << "Unable to insert thumbnail for URL"; } } transaction.Commit(); base::TimeDelta delta = base::TimeTicks::Now() - db_start; HISTOGRAM_TIMES("ThumbnailStore.WriteDBToDisk", delta); } void ThumbnailStore::InitializeFromDB(const FilePath& db_name, MessageLoop* cb_loop) { db_.set_page_size(4096); db_.set_cache_size(64); db_.set_exclusive_locking(); if (!db_.Open(db_name)) return; if (!db_.DoesTableExist("thumbnails")) { if (!db_.Execute("CREATE TABLE thumbnails (" "url LONGVARCHAR PRIMARY KEY," "boring_score DOUBLE DEFAULT 1.0," "good_clipping INTEGER DEFAULT 0," "at_top INTEGER DEFAULT 0," "time_taken INTEGER DEFAULT 0," "data BLOB)")) return; } if (cb_loop) GetAllThumbnailsFromDisk(cb_loop); } void ThumbnailStore::GetAllThumbnailsFromDisk(MessageLoop* cb_loop) { sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, "SELECT * FROM thumbnails")); if (!statement) return; Cache* cache = new Cache; while (statement.Step()) { // The URL GURL url(statement.ColumnString(0)); // The score. ThumbnailScore score(statement.ColumnDouble(1), // Boring score statement.ColumnBool(2), // Good clipping statement.ColumnBool(3), // At top base::Time::FromInternalValue( statement.ColumnInt64(4))); // Time taken // The image. scoped_refptr data = new RefCountedBytes; statement.ColumnBlobAsVector(5, &data->data); (*cache)[url] = CacheEntry(data, score, false); } cb_loop->PostTask(FROM_HERE, NewRunnableMethod(this, &ThumbnailStore::OnDiskDataAvailable, cache)); } void ThumbnailStore::OnDiskDataAvailable(Cache* cache) { if (cache) cache_.reset(cache); disk_data_loaded_ = true; if (IsReady()) NotifyThumbnailStoreReady(); } bool ThumbnailStore::ShouldStoreThumbnailForURL(const GURL& url) const { if (!cache_.get()) return false; if (IsURLBlacklisted(url) || cache_->size() >= kMaxCacheSize) return false; return IsPopular(url); } bool ThumbnailStore::IsURLBlacklisted(const GURL& url) const { if (url_blacklist_) return url_blacklist_->HasKey(GetDictionaryKeyForURL(url.spec())); return false; } std::wstring ThumbnailStore::GetDictionaryKeyForURL( const std::string& url) const { return ASCIIToWide(MD5String(url)); } bool ThumbnailStore::IsPopular(const GURL& url) const { return most_visited_urls_->size() < kMaxCacheSize || most_visited_urls_->find(url) != most_visited_urls_->end(); }