#include "config.h"
#include "IconDatabase.h"
+#include "AutodrainedPool.h"
#include "CString.h"
+#include "DocumentLoader.h"
#include "FileSystem.h"
-#include "IconDataCache.h"
+#include "IconDatabaseClient.h"
+#include "IconRecord.h"
#include "Image.h"
+#include "IntSize.h"
+#include "KURL.h"
#include "Logging.h"
+#include "PageURLRecord.h"
#include "SQLStatement.h"
#include "SQLTransaction.h"
#include "SystemTime.h"
#include <sys/stat.h>
#endif
+// For methods that are meant to support API from the main thread - should not be called internally
+#define ASSERT_NOT_SYNC_THREAD() ASSERT(!m_syncThreadRunning || !IS_ICON_SYNC_THREAD())
+
+// For methods that are meant to support the sync thread ONLY
+#define IS_ICON_SYNC_THREAD() pthread_equal(pthread_self(), m_syncThread)
+#define ASSERT_ICON_SYNC_THREAD() ASSERT(IS_ICON_SYNC_THREAD())
+
namespace WebCore {
static IconDatabase* sharedIconDatabase = 0;
+static int databaseCleanupCounter = 0;
// This version number is in the DB and marks the current generation of the schema
-// Theoretically once the switch is flipped this should never change
-// Currently, an out-of-date schema causes the DB to be wiped and reset. This isn't
+// Currently, a mismatched schema causes the DB to be wiped and reset. This isn't
// so bad during development but in the future, we would need to write a conversion
// function to advance older released schemas to "current"
-const int currentDatabaseVersion = 5;
+const int currentDatabaseVersion = 6;
-// Icons expire once a day
-const int iconExpirationTime = 60*60*24;
-// Absent icons are rechecked once a week
-const int missingIconExpirationTime = 60*60*24*7;
+// Icons expire once every 4 days
+const int iconExpirationTime = 60*60*24*4;
const int updateTimerDelay = 5;
static bool checkIntegrityOnOpen = false;
-const String& IconDatabase::defaultDatabaseFilename()
+#ifndef NDEBUG
+static String urlForLogging(const String& url)
{
- static String defaultDatabaseFilename = "Icons.db";
- return defaultDatabaseFilename;
+ static unsigned urlTruncationLength = 120;
+
+ if (url.length() < urlTruncationLength)
+ return url;
+ return url.substring(0, urlTruncationLength) + "...";
}
+#endif
-void IconDatabase::checkIntegrityBeforeOpening()
+static IconDatabaseClient* defaultClient()
{
- checkIntegrityOnOpen = true;
+ static IconDatabaseClient* defaultClient = new IconDatabaseClient();
+ return defaultClient;
}
IconDatabase* iconDatabase()
return sharedIconDatabase;
}
-IconDatabase::IconDatabase()
- : m_timeStampForIconURLStatement(0)
- , m_iconURLForPageURLStatement(0)
- , m_hasIconForIconURLStatement(0)
- , m_forgetPageURLStatement(0)
- , m_setIconIDForPageURLStatement(0)
- , m_getIconIDForIconURLStatement(0)
- , m_addIconForIconURLStatement(0)
- , m_imageDataForIconURLStatement(0)
- , m_importedStatement(0)
- , m_setImportedStatement(0)
- , m_currentDB(&m_mainDB)
- , m_defaultIconDataCache(0)
- , m_isEnabled(false)
- , m_privateBrowsingEnabled(false)
- , m_startupTimer(this, &IconDatabase::pruneUnretainedIconsOnStartup)
- , m_updateTimer(this, &IconDatabase::updateDatabase)
- , m_initialPruningComplete(false)
- , m_imported(false)
- , m_isImportedSet(false)
+// ************************
+// *** Main Thread Only ***
+// ************************
+
+void IconDatabase::setClient(IconDatabaseClient* client)
{
-
+ // We don't allow a null client, because we never null check it anywhere in this code
+ // Also don't allow a client change after the thread has already began
+ // (setting the client should occur before the database is opened)
+ ASSERT(client);
+ ASSERT(!m_syncThreadRunning);
+ if (!client || m_syncThreadRunning)
+ return;
+
+ m_client = client;
}
bool makeAllDirectories(const String& path)
if (!SHCreateDirectoryEx(0, fullPath.charactersWithNullTermination(), 0)) {
DWORD error = GetLastError();
if (error != ERROR_FILE_EXISTS && error != ERROR_ALREADY_EXISTS) {
- LOG_ERROR("Failed to create path %s", path.ascii().data());
+ LOG_ERROR("Failed to create path %s", path.utf8().data());
return false;
}
}
#else
CString fullPath = path.utf8();
+ if (!access(fullPath.data(), F_OK))
+ return true;
+
char* p = fullPath.mutableData() + 1;
int length = fullPath.length();
bool IconDatabase::open(const String& databasePath)
{
+ ASSERT_NOT_SYNC_THREAD();
+
if (!m_isEnabled)
return false;
-
+
if (isOpen()) {
LOG_ERROR("Attempt to reopen the IconDatabase which is already open. Must close it first.");
return false;
}
-
- // <rdar://problem/4730811> - Need to create the database path if it doesn't already exist
+
+ // Need to create the database path if it doesn't already exist
makeAllDirectories(databasePath);
// First we'll formulate the full path for the database file
dbFilename = databasePath + "/" + defaultDatabaseFilename();
#endif
- String journalFilename = dbFilename + "-journal";
- if (!checkIntegrityOnOpen)
- checkIntegrityOnOpen = fileExists(journalFilename);
-
- // <rdar://problem/4707718> - If user's Icon directory is unwritable, Safari will crash at startup
- // Now, we'll see if we can open the on-disk database. And, if we can't, we'll return false.
- // WebKit will then ignore us and act as if the database is disabled
- if (!m_mainDB.open(dbFilename)) {
- LOG_ERROR("Unable to open icon database at path %s - %s", dbFilename.ascii().data(), m_mainDB.lastErrorMsg());
- return false;
- }
-
- if (checkIntegrityOnOpen) {
- checkIntegrityOnOpen = false;
- if (!checkIntegrity()) {
- LOG(IconDatabase, "Integrity check was bad - dumping IconDatabase");
- close();
-
- // Should've been consumed by SQLite, delete just to make sure we don't see it again in the future;
- deleteFile(journalFilename);
- deleteFile(dbFilename);
-
- // Reopen the main database, creating it from scratch
- if (!m_mainDB.open(dbFilename)) {
- LOG_ERROR("Unable to open icon database at path %s - %s", dbFilename.ascii().data(), m_mainDB.lastErrorMsg());
- return false;
- }
- }
- }
-
- if (!isValidDatabase(m_mainDB)) {
- LOG(IconDatabase, "%s is missing or in an invalid state - reconstructing", dbFilename.ascii().data());
- m_mainDB.clearAllTables();
- createDatabaseTables(m_mainDB);
- }
-
- // Reduce sqlite RAM cache size from default 2000 pages (~1.5kB per page). 3MB of cache for icon database is overkill
- if (!SQLStatement(m_mainDB, "PRAGMA cache_size = 200;").executeCommand())
- LOG_ERROR("SQLite database could not set cache_size");
-
- // Open the in-memory table for private browsing
- if (!m_privateBrowsingDB.open(":memory:"))
- LOG_ERROR("Unable to open in-memory database for private browsing - %s", m_privateBrowsingDB.lastErrorMsg());
-
- // Only if we successfully remained open will we start our "initial purge timer"
- // rdar://4690949 - when we have deferred reads and writes all the way in, the prunetimer
- // will become "deferredTimer" or something along those lines, and will be set only when
- // a deferred read/write is queued
- if (isOpen())
- m_startupTimer.startOneShot(0);
+ m_completeDatabasePath = dbFilename.copy();
- return isOpen();
-}
+ // Lock here as well as first thing in the thread so the tread doesn't actually commence until the pthread_create() call
+ // completes and m_syncThreadRunning is properly set
+ m_syncLock.lock();
+ m_syncThreadRunning = !pthread_create(&m_syncThread, NULL, IconDatabase::iconDatabaseSyncThreadStart, this);
+ m_syncLock.unlock();
-bool IconDatabase::isOpen() const
-{
- return m_mainDB.isOpen() && m_privateBrowsingDB.isOpen();
+ return m_syncThreadRunning;
}
void IconDatabase::close()
{
- // This will close all the SQL statements and transactions we have open,
- // syncing the DB at the appropriate point
- deleteAllPreparedStatements(true);
-
- m_mainDB.close();
- m_privateBrowsingDB.close();
-}
-
-String IconDatabase::databasePath() const
-{
- return m_mainDB.isOpen() ? m_mainDB.path() : String();
-}
-
-void IconDatabase::removeAllIcons()
-{
- if (!isOpen())
- return;
-
- // We don't need to sync anything anymore since we're wiping everything.
- // So we can kill the update timer, and clear all the hashes of "items that need syncing"
- m_updateTimer.stop();
- m_iconDataCachesPendingUpdate.clear();
- m_pageURLsPendingAddition.clear();
- m_pageURLsPendingDeletion.clear();
- m_iconURLsPendingDeletion.clear();
-
- // Now clear all in-memory URLs and Icons
- m_pageURLToIconURLMap.clear();
- m_pageURLToRetainCount.clear();
- m_iconURLToRetainCount.clear();
+ ASSERT_NOT_SYNC_THREAD();
- deleteAllValues(m_iconURLToIconDataCacheMap);
- m_iconURLToIconDataCacheMap.clear();
-
- // Wipe any pre-prepared statements, otherwise resetting the SQLDatabases themselves will fail
- deleteAllPreparedStatements(false);
-
- // The easiest way to wipe the in-memory database is by closing and reopening it
- m_privateBrowsingDB.close();
- if (!m_privateBrowsingDB.open(":memory:"))
- LOG_ERROR("Unable to open in-memory database for private browsing - %s", m_privateBrowsingDB.lastErrorMsg());
- createDatabaseTables(m_privateBrowsingDB);
+ if (m_syncThreadRunning) {
+ // Set the flag to tell the sync thread to wrap it up
+ m_threadTerminationRequested = true;
+
+ // Wake up the sync thread if it's waiting
+ wakeSyncThread();
- // To reset the on-disk database, we'll wipe all its tables then vacuum it
- // This is easier and safer than closing it, deleting the file, and recreating from scratch
- m_mainDB.clearAllTables();
- m_mainDB.runVacuumCommand();
- createDatabaseTables(m_mainDB);
-}
+ // Wait for the sync thread to terminate
+ if (pthread_join(m_syncThread, NULL) == EDEADLK)
+ LOG_ERROR("m_syncThread was found to be deadlocked trying to quit");
+ }
-// There are two instances where you'd want to deleteAllPreparedStatements - one with sync, and one without
-// A - Closing down the database on application exit - in this case, you *do* want to save the icons out
-// B - Resetting the DB via removeAllIcons() - in this case, you *don't* want to sync, because it would be a waste of time
-void IconDatabase::deleteAllPreparedStatements(bool withSync)
-{
- // Sync, if desired
- if (withSync)
- syncDatabase();
-
- // Order doesn't matter on these
- delete m_timeStampForIconURLStatement;
- m_timeStampForIconURLStatement = 0;
- delete m_iconURLForPageURLStatement;
- m_iconURLForPageURLStatement = 0;
- delete m_hasIconForIconURLStatement;
- m_hasIconForIconURLStatement = 0;
- delete m_forgetPageURLStatement;
- m_forgetPageURLStatement = 0;
- delete m_setIconIDForPageURLStatement;
- m_setIconIDForPageURLStatement = 0;
- delete m_getIconIDForIconURLStatement;
- m_getIconIDForIconURLStatement = 0;
- delete m_addIconForIconURLStatement;
- m_addIconForIconURLStatement = 0;
- delete m_imageDataForIconURLStatement;
- m_imageDataForIconURLStatement = 0;
- delete m_importedStatement;
- m_importedStatement = 0;
- delete m_setImportedStatement;
- m_setImportedStatement = 0;
-}
-
-bool IconDatabase::isEmpty()
-{
- if (m_privateBrowsingEnabled)
- if (!pageURLTableIsEmptyQuery(m_privateBrowsingDB))
- return false;
-
- return pageURLTableIsEmptyQuery(m_mainDB);
+ m_syncThreadRunning = false;
+ m_threadTerminationRequested = false;
+ m_removeIconsRequested = false;
}
-bool IconDatabase::isValidDatabase(SQLDatabase& db)
+void IconDatabase::removeAllIcons()
{
- // These two tables should always exist in a valid db
- if (!db.tableExists("Icon") || !db.tableExists("PageURL") || !db.tableExists("IconDatabaseInfo"))
- return false;
-
- if (SQLStatement(db, "SELECT value FROM IconDatabaseInfo WHERE key = 'Version';").getColumnInt(0) < currentDatabaseVersion) {
- LOG(IconDatabase, "DB version is not found or below expected valid version");
- return false;
- }
+ ASSERT_NOT_SYNC_THREAD();
- return true;
-}
-
-void IconDatabase::createDatabaseTables(SQLDatabase& db)
-{
- if (!db.executeCommand("CREATE TABLE PageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) {
- LOG_ERROR("Could not create PageURL table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
- db.close();
- return;
- }
- if (!db.executeCommand("CREATE TABLE Icon (iconID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE ON CONFLICT REPLACE, url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT FAIL, stamp INTEGER, data BLOB);")) {
- LOG_ERROR("Could not create Icon table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
- db.close();
- return;
- }
- if (!db.executeCommand("CREATE TABLE IconDatabaseInfo (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) {
- LOG_ERROR("Could not create IconDatabaseInfo table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
- db.close();
- return;
- }
- if (!db.executeCommand(String("INSERT INTO IconDatabaseInfo VALUES ('Version', ") + String::number(currentDatabaseVersion) + ");")) {
- LOG_ERROR("Could not insert icon database version into IconDatabaseInfo table (%i) - %s", db.lastError(), db.lastErrorMsg());
- db.close();
+ if (!isOpen())
return;
- }
-}
-PassRefPtr<SharedBuffer> IconDatabase::imageDataForIconURL(const String& iconURL)
-{
- // If private browsing is enabled, we'll check there first as the most up-to-date data for an icon will be there
- if (m_privateBrowsingEnabled) {
- RefPtr<SharedBuffer> result = imageDataForIconURLQuery(m_privateBrowsingDB, iconURL);
- if (result && !result->isEmpty())
- return result.release();
- }
-
- // It wasn't found there, so lets check the main tables
- return imageDataForIconURLQuery(m_mainDB, iconURL);
+ LOG(IconDatabase, "Requesting background thread to remove all icons");
+
+ m_removeIconsRequested = true;
+ wakeSyncThread();
}
-void IconDatabase::setPrivateBrowsingEnabled(bool flag)
-{
+Image* IconDatabase::iconForPageURL(const String& pageURLOriginal, const IntSize& size, bool cache)
+{
+ ASSERT_NOT_SYNC_THREAD();
+
+ // pageURLOriginal can not be stored without being deep copied first.
+ // We should go our of our way to only copy it if we have to store it
+
if (!isOpen())
- return;
- if (m_privateBrowsingEnabled == flag)
- return;
+ return defaultIcon(size);
+
+ MutexLocker locker(m_urlAndIconLock);
- // Sync any deferred DB changes before we change the active DB
- syncDatabase();
+ String pageURLCopy; // Creates a null string for easy testing
- m_privateBrowsingEnabled = flag;
+ PageURLRecord* pageRecord = m_pageURLToRecordMap.get(pageURLOriginal);
+ if (!pageRecord) {
+ pageURLCopy = pageURLOriginal.copy();
+ pageRecord = getOrCreatePageURLRecord(pageURLCopy);
+ }
- if (m_privateBrowsingEnabled) {
- createDatabaseTables(m_privateBrowsingDB);
- m_currentDB = &m_privateBrowsingDB;
- } else {
- m_privateBrowsingDB.clearAllTables();
- m_currentDB = &m_mainDB;
+ // If pageRecord is NULL, one of two things is true -
+ // 1 - The initial url import is incomplete and this pageURL was marked to be notified once it is complete if an iconURL exists
+ // 2 - The initial url import IS complete and this pageURL has no icon
+ if (!pageRecord) {
+ MutexLocker locker(m_pendingReadingLock);
+
+ // Import is ongoing, there might be an icon. In this case, register to be notified when the icon comes in
+ // If we ever reach this condition, we know we've already made the pageURL copy
+ if (!m_iconURLImportComplete)
+ m_pageURLsInterestedInIcons.add(pageURLCopy);
+
+ return 0;
}
-}
-bool IconDatabase::isPrivateBrowsingEnabled() const
-{
- return m_privateBrowsingEnabled;
-}
-
-Image* IconDatabase::iconForPageURL(const String& pageURL, const IntSize& size, bool cache)
-{
- if (!isOpen())
- return defaultIcon(size);
-
- // See if we even have an IconURL for this PageURL...
- String iconURL = iconURLForPageURL(pageURL);
- if (iconURL.isEmpty())
+ IconRecord* iconRecord = pageRecord->iconRecord();
+
+ // If the initial URL import isn't complete, it's possible to have a PageURL record without an associated icon
+ // In this case, the pageURL is already in the set to alert the client when the iconURL mapping is complete so
+ // we can just bail now
+ if (!m_iconURLImportComplete && !iconRecord)
return 0;
- // If we do, maybe we have a IconDataCache for this IconURL
- IconDataCache* icon = getOrCreateIconDataCache(iconURL);
+ // The only way we should *not* have an icon record is if this pageURL is retained but has no icon yet - make sure of that
+ ASSERT(iconRecord || m_retainedPageURLs.contains(pageURLOriginal));
- // If it's a new IconDataCache object that doesn't have its imageData set yet,
- // we'll read in that image data now
- if (icon->imageDataStatus() == ImageDataStatusUnknown) {
- RefPtr<SharedBuffer> data = imageDataForIconURL(iconURL);
- icon->setImageData(data.get());
+ if (!iconRecord)
+ return 0;
+
+ // If it's a new IconRecord object that doesn't have its imageData set yet,
+ // mark it to be read by the background thread
+ if (iconRecord->imageDataStatus() == ImageDataStatusUnknown) {
+ if (pageURLCopy.isNull())
+ pageURLCopy = pageURLOriginal.copy();
+
+ MutexLocker locker(m_pendingReadingLock);
+ m_pageURLsInterestedInIcons.add(pageURLCopy);
+ m_iconsPendingReading.add(iconRecord);
+ wakeSyncThread();
+ return 0;
}
+
+ // If the size parameter was (0, 0) that means the caller of this method just wanted the read from disk to be kicked off
+ // and isn't actually interested in the image return value
+ if (size == IntSize(0, 0))
+ return 0;
- return icon->getImage(size);
+ // PARANOID DISCUSSION: This method makes some assumptions. It returns a WebCore::image which the icon database might dispose of at anytime in the future,
+ // and Images aren't ref counted. So there is no way for the client to guarantee continued existence of the image.
+ // This has *always* been the case, but in practice clients would always create some other platform specific representation of the image
+ // and drop the raw Image*. On Mac an NSImage, and on windows drawing into an HBITMAP.
+ // The async aspect adds a huge question - what if the image is deleted before the platform specific API has a chance to create its own
+ // representation out of it?
+ // If an image is read in from the icondatabase, we do *not* overwrite any image data that exists in the in-memory cache.
+ // This is because we make the assumption that anything in memory is newer than whatever is in the database.
+ // So the only time the data will be set from the second thread is when it is INITIALLY being read in from the database, but we would never
+ // delete the image on the secondary thread if the image already exists.
+ return iconRecord->image(size);
}
-// FIXME 4667425 - this check needs to see if the icon's data is empty or not and apply
-// iconExpirationTime to present icons, and missingIconExpirationTime for missing icons
-bool IconDatabase::isIconExpiredForIconURL(const String& iconURL)
+void IconDatabase::readIconForPageURLFromDisk(const String& pageURL)
{
- // If we're closed and someone is making this call, it is likely a return value of
- // false will discourage them to take any further action, which is our goal in this case
- // Same notion for an empty iconURL - which is now defined as "never expires"
- if (!isOpen() || iconURL.isEmpty())
- return false;
-
- // If we have a IconDataCache, then it definitely has the Timestamp in it
- IconDataCache* icon = m_iconURLToIconDataCacheMap.get(iconURL);
- if (icon)
- return (int)currentTime() - icon->getTimestamp() > iconExpirationTime;
-
- // Otherwise, we'll get the timestamp from the DB and use it
- int stamp;
- if (m_privateBrowsingEnabled) {
- stamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
- if (stamp)
- return ((int)currentTime() - stamp) > iconExpirationTime;
- }
-
- stamp = timeStampForIconURLQuery(m_mainDB, iconURL);
- if (stamp)
- return ((int)currentTime() - stamp) > iconExpirationTime;
-
- return false;
+ // The effect of asking for an Icon for a pageURL automatically queues it to be read from disk
+ // if it hasn't already been set in memory. The special IntSize (0, 0) is a special way of telling
+ // that method "I don't care about the actual Image, i just want you to make sure you're getting it from disk.
+ iconForPageURL(pageURL, IntSize(0,0));
}
-
-String IconDatabase::iconURLForPageURL(const String& pageURL)
+
+String IconDatabase::iconURLForPageURL(const String& pageURLOriginal)
{
- if (!isOpen() || pageURL.isEmpty())
+ ASSERT_NOT_SYNC_THREAD();
+
+ // Cannot do anything with pageURLOriginal that would end up storing it without deep copying first
+ // Also, in the case we have a real answer for the caller, we must deep copy that as well
+
+ if (!isOpen() || pageURLOriginal.isEmpty())
return String();
- if (m_pageURLToIconURLMap.contains(pageURL))
- return m_pageURLToIconURLMap.get(pageURL);
+ MutexLocker locker(m_urlAndIconLock);
- // Try the private browsing database because if any PageURL's IconURL was updated during privated browsing,
- // the most up-to-date iconURL would be there
- if (m_privateBrowsingEnabled) {
- String iconURL = iconURLForPageURLQuery(m_privateBrowsingDB, pageURL);
- if (!iconURL.isEmpty()) {
- m_pageURLToIconURLMap.set(pageURL, iconURL);
- return iconURL;
- }
- }
+ PageURLRecord* pageRecord = m_pageURLToRecordMap.get(pageURLOriginal);
+ if (!pageRecord)
+ pageRecord = getOrCreatePageURLRecord(pageURLOriginal.copy());
+
+ // If pageRecord is NULL, one of two things is true -
+ // 1 - The initial url import is incomplete and this pageURL has already been marked to be notified once it is complete if an iconURL exists
+ // 2 - The initial url import IS complete and this pageURL has no icon
+ if (!pageRecord)
+ return String();
- String iconURL = iconURLForPageURLQuery(m_mainDB, pageURL);
- if (!iconURL.isEmpty())
- m_pageURLToIconURLMap.set(pageURL, iconURL);
- return iconURL;
+ // Possible the pageRecord is around because it's a retained pageURL with no iconURL, so we have to check
+ return pageRecord->iconRecord() ? pageRecord->iconRecord()->iconURL().copy() : String();
}
Image* IconDatabase::defaultIcon(const IntSize& size)
{
- if (!m_defaultIconDataCache) {
- m_defaultIconDataCache = new IconDataCache("urlIcon");
- m_defaultIconDataCache->loadImageFromResource("urlIcon");
+ ASSERT_NOT_SYNC_THREAD();
+
+ if (!m_defaultIconRecord) {
+ m_defaultIconRecord = new IconRecord("urlIcon");
+ m_defaultIconRecord->loadImageFromResource("urlIcon");
}
- return m_defaultIconDataCache->getImage(size);
+ return m_defaultIconRecord->image(size);
}
-void IconDatabase::retainIconForPageURL(const String& pageURL)
-{
- if (!isOpen() || pageURL.isEmpty())
+
+void IconDatabase::retainIconForPageURL(const String& pageURLOriginal)
+{
+ ASSERT_NOT_SYNC_THREAD();
+
+ // Cannot do anything with pageURLOriginal that would end up storing it without deep copying first
+
+ if (!isOpen() || pageURLOriginal.isEmpty())
return;
+
+ MutexLocker locker(m_urlAndIconLock);
+
+ PageURLRecord* record = m_pageURLToRecordMap.get(pageURLOriginal);
+
+ String pageURL;
- // If we don't have the retain count for this page, we need to setup records of its retain
- // Otherwise, get the count and increment it
- int retainCount;
- if (!(retainCount = m_pageURLToRetainCount.get(pageURL))) {
- m_pageURLToRetainCount.set(pageURL, 1);
+ if (!record) {
+ pageURL = pageURLOriginal.copy();
- // If we haven't done pruning yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot,
+ record = new PageURLRecord(pageURL);
+ m_pageURLToRecordMap.set(pageURL, record);
+ m_retainedPageURLs.add(pageURL);
+ }
+
+ if (record->retain()) {
+ // If we read the iconURLs yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot,
// so we bail here and skip those steps
- if (!m_initialPruningComplete)
+ if (!m_iconURLImportComplete)
return;
-
- // If this pageURL is marked for deletion, bring it back from the brink
- m_pageURLsPendingDeletion.remove(pageURL);
-
- // If we have an iconURL for this pageURL, we'll now retain the iconURL
- String iconURL = iconURLForPageURL(pageURL);
- if (!iconURL.isEmpty())
- retainIconURL(iconURL);
- } else
- m_pageURLToRetainCount.set(pageURL, retainCount + 1);
+ MutexLocker locker(m_pendingSyncLock);
+ // If this pageURL waiting to be sync'ed, update the sync record
+ // This saves us in the case where a page was ready to be deleted from the database but was just retained - so theres no need to delete it!
+ if (!m_privateBrowsingEnabled && m_pageURLsPendingSync.contains(pageURLOriginal)) {
+ if (pageURL.isNull())
+ pageURL = pageURLOriginal.copy();
+
+ LOG(IconDatabase, "Bringing %s back from the brink", pageURL.utf8().data());
+ m_pageURLsPendingSync.set(pageURL, record->snapshot());
+ }
+ }
}
-void IconDatabase::releaseIconForPageURL(const String& pageURL)
+void IconDatabase::releaseIconForPageURL(const String& pageURLOriginal)
{
- if (!isOpen() || pageURL.isEmpty())
- return;
+ ASSERT_NOT_SYNC_THREAD();
+ // Cannot do anything with pageURLOriginal that would end up storing it without deep copying first
+
+ if (!isOpen() || pageURLOriginal.isEmpty())
+ return;
+
+ MutexLocker locker(m_urlAndIconLock);
+
// Check if this pageURL is actually retained
- if (!m_pageURLToRetainCount.contains(pageURL)) {
- LOG_ERROR("Attempting to release icon for URL %s which is not retained", pageURL.ascii().data());
+ if (!m_retainedPageURLs.contains(pageURLOriginal)) {
+ LOG_ERROR("Attempting to release icon for URL %s which is not retained", urlForLogging(pageURLOriginal).utf8().data());
return;
}
- // Get its retain count
- int retainCount = m_pageURLToRetainCount.get(pageURL);
- ASSERT(retainCount > 0);
-
+ // Get its retain count - if it's retained, we'd better have a PageURLRecord for it
+ PageURLRecord* pageRecord = m_pageURLToRecordMap.get(pageURLOriginal);
+ ASSERT(pageRecord);
+ LOG(IconDatabase, "Releasing pageURL %s to a retain count of %i", urlForLogging(pageURLOriginal).utf8().data(), pageRecord->retainCount() - 1);
+ ASSERT(pageRecord->retainCount() > 0);
+
// If it still has a positive retain count, store the new count and bail
- if (--retainCount) {
- m_pageURLToRetainCount.set(pageURL, retainCount);
+ if (pageRecord->release())
return;
- }
-
- LOG(IconDatabase, "No more retainers for PageURL %s", pageURL.ascii().data());
+
+ // This pageRecord has now been fully released. Do the appropriate cleanup
+ LOG(IconDatabase, "No more retainers for PageURL %s", urlForLogging(pageURLOriginal).utf8().data());
+ m_pageURLToRecordMap.remove(pageURLOriginal);
+ m_retainedPageURLs.remove(pageURLOriginal);
- // Otherwise, remove all record of the retain count
- m_pageURLToRetainCount.remove(pageURL);
+ // Grab the iconRecord for later use (and do a sanity check on it for kicks)
+ IconRecord* iconRecord = pageRecord->iconRecord();
- // If we haven't done pruning yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot,
- // so we bail here and skip those steps
- if (!m_initialPruningComplete)
- return;
+ ASSERT(!iconRecord || (iconRecord && m_iconURLToRecordMap.get(iconRecord->iconURL()) == iconRecord));
+ {
+ MutexLocker locker(m_pendingReadingLock);
+
+ // Since this pageURL is going away, there's no reason anyone would ever be interested in its read results
+ if (!m_iconURLImportComplete)
+ m_pageURLsPendingImport.remove(pageURLOriginal);
+ m_pageURLsInterestedInIcons.remove(pageURLOriginal);
+
+ // If this icon is down to it's last retainer, we don't care about reading it in from disk anymore
+ if (iconRecord && iconRecord->hasOneRef()) {
+ m_iconURLToRecordMap.remove(iconRecord->iconURL());
+ m_iconsPendingReading.remove(iconRecord);
+ }
+ }
- // Then mark this pageURL for deletion
- m_pageURLsPendingDeletion.add(pageURL);
+ // Mark stuff for deletion from the database only if we're not in private browsing
+ if (!m_privateBrowsingEnabled) {
+ MutexLocker locker(m_pendingSyncLock);
+ m_pageURLsPendingSync.set(pageURLOriginal.copy(), pageRecord->snapshot(true));
- // Grab the iconURL and release it
- String iconURL = iconURLForPageURL(pageURL);
- if (!iconURL.isEmpty())
- releaseIconURL(iconURL);
-}
-
-void IconDatabase::retainIconURL(const String& iconURL)
-{
- ASSERT(!iconURL.isEmpty());
+ // If this page is the last page to refer to a particular IconRecord, that IconRecord needs to
+ // be marked for deletion
+ if (iconRecord && iconRecord->hasOneRef())
+ m_iconsPendingSync.set(iconRecord->iconURL(), iconRecord->snapshot(true));
+ }
- if (int retainCount = m_iconURLToRetainCount.get(iconURL)) {
- ASSERT(retainCount > 0);
- m_iconURLToRetainCount.set(iconURL, retainCount + 1);
- } else {
- m_iconURLToRetainCount.set(iconURL, 1);
- if (m_iconURLsPendingDeletion.contains(iconURL))
- m_iconURLsPendingDeletion.remove(iconURL);
- }
+ delete pageRecord;
+
+ scheduleOrDeferSyncTimer();
}
-void IconDatabase::releaseIconURL(const String& iconURL)
-{
- ASSERT(!iconURL.isEmpty());
-
- // If the iconURL has no retain count, we can bail
- if (!m_iconURLToRetainCount.contains(iconURL))
- return;
+void IconDatabase::setIconDataForIconURL(PassRefPtr<SharedBuffer> dataOriginal, const String& iconURLOriginal)
+{
+ ASSERT_NOT_SYNC_THREAD();
- // Otherwise, decrement it
- int retainCount = m_iconURLToRetainCount.get(iconURL) - 1;
- ASSERT(retainCount > -1);
+ // Cannot do anything with dataOriginal or iconURLOriginal that would end up storing them without deep copying first
- // If the icon is still retained, store the count and bail
- if (retainCount) {
- m_iconURLToRetainCount.set(iconURL, retainCount);
+ if (!isOpen() || iconURLOriginal.isEmpty())
return;
- }
- LOG(IconDatabase, "No more retainers for IconURL %s", iconURL.ascii().data());
+ RefPtr<SharedBuffer> data = dataOriginal ? dataOriginal->copy() : 0;
+ String iconURL = iconURLOriginal.copy();
- // Otherwise, this icon is toast. Remove all traces of its retain count...
- m_iconURLToRetainCount.remove(iconURL);
-
- // And since we delay the actual deletion of icons, so lets add it to that queue
- m_iconURLsPendingDeletion.add(iconURL);
-}
-
-void IconDatabase::forgetPageURL(const String& pageURL)
-{
- // Remove the PageURL->IconURL mapping
- m_pageURLToIconURLMap.remove(pageURL);
+ Vector<String> pageURLs;
+ {
+ MutexLocker locker(m_urlAndIconLock);
- // And remove this pageURL from the DB
- forgetPageURLQuery(*m_currentDB, pageURL);
-}
+ // If this icon was pending a read, remove it from that set because this new data should override what is on disk
+ IconRecord* icon = m_iconURLToRecordMap.get(iconURL);
+ if (icon) {
+ MutexLocker locker(m_pendingReadingLock);
+ m_iconsPendingReading.remove(icon);
+ } else
+ icon = getOrCreateIconRecord(iconURL);
-bool IconDatabase::isIconURLRetained(const String& iconURL)
-{
- if (iconURL.isEmpty())
- return false;
+ // Update the data and set the time stamp
+ icon->setImageData(data);
+ icon->setTimestamp((int)currentTime());
+
+ // Copy the current retaining pageURLs - if any - to notify them of the change
+ pageURLs.appendRange(icon->retainingPageURLs().begin(), icon->retainingPageURLs().end());
- return m_iconURLToRetainCount.contains(iconURL);
+ // Mark the IconRecord as requiring an update to the database only if private browsing is disabled
+ if (!m_privateBrowsingEnabled) {
+ MutexLocker locker(m_pendingSyncLock);
+ m_iconsPendingSync.set(iconURL, icon->snapshot());
+ }
+ }
+
+ // Send notification out regarding all PageURLs that retain this icon
+ // But not if we're on the sync thread because that implies this mapping
+ // comes from the initial import which we don't want notifications for
+ if (!IS_ICON_SYNC_THREAD()) {
+ // Start the timer to commit this change - or further delay the timer if it was already started
+ scheduleOrDeferSyncTimer();
+
+ // Informal testing shows that draining the autorelease pool every 25 iterations is about as low as we can go
+ // before performance starts to drop off, but we don't want to increase this number because then accumulated memory usage will go up
+ AutodrainedPool pool(25);
+
+ for (unsigned i = 0; i < pageURLs.size(); ++i) {
+ LOG(IconDatabase, "Dispatching notification that retaining pageURL %s has a new icon", urlForLogging(pageURLs[i]).utf8().data());
+ m_client->dispatchDidAddIconForPageURL(pageURLs[i]);
+
+ pool.cycle();
+ }
+ }
}
-void IconDatabase::forgetIconForIconURLFromDatabase(const String& iconURL)
-{
- if (iconURL.isEmpty())
- return;
+void IconDatabase::setIconURLForPageURL(const String& iconURLOriginal, const String& pageURLOriginal)
+{
+ ASSERT_NOT_SYNC_THREAD();
- // For private browsing safety, since this alters the database, we only forget from the current database
- // If we're in private browsing and the Icon also exists in the main database, it will be pruned on the next startup
- int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL, false);
+ // Cannot do anything with iconURLOriginal or pageURLOriginal that would end up storing them without deep copying first
- // If we didn't actually have an icon for this iconURL... well, thats a screwy condition we should track down, but also
- // something we could move on from
- ASSERT(iconID);
- if (!iconID) {
- LOG_ERROR("Attempting to forget icon for IconURL %s, though we don't have it in the database", iconURL.ascii().data());
+ ASSERT(!iconURLOriginal.isEmpty());
+
+ if (!isOpen() || pageURLOriginal.isEmpty())
return;
- }
- if (!m_currentDB->executeCommand(String::format("DELETE FROM Icon WHERE Icon.iconID = %lli;", iconID)))
- LOG_ERROR("Unable to drop Icon for IconURL", iconURL.ascii().data());
- if (!m_currentDB->executeCommand(String::format("DELETE FROM PageURL WHERE PageURL.iconID = %lli", iconID)))
- LOG_ERROR("Unable to drop all PageURL for IconURL", iconURL.ascii().data());
-}
+ String iconURL, pageURL;
+
+ {
+ MutexLocker locker(m_urlAndIconLock);
-IconDataCache* IconDatabase::getOrCreateIconDataCache(const String& iconURL)
-{
- IconDataCache* icon;
- if ((icon = m_iconURLToIconDataCacheMap.get(iconURL)))
- return icon;
+ PageURLRecord* pageRecord = m_pageURLToRecordMap.get(pageURLOriginal);
- icon = new IconDataCache(iconURL);
- m_iconURLToIconDataCacheMap.set(iconURL, icon);
-
- // Get the most current time stamp for this IconURL
- int timestamp = 0;
- if (m_privateBrowsingEnabled)
- timestamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
- if (!timestamp)
- timestamp = timeStampForIconURLQuery(m_mainDB, iconURL);
+ // If the urls already map to each other, bail.
+ // This happens surprisingly often, and seems to cream iBench performance
+ if (pageRecord && pageRecord->iconRecord() && pageRecord->iconRecord()->iconURL() == iconURLOriginal)
+ return;
+
+ pageURL = pageURLOriginal.copy();
+ iconURL = iconURLOriginal.copy();
+
+ if (!pageRecord) {
+ pageRecord = new PageURLRecord(pageURL);
+ m_pageURLToRecordMap.set(pageURL, pageRecord);
+ }
+
+ RefPtr<IconRecord> iconRecord = pageRecord->iconRecord();
+
+ // Otherwise, set the new icon record for this page
+ IconRecord* newIconRecord = getOrCreateIconRecord(iconURL);
+ pageRecord->setIconRecord(newIconRecord);
+
+ // If the current icon has only a single ref left, it is about to get wiped out.
+ // Remove it from the in-memory records and don't bother reading it in from disk anymore
+ if (iconRecord && iconRecord->hasOneRef()) {
+ ASSERT(iconRecord->retainingPageURLs().size() == 0);
+ LOG(IconDatabase, "Icon for icon url %s is about to be destroyed - removing mapping for it", urlForLogging(iconRecord->iconURL()).utf8().data());
+ m_iconURLToRecordMap.remove(iconRecord->iconURL());
+ MutexLocker locker(m_pendingReadingLock);
+ m_iconsPendingReading.remove(iconRecord.get());
+ }
- // If we can't get a timestamp for this URL, then it is a new icon and we initialize its timestamp now
- if (!timestamp) {
- icon->setTimestamp((int)currentTime());
- m_iconDataCachesPendingUpdate.add(icon);
- } else
- icon->setTimestamp(timestamp);
+ // And mark this mapping to be added to the database
+ if (!m_privateBrowsingEnabled) {
+ MutexLocker locker(m_pendingSyncLock);
+ m_pageURLsPendingSync.set(pageURL, pageRecord->snapshot());
+
+ // If the icon is on it's last ref, mark it for deletion
+ if (iconRecord && iconRecord->hasOneRef())
+ m_iconsPendingSync.set(iconRecord->iconURL(), iconRecord->snapshot(true));
+ }
+ }
+
+ // Since this mapping is new, send the notification out - but not if we're on the sync thread because that implies this mapping
+ // comes from the initial import which we don't want notifications for
+ if (!IS_ICON_SYNC_THREAD()) {
+ // Start the timer to commit this change - or further delay the timer if it was already started
+ scheduleOrDeferSyncTimer();
- return icon;
+ LOG(IconDatabase, "Dispatching notification that we changed an icon mapping for url %s", urlForLogging(pageURL).utf8().data());
+ AutodrainedPool pool;
+ m_client->dispatchDidAddIconForPageURL(pageURL);
+ }
}
-void IconDatabase::setIconDataForIconURL(PassRefPtr<SharedBuffer> data, const String& iconURL)
+IconLoadDecision IconDatabase::loadDecisionForIconURL(const String& iconURL, DocumentLoader* notificationDocumentLoader)
{
- if (!isOpen() || iconURL.isEmpty())
- return;
+ ASSERT_NOT_SYNC_THREAD();
- // Get the IconDataCache for this IconURL (note, IconDataCacheForIconURL will create it if necessary)
- IconDataCache* icon = getOrCreateIconDataCache(iconURL);
-
- // Set the data in the IconDataCache
- icon->setImageData(data);
+ if (!isOpen() || iconURL.isEmpty())
+ return IconLoadNo;
+
+ // If we have a IconRecord, it should also have its timeStamp marked because there is only two times when we create the IconRecord:
+ // 1 - When we read the icon urls from disk, getting the timeStamp at the same time
+ // 2 - When we get a new icon from the loader, in which case the timestamp is set at that time
+ {
+ MutexLocker locker(m_urlAndIconLock);
+ if (IconRecord* icon = m_iconURLToRecordMap.get(iconURL)) {
+ LOG(IconDatabase, "Found expiration time on a present icon based on existing IconRecord");
+ return (int)currentTime() - icon->getTimestamp() > iconExpirationTime ? IconLoadYes : IconLoadNo;
+ }
+ }
- // Update the timestamp in the IconDataCache to NOW
- icon->setTimestamp((int)currentTime());
+ // If we don't have a record for it, but we *have* imported all iconURLs from disk, then we should load it now
+ MutexLocker readingLocker(m_pendingReadingLock);
+ if (m_iconURLImportComplete)
+ return IconLoadYes;
+
+ // Otherwise - since we refuse to perform I/O on the main thread to find out for sure - we return the answer that says
+ // "You might be asked to load this later, so flag that"
+ LOG(IconDatabase, "Don't know if we should load %s or not - adding %p to the set of document loaders waiting on a decision", iconURL.utf8().data(), notificationDocumentLoader);
+ m_loadersPendingDecision.add(notificationDocumentLoader);
- // Mark the IconDataCache as requiring an update to the database
- m_iconDataCachesPendingUpdate.add(icon);
+ return IconLoadUnknown;
}
-void IconDatabase::setHaveNoIconForIconURL(const String& iconURL)
-{
- setIconDataForIconURL(0, iconURL);
+bool IconDatabase::iconDataKnownForIconURL(const String& iconURL)
+{
+ ASSERT_NOT_SYNC_THREAD();
+
+ MutexLocker locker(m_urlAndIconLock);
+ if (IconRecord* icon = m_iconURLToRecordMap.get(iconURL))
+ return icon->imageDataStatus() != ImageDataStatusUnknown;
+
+ return false;
}
-bool IconDatabase::setIconURLForPageURL(const String& iconURL, const String& pageURL)
+void IconDatabase::setEnabled(bool enabled)
{
- ASSERT(!iconURL.isEmpty());
- if (!isOpen() || pageURL.isEmpty())
- return false;
+ ASSERT_NOT_SYNC_THREAD();
- // If the urls already map to each other, bail.
- // This happens surprisingly often, and seems to cream iBench performance
- if (m_pageURLToIconURLMap.get(pageURL) == iconURL)
- return false;
-
- // If this pageURL is retained, we have some work to do on the IconURL retain counts
- if (m_pageURLToRetainCount.contains(pageURL)) {
- String oldIconURL = m_pageURLToIconURLMap.get(pageURL);
- if (!oldIconURL.isEmpty())
- releaseIconURL(oldIconURL);
- retainIconURL(iconURL);
- } else {
- // If this pageURL is *not* retained, then we may be marking it for deletion, as well!
- // As counterintuitive as it seems to mark it for addition and for deletion at the same time,
- // it's valid because when we do a new pageURL->iconURL mapping we *have* to mark it for addition,
- // no matter what, as there is no efficient was to determine if the mapping is in the DB already.
- // But, if the iconURL is marked for deletion, we'll also mark this pageURL for deletion - if a
- // client comes along and retains it before the timer fires, the "pendingDeletion" lists will
- // be manipulated appopriately and new pageURL will be brought back from the brink
- if (m_iconURLsPendingDeletion.contains(iconURL))
- m_pageURLsPendingDeletion.add(pageURL);
+ if (!enabled && isOpen())
+ close();
+ m_isEnabled = enabled;
+}
+
+bool IconDatabase::isEnabled() const
+{
+ ASSERT_NOT_SYNC_THREAD();
+
+ return m_isEnabled;
+}
+
+void IconDatabase::setPrivateBrowsingEnabled(bool flag)
+{
+ m_privateBrowsingEnabled = flag;
+}
+
+bool IconDatabase::isPrivateBrowsingEnabled() const
+{
+ return m_privateBrowsingEnabled;
+}
+
+void IconDatabase::delayDatabaseCleanup()
+{
+ ++databaseCleanupCounter;
+ if (databaseCleanupCounter == 1)
+ LOG(IconDatabase, "Database cleanup is now DISABLED");
+}
+
+void IconDatabase::allowDatabaseCleanup()
+{
+ if (--databaseCleanupCounter < 0)
+ databaseCleanupCounter = 0;
+ if (databaseCleanupCounter == 0)
+ LOG(IconDatabase, "Database cleanup is now ENABLED");
+}
+
+void IconDatabase::checkIntegrityBeforeOpening()
+{
+ checkIntegrityOnOpen = true;
+}
+
+size_t IconDatabase::pageURLMappingCount()
+{
+ MutexLocker locker(m_urlAndIconLock);
+ return m_pageURLToRecordMap.size();
+}
+
+size_t IconDatabase::retainedPageURLCount()
+{
+ MutexLocker locker(m_urlAndIconLock);
+ return m_retainedPageURLs.size();
+}
+
+size_t IconDatabase::iconRecordCount()
+{
+ MutexLocker locker(m_urlAndIconLock);
+ return m_iconURLToRecordMap.size();
+}
+
+size_t IconDatabase::iconRecordCountWithData()
+{
+ MutexLocker locker(m_urlAndIconLock);
+ size_t result = 0;
+
+ HashMap<String, IconRecord*>::iterator i = m_iconURLToRecordMap.begin();
+ HashMap<String, IconRecord*>::iterator end = m_iconURLToRecordMap.end();
+
+ for (; i != end; ++i)
+ result += ((*i).second->imageDataStatus() == ImageDataStatusPresent);
+
+ return result;
+}
+
+IconDatabase::IconDatabase()
+ : m_syncTimer(this, &IconDatabase::syncTimerFired)
+ , m_syncThreadRunning(false)
+ , m_defaultIconRecord(0)
+ , m_isEnabled(false)
+ , m_privateBrowsingEnabled(false)
+ , m_threadTerminationRequested(false)
+ , m_removeIconsRequested(false)
+ , m_iconURLImportComplete(false)
+ , m_initialPruningComplete(false)
+ , m_client(defaultClient())
+ , m_imported(false)
+ , m_isImportedSet(false)
+{
+ ASSERT(pthread_main_np());
+}
+
+IconDatabase::~IconDatabase()
+{
+ ASSERT_NOT_REACHED();
+}
+
+void IconDatabase::notifyPendingLoadDecisions()
+{
+ iconDatabase()->notifyPendingLoadDecisionsInternal();
+}
+
+void IconDatabase::notifyPendingLoadDecisionsInternal()
+{
+ ASSERT_NOT_SYNC_THREAD();
+
+ // This method should only be called upon completion of the initial url import from the database
+ ASSERT(m_iconURLImportComplete);
+ LOG(IconDatabase, "Notifying all DocumentLoaders that were waiting on a load decision for thier icons");
+
+ HashSet<RefPtr<DocumentLoader> >::iterator i = m_loadersPendingDecision.begin();
+ HashSet<RefPtr<DocumentLoader> >::iterator end = m_loadersPendingDecision.end();
+
+ for (; i != end; ++i)
+ if ((*i)->refCount() > 1)
+ (*i)->iconLoadDecisionAvailable();
+
+ m_loadersPendingDecision.clear();
+}
+
+void IconDatabase::wakeSyncThread()
+{
+ MutexLocker locker(m_syncLock);
+ m_syncCondition.signal();
+}
+
+void IconDatabase::scheduleOrDeferSyncTimer()
+{
+ ASSERT_NOT_SYNC_THREAD();
+
+ m_syncTimer.startOneShot(updateTimerDelay);
+}
+
+void IconDatabase::syncTimerFired(Timer<IconDatabase>*)
+{
+ ASSERT_NOT_SYNC_THREAD();
+ wakeSyncThread();
+}
+
+// ******************
+// *** Any Thread ***
+// ******************
+
+bool IconDatabase::isOpen() const
+{
+ MutexLocker locker(m_syncLock);
+ return m_syncDB.isOpen();
+}
+
+String IconDatabase::databasePath() const
+{
+ MutexLocker locker(m_syncLock);
+ return m_completeDatabasePath.copy();
+}
+
+String IconDatabase::defaultDatabaseFilename()
+{
+ static String defaultDatabaseFilename = "Icons.db";
+ return defaultDatabaseFilename.copy();
+}
+
+// Unlike getOrCreatePageURLRecord(), getOrCreateIconRecord() does not mark the icon as "interested in import"
+IconRecord* IconDatabase::getOrCreateIconRecord(const String& iconURL)
+{
+ // Clients of getOrCreateIconRecord() are required to acquire the m_urlAndIconLock before calling this method
+ ASSERT(m_urlAndIconLock.tryLock() == EBUSY);
+
+ if (IconRecord* icon = m_iconURLToRecordMap.get(iconURL))
+ return icon;
+
+ IconRecord* newIcon = new IconRecord(iconURL);
+ m_iconURLToRecordMap.set(iconURL, newIcon);
+
+ return newIcon;
+}
+
+// This method retrieves the existing PageURLRecord, or creates a new one and marks it as "interested in the import" for later notification
+PageURLRecord* IconDatabase::getOrCreatePageURLRecord(const String& pageURL)
+{
+ // Clients of getOrCreatePageURLRecord() are required to acquire the m_urlAndIconLock before calling this method
+ ASSERT(m_urlAndIconLock.tryLock() == EBUSY);
+
+ PageURLRecord* pageRecord = m_pageURLToRecordMap.get(pageURL);
+
+ MutexLocker locker(m_pendingReadingLock);
+ if (!m_iconURLImportComplete) {
+ // If the initial import of all URLs hasn't completed and we have no page record, we assume we *might* know about this later and create a record for it
+ if (!pageRecord) {
+ LOG(IconDatabase, "Creating new PageURLRecord for pageURL %s", urlForLogging(pageURL).utf8().data());
+ pageRecord = new PageURLRecord(pageURL);
+ m_pageURLToRecordMap.set(pageURL, pageRecord);
+ }
+
+ // If the pageRecord for this page does not have an iconRecord attached to it, then it is a new pageRecord still awaiting the initial import
+ // Mark the URL as "interested in the result of the import" then bail
+ if (!pageRecord->iconRecord()) {
+ m_pageURLsPendingImport.add(pageURL);
+ return 0;
+ }
}
+
+ // We've done the initial import of all URLs known in the database. If this record doesn't exist now, it never will
+ return pageRecord;
+}
+
+
+// ************************
+// *** Sync Thread Only ***
+// ************************
+
+void IconDatabase::importIconURLForPageURL(const String& iconURL, const String& pageURL)
+{
+ ASSERT_ICON_SYNC_THREAD();
+
+ // This function is only for setting actual existing url mappings so assert that neither of these URLs are empty
+ ASSERT(!iconURL.isEmpty() && !pageURL.isEmpty());
+
+ setIconURLForPageURLInSQLDatabase(iconURL, pageURL);
+}
+
+void IconDatabase::importIconDataForIconURL(PassRefPtr<SharedBuffer> data, const String& iconURL)
+{
+ ASSERT_ICON_SYNC_THREAD();
- // Cache the pageURL->iconURL map
- m_pageURLToIconURLMap.set(pageURL, iconURL);
+ ASSERT(!iconURL.isEmpty());
+
+ writeIconSnapshotToSQLDatabase(IconSnapshot(iconURL, (int)currentTime(), data.get()));
+}
+
+bool IconDatabase::shouldStopThreadActivity() const
+{
+ ASSERT_ICON_SYNC_THREAD();
- // And mark this mapping to be added to the database
- m_pageURLsPendingAddition.add(pageURL);
+ return m_threadTerminationRequested || m_removeIconsRequested;
+}
+
+void* IconDatabase::iconDatabaseSyncThreadStart(void* vIconDatabase)
+{
+ IconDatabase* iconDB = static_cast<IconDatabase*>(vIconDatabase);
- // Then start the timer to commit this change - or further delay the timer if it
- // was already started
- m_updateTimer.startOneShot(updateTimerDelay);
+ return iconDB->iconDatabaseSyncThread();
+}
+
+void* IconDatabase::iconDatabaseSyncThread()
+{
+ // The call to create this thread might not complete before the thread actually starts, so we might fail this ASSERT_ICON_SYNC_THREAD() because the pointer
+ // to our thread structure hasn't been filled in yet.
+ // To fix this, the main thread acquires this lock before creating us, then releases the lock after creation is complete. A quick lock/unlock cycle here will
+ // prevent us from running before that call completes
+ m_syncLock.lock();
+ m_syncLock.unlock();
+
+ ASSERT_ICON_SYNC_THREAD();
+
+ LOG(IconDatabase, "(THREAD) IconDatabase sync thread started");
+
+#ifndef NDEBUG
+ double startTime = currentTime();
+#endif
+
+ // Existence of a journal file is evidence of a previous crash/force quit and automatically qualifies
+ // us to do an integrity check
+ String journalFilename = m_completeDatabasePath + "-journal";
+ if (!checkIntegrityOnOpen)
+ checkIntegrityOnOpen = fileExists(journalFilename);
+
+ {
+ MutexLocker locker(m_syncLock);
+ if (!m_syncDB.open(m_completeDatabasePath)) {
+ LOG_ERROR("Unable to open icon database at path %s - %s", m_completeDatabasePath.utf8().data(), m_syncDB.lastErrorMsg());
+ return 0;
+ }
+ }
+
+ if (shouldStopThreadActivity())
+ return syncThreadMainLoop();
+
+#ifndef NDEBUG
+ double timeStamp = currentTime();
+ LOG(IconDatabase, "(THREAD) Open took %.4f seconds", timeStamp - startTime);
+#endif
+
+ performOpenInitialization();
+ if (shouldStopThreadActivity())
+ return syncThreadMainLoop();
+
+#ifndef NDEBUG
+ double newStamp = currentTime();
+ LOG(IconDatabase, "(THREAD) performOpenInitialization() took %.4f seconds, now %.4f seconds from thread start", newStamp - timeStamp, newStamp - startTime);
+ timeStamp = newStamp;
+#endif
+
+ if (!imported()) {
+ LOG(IconDatabase, "(THREAD) Performing Safari2 import procedure");
+ SQLTransaction importTransaction(m_syncDB);
+ importTransaction.begin();
+
+ // Commit the transaction only if the import completes (the import should be atomic)
+ if (m_client->performImport()) {
+ setImported(true);
+ importTransaction.commit();
+ } else {
+ LOG(IconDatabase, "(THREAD) Safari 2 import was cancelled");
+ importTransaction.rollback();
+ }
+
+ if (shouldStopThreadActivity())
+ return syncThreadMainLoop();
+
+#ifndef NDEBUG
+ newStamp = currentTime();
+ LOG(IconDatabase, "(THREAD) performImport() took %.4f seconds, now %.4f seconds from thread start", newStamp - timeStamp, newStamp - startTime);
+ timeStamp = newStamp;
+#endif
+ }
+
+ // Uncomment the following line to simulate a long lasting URL import (*HUGE* icon databases, or network home directories)
+ // while (currentTime() - timeStamp < 10);
+
+ // Read in URL mappings from the database
+ LOG(IconDatabase, "(THREAD) Starting iconURL import");
+ performURLImport();
+
+ if (shouldStopThreadActivity())
+ return syncThreadMainLoop();
+
+#ifndef NDEBUG
+ newStamp = currentTime();
+ LOG(IconDatabase, "(THREAD) performURLImport() took %.4f seconds. Entering main loop %.4f seconds from thread start", newStamp - timeStamp, newStamp - startTime);
+#endif
+
+ LOG(IconDatabase, "(THREAD) Beginning sync");
+ return syncThreadMainLoop();
+}
+
+static int databaseVersionNumber(SQLDatabase& db)
+{
+ return SQLStatement(db, "SELECT value FROM IconDatabaseInfo WHERE key = 'Version';").getColumnInt(0);
+}
+
+static bool isValidDatabase(SQLDatabase& db)
+{
+
+ // These four tables should always exist in a valid db
+ if (!db.tableExists("IconInfo") || !db.tableExists("IconData") || !db.tableExists("PageURL") || !db.tableExists("IconDatabaseInfo"))
+ return false;
+
+ if (databaseVersionNumber(db) != currentDatabaseVersion) {
+ LOG(IconDatabase, "DB version is not found or below expected valid version");
+ return false;
+ }
return true;
}
-void IconDatabase::setIconURLForPageURLInDatabase(const String& iconURL, const String& pageURL)
-{
- int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL);
- if (!iconID) {
- LOG_ERROR("Failed to establish an ID for iconURL %s", iconURL.ascii().data());
- return;
- }
- setIconIDForPageURLQuery(*m_currentDB, iconID, pageURL);
-}
+static void createDatabaseTables(SQLDatabase& db)
+{
+ if (!db.executeCommand("CREATE TABLE PageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) {
+ LOG_ERROR("Could not create PageURL table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
+ db.close();
+ return;
+ }
+ if (!db.executeCommand("CREATE INDEX PageURLIndex ON PageURL (url);")) {
+ LOG_ERROR("Could not create PageURL index in database (%i) - %s", db.lastError(), db.lastErrorMsg());
+ db.close();
+ return;
+ }
+ if (!db.executeCommand("CREATE TABLE IconInfo (iconID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE ON CONFLICT REPLACE, url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT FAIL, stamp INTEGER);")) {
+ LOG_ERROR("Could not create IconInfo table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
+ db.close();
+ return;
+ }
+ if (!db.executeCommand("CREATE INDEX IconInfoIndex ON IconInfo (url, iconID);")) {
+ LOG_ERROR("Could not create PageURL index in database (%i) - %s", db.lastError(), db.lastErrorMsg());
+ db.close();
+ return;
+ }
+ if (!db.executeCommand("CREATE TABLE IconData (iconID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE ON CONFLICT REPLACE, data BLOB);")) {
+ LOG_ERROR("Could not create IconData table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
+ db.close();
+ return;
+ }
+ if (!db.executeCommand("CREATE INDEX IconDataIndex ON IconData (iconID);")) {
+ LOG_ERROR("Could not create PageURL index in database (%i) - %s", db.lastError(), db.lastErrorMsg());
+ db.close();
+ return;
+ }
+ if (!db.executeCommand("CREATE TABLE IconDatabaseInfo (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) {
+ LOG_ERROR("Could not create IconDatabaseInfo table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
+ db.close();
+ return;
+ }
+ if (!db.executeCommand(String("INSERT INTO IconDatabaseInfo VALUES ('Version', ") + String::number(currentDatabaseVersion) + ");")) {
+ LOG_ERROR("Could not insert icon database version into IconDatabaseInfo table (%i) - %s", db.lastError(), db.lastErrorMsg());
+ db.close();
+ return;
+ }
+}
+
+void IconDatabase::performOpenInitialization()
+{
+ ASSERT_ICON_SYNC_THREAD();
+
+ if (!isOpen())
+ return;
+
+ if (checkIntegrityOnOpen) {
+ checkIntegrityOnOpen = false;
+ if (!checkIntegrity()) {
+ LOG(IconDatabase, "Integrity check was bad - dumping IconDatabase");
+
+ close();
+
+ {
+ MutexLocker locker(m_syncLock);
+ // Should've been consumed by SQLite, delete just to make sure we don't see it again in the future;
+ deleteFile(m_completeDatabasePath + "-journal");
+ deleteFile(m_completeDatabasePath);
+ }
+
+ // Reopen the write database, creating it from scratch
+ if (!m_syncDB.open(m_completeDatabasePath)) {
+ LOG_ERROR("Unable to open icon database at path %s - %s", m_completeDatabasePath.utf8().data(), m_syncDB.lastErrorMsg());
+ return;
+ }
+ }
+ }
+
+ if (!isValidDatabase(m_syncDB)) {
+ LOG(IconDatabase, "%s is missing or in an invalid state - reconstructing", m_syncDB.path().utf8().data());
+ m_syncDB.clearAllTables();
+ createDatabaseTables(m_syncDB);
+ }
+
+ // Reduce sqlite RAM cache size from default 2000 pages (~1.5kB per page). 3MB of cache for icon database is overkill
+ if (!SQLStatement(m_syncDB, "PRAGMA cache_size = 200;").executeCommand())
+ LOG_ERROR("SQLite database could not set cache_size");
+}
+
+bool IconDatabase::checkIntegrity()
+{
+ ASSERT_ICON_SYNC_THREAD();
+
+ SQLStatement integrity(m_syncDB, "PRAGMA integrity_check;");
+ if (integrity.prepare() != SQLResultOk) {
+ LOG_ERROR("checkIntegrity failed to execute");
+ return false;
+ }
+
+ int resultCode = integrity.step();
+ if (resultCode == SQLResultOk)
+ return true;
+
+ if (resultCode != SQLResultRow)
+ return false;
+
+ int columns = integrity.columnCount();
+ if (columns != 1) {
+ LOG_ERROR("Received %i columns performing integrity check, should be 1", columns);
+ return false;
+ }
+
+ String resultText = integrity.getColumnText16(0);
+
+ // A successful, no-error integrity check will be "ok" - all other strings imply failure
+ if (resultText == "ok")
+ return true;
+
+ LOG_ERROR("Icon database integrity check failed - \n%s", resultText.utf8().data());
+ return false;
+}
+
+void IconDatabase::performURLImport()
+{
+ ASSERT_ICON_SYNC_THREAD();
+
+ SQLStatement query(m_syncDB, "SELECT PageURL.url, IconInfo.url, IconInfo.stamp FROM PageURL INNER JOIN IconInfo ON PageURL.iconID=IconInfo.iconID;");
+
+ if (query.prepare() != SQLResultOk) {
+ LOG_ERROR("Unable to prepare icon url import query");
+ return;
+ }
+
+ // Informal testing shows that draining the autorelease pool every 25 iterations is about as low as we can go
+ // before performance starts to drop off, but we don't want to increase this number because then accumulated memory usage will go up
+ AutodrainedPool pool(25);
+
+ int result = query.step();
+ while (result == SQLResultRow) {
+ String pageURL = query.getColumnText16(0);
+ String iconURL = query.getColumnText16(1);
+
+ {
+ MutexLocker locker(m_urlAndIconLock);
+
+ PageURLRecord* pageRecord = m_pageURLToRecordMap.get(pageURL);
+
+ // If the pageRecord doesn't exist in this map, then no one has retained this pageURL
+ // If the s_databaseCleanupCounter count is non-zero, then we're not supposed to be pruning the database in any manner,
+ // so go ahead and actually create a pageURLRecord for this url even though it's not retained.
+ // If database cleanup *is* allowed, we don't want to bother pulling in a page url from disk that noone is actually interested
+ // in - we'll prune it later instead!
+ if (!pageRecord && databaseCleanupCounter) {
+ pageRecord = new PageURLRecord(pageURL);
+ m_pageURLToRecordMap.set(pageURL, pageRecord);
+ }
+
+ if (pageRecord) {
+ IconRecord* currentIcon = pageRecord->iconRecord();
+
+ if (!currentIcon || currentIcon->iconURL() != iconURL) {
+ currentIcon = getOrCreateIconRecord(iconURL);
+ pageRecord->setIconRecord(currentIcon);
+ }
+
+ // Regardless, the time stamp from disk still takes precedence. Until we read this icon from disk, we didn't think we'd seen it before
+ // so we marked the timestamp as "now", but it's really much older
+ currentIcon->setTimestamp(query.getColumnInt(2));
+ }
+ }
+
+ // FIXME: Currently the WebKit API supports 1 type of notification that is sent whenever we get an Icon URL for a Page URL. We might want to re-purpose it to work for
+ // getting the actually icon itself also (so each pageurl would get this notification twice) or we might want to add a second type of notification -
+ // one for the URL and one for the Image itself
+ // Note that WebIconDatabase is not neccessarily API so we might be able to make this change
+ {
+ MutexLocker locker(m_pendingReadingLock);
+ if (m_pageURLsPendingImport.contains(pageURL)) {
+ m_client->dispatchDidAddIconForPageURL(pageURL);
+ m_pageURLsPendingImport.remove(pageURL);
+
+ pool.cycle();
+ }
+ }
+
+ // Stop the import at any time of the thread has been asked to shutdown
+ if (shouldStopThreadActivity()) {
+ LOG(IconDatabase, "IconDatabase asked to terminate during performURLImport()");
+ return;
+ }
+
+ result = query.step();
+ }
+
+ if (result != SQLResultDone)
+ LOG(IconDatabase, "Error reading page->icon url mappings from database");
+
+ // Clear the m_pageURLsPendingImport set - either the page URLs ended up with an iconURL (that we'll notify about) or not,
+ // but after m_iconURLImportComplete is set to true, we don't care about this set anymore
+ Vector<String> urls;
+ {
+ MutexLocker locker(m_pendingReadingLock);
+
+ urls.appendRange(m_pageURLsPendingImport.begin(), m_pageURLsPendingImport.end());
+ m_pageURLsPendingImport.clear();
+ m_iconURLImportComplete = true;
+ }
+
+ Vector<String> urlsToNotify;
+
+ // Loop through the urls pending import
+ // Remove unretained ones if database cleanup is allowed
+ // Keep a set of ones that are retained and pending notification
+
+ {
+ MutexLocker locker(m_urlAndIconLock);
+
+ for (unsigned i = 0; i < urls.size(); ++i) {
+ if (!m_retainedPageURLs.contains(urls[i])) {
+ PageURLRecord* record = m_pageURLToRecordMap.get(urls[i]);
+ if (record && !databaseCleanupCounter) {
+ m_pageURLToRecordMap.remove(urls[i]);
+ IconRecord* iconRecord = record->iconRecord();
+
+ // If this page is the only remaining retainer of its icon, mark that icon for deletion and don't bother
+ // reading anything related to it
+ if (iconRecord && iconRecord->hasOneRef()) {
+ m_iconURLToRecordMap.remove(iconRecord->iconURL());
+
+ {
+ MutexLocker locker(m_pendingReadingLock);
+ m_pageURLsInterestedInIcons.remove(urls[i]);
+ m_iconsPendingReading.remove(iconRecord);
+ }
+ {
+ MutexLocker locker(m_pendingSyncLock);
+ m_iconsPendingSync.set(iconRecord->iconURL(), iconRecord->snapshot(true));
+ }
+ }
+
+ delete record;
+ }
+ } else {
+ urlsToNotify.append(urls[i]);
+ }
+ }
+ }
+
+ LOG(IconDatabase, "Notifying %i interested page URLs that their icon URL is known due to the import", urlsToNotify.size());
+ // Now that we don't hold any locks, perform the actual notifications
+ for (unsigned i = 0; i < urlsToNotify.size(); ++i) {
+ LOG(IconDatabase, "Notifying icon info known for pageURL %s", urlsToNotify[i].utf8().data());
+ m_client->dispatchDidAddIconForPageURL(urlsToNotify[i]);
+ if (shouldStopThreadActivity())
+ return;
+
+ pool.cycle();
+ }
+
+ // Notify all DocumentLoaders that were waiting for an icon load decision on the main thread
+ callOnMainThread(notifyPendingLoadDecisions);
+}
+
+void* IconDatabase::syncThreadMainLoop()
+{
+ ASSERT_ICON_SYNC_THREAD();
+ static bool prunedUnretainedIcons = false;
+
+ while (true) {
+ m_syncLock.unlock();
+
+#ifndef NDEBUG
+ double timeStamp = currentTime();
+#endif
+ LOG(IconDatabase, "(THREAD) Main work loop starting");
+
+ // If we should remove all icons, do it now. This is an uninteruptible procedure that we will always do before quitting if it is requested
+ if (m_removeIconsRequested) {
+ removeAllIconsOnThread();
+ m_removeIconsRequested = false;
+ }
+
+ // Then, if the thread should be quitting, quit now!
+ if (m_threadTerminationRequested)
+ break;
+
+ bool didAnyWork = true;
+ while (didAnyWork) {
+ didAnyWork = readFromDatabase();
+ if (shouldStopThreadActivity())
+ break;
+
+ bool didWrite = writeToDatabase();
+ if (shouldStopThreadActivity())
+ break;
+
+ // Prune unretained icons after the first time we sync anything out to the database
+ // This way, pruning won't be the only operation we perform to the database by itself
+ // We also don't want to bother doing this if the thread should be terminating (the user is quitting)
+ // or if private browsing is enabled
+ // We also don't want to prune if the m_databaseCleanupCounter count is non-zero - that means someone
+ // has asked to delay pruning
+ if (didWrite && !m_privateBrowsingEnabled && !prunedUnretainedIcons && !databaseCleanupCounter) {
+#ifndef NDEBUG
+ double time = currentTime();
+#endif
+ LOG(IconDatabase, "(THREAD) Starting pruneUnretainedIcons()");
+
+ pruneUnretainedIcons();
+
+ LOG(IconDatabase, "(THREAD) pruneUnretainedIcons() took %.4f seconds", currentTime() - time);
+
+ // If pruneUnretainedIcons() returned early due to requested thread termination, its still okay
+ // to mark prunedUnretainedIcons true because we're about to terminate anyway
+ prunedUnretainedIcons = true;
+ }
+
+ didAnyWork = didAnyWork || didWrite;
+ if (shouldStopThreadActivity())
+ break;
+ }
+
+#ifndef NDEBUG
+ double newstamp = currentTime();
+ LOG(IconDatabase, "(THREAD) Main work loop ran for %.4f seconds, %s requested to terminate", newstamp - timeStamp, shouldStopThreadActivity() ? "was" : "was not");
+#endif
+
+ m_syncLock.lock();
+
+ // There is some condition that is asking us to stop what we're doing now and handle a special case
+ // This is either removing all icons, or shutting down the thread to quit the app
+ // We handle those at the top of this main loop so continue to jump back up there
+ if (shouldStopThreadActivity())
+ continue;
+
+ m_syncCondition.wait(m_syncLock);
+ }
+ m_syncLock.unlock();
+
+ // Thread is terminating at this point
+ return cleanupSyncThread();
+}
+
+bool IconDatabase::readFromDatabase()
+{
+ ASSERT_ICON_SYNC_THREAD();
+
+#ifndef NDEBUG
+ double timeStamp = currentTime();
+#endif
+
+ bool didAnyWork = false;
+
+ // We'll make a copy of the sets of things that need to be read. Then we'll verify at the time of updating the record that it still wants to be updated
+ // This way we won't hold the lock for a long period of time
+ Vector<IconRecord*> icons;
+ {
+ MutexLocker locker(m_pendingReadingLock);
+ icons.appendRange(m_iconsPendingReading.begin(), m_iconsPendingReading.end());
+ }
+
+ // Keep track of icons we actually read to notify them of the new icon
+ HashSet<String> urlsToNotify;
+
+ for (unsigned i = 0; i < icons.size(); ++i) {
+ didAnyWork = true;
+ RefPtr<SharedBuffer> imageData = getImageDataForIconURLFromSQLDatabase(icons[i]->iconURL());
+
+ // Verify this icon still wants to be read from disk
+ {
+ MutexLocker urlLocker(m_urlAndIconLock);
+ {
+ MutexLocker readLocker(m_pendingReadingLock);
+
+ if (m_iconsPendingReading.contains(icons[i])) {
+ // Set the new data
+ icons[i]->setImageData(imageData.get());
+
+ // Remove this icon from the set that needs to be read
+ m_iconsPendingReading.remove(icons[i]);
+
+ // We have a set of all Page URLs that retain this icon as well as all PageURLs waiting for an icon
+ // We want to find the intersection of these two sets to notify them
+ // Check the sizes of these two sets to minimize the number of iterations
+ const HashSet<String>* outerHash;
+ const HashSet<String>* innerHash;
+
+ if (icons[i]->retainingPageURLs().size() > m_pageURLsInterestedInIcons.size()) {
+ outerHash = &m_pageURLsInterestedInIcons;
+ innerHash = &(icons[i]->retainingPageURLs());
+ } else {
+ innerHash = &m_pageURLsInterestedInIcons;
+ outerHash = &(icons[i]->retainingPageURLs());
+ }
+
+ HashSet<String>::const_iterator iter = outerHash->begin();
+ HashSet<String>::const_iterator end = outerHash->end();
+ for (; iter != end; ++iter) {
+ if (innerHash->contains(*iter)) {
+ LOG(IconDatabase, "%s is interesting in the icon we just read. Adding it to the list and removing it from the interested set", urlForLogging(*iter).utf8().data());
+ urlsToNotify.add(*iter);
+ }
+
+ // If we ever get to the point were we've seen every url interested in this icon, break early
+ if (urlsToNotify.size() == m_pageURLsInterestedInIcons.size())
+ break;
+ }
+
+ // We don't need to notify a PageURL twice, so all the ones we're about to notify can be removed from the interested set
+ if (urlsToNotify.size() == m_pageURLsInterestedInIcons.size())
+ m_pageURLsInterestedInIcons.clear();
+ else {
+ iter = urlsToNotify.begin();
+ end = urlsToNotify.end();
+ for (; iter != end; ++iter)
+ m_pageURLsInterestedInIcons.remove(*iter);
+ }
+ }
+ }
+ }
+
+ if (shouldStopThreadActivity())
+ return didAnyWork;
+
+ // Informal testing shows that draining the autorelease pool every 25 iterations is about as low as we can go
+ // before performance starts to drop off, but we don't want to increase this number because then accumulated memory usage will go up
+ AutodrainedPool pool(25);
+
+ // Now that we don't hold any locks, perform the actual notifications
+ HashSet<String>::iterator iter = urlsToNotify.begin();
+ HashSet<String>::iterator end = urlsToNotify.end();
+ for (unsigned iteration = 0; iter != end; ++iter, ++iteration) {
+ LOG(IconDatabase, "Notifying icon received for pageURL %s", urlForLogging(*iter).utf8().data());
+ m_client->dispatchDidAddIconForPageURL(*iter);
+ if (shouldStopThreadActivity())
+ return didAnyWork;
+
+ pool.cycle();
+ }
+
+ LOG(IconDatabase, "Done notifying %i pageURLs who just received their icons", urlsToNotify.size());
+ urlsToNotify.clear();
+
+ if (shouldStopThreadActivity())
+ return didAnyWork;
+ }
+
+ LOG(IconDatabase, "Reading from database took %.4f seconds", currentTime() - timeStamp);
+
+ return didAnyWork;
+}
+
+bool IconDatabase::writeToDatabase()
+{
+ ASSERT_ICON_SYNC_THREAD();
+
+#ifndef NDEBUG
+ double timeStamp = currentTime();
+#endif
+
+ bool didAnyWork = false;
+
+ // We can copy the current work queue then clear it out - If any new work comes in while we're writing out,
+ // we'll pick it up on the next pass. This greatly simplifies the locking strategy for this method and remains cohesive with changes
+ // asked for by the database on the main thread
+ Vector<IconSnapshot> iconSnapshots;
+ Vector<PageURLSnapshot> pageSnapshots;
+ {
+ MutexLocker locker(m_pendingSyncLock);
+
+ iconSnapshots.appendRange(m_iconsPendingSync.begin().values(), m_iconsPendingSync.end().values());
+ m_iconsPendingSync.clear();
+
+ pageSnapshots.appendRange(m_pageURLsPendingSync.begin().values(), m_pageURLsPendingSync.end().values());
+ m_pageURLsPendingSync.clear();
+ }
+
+ if (iconSnapshots.size() || pageSnapshots.size())
+ didAnyWork = true;
+
+ SQLTransaction syncTransaction(m_syncDB);
+ syncTransaction.begin();
+
+ for (unsigned i = 0; i < iconSnapshots.size(); ++i) {
+ writeIconSnapshotToSQLDatabase(iconSnapshots[i]);
+ LOG(IconDatabase, "Wrote IconRecord for IconURL %s with timeStamp of %li to the DB", urlForLogging(iconSnapshots[i].iconURL).utf8().data(), iconSnapshots[i].timestamp);
+ }
+
+ for (unsigned i = 0; i < pageSnapshots.size(); ++i) {
+ String iconURL = pageSnapshots[i].iconURL;
+
+ // If the icon URL is empty, this page is meant to be deleted
+ // ASSERTs are sanity checks to make sure the mappings exist if they should and don't if they shouldn't
+ if (pageSnapshots[i].iconURL.isEmpty())
+ removePageURLFromSQLDatabase(pageSnapshots[i].pageURL);
+ else {
+ setIconURLForPageURLInSQLDatabase(pageSnapshots[i].iconURL, pageSnapshots[i].pageURL);
+ }
+ LOG(IconDatabase, "Committed IconURL for PageURL %s to database", urlForLogging(pageSnapshots[i].pageURL).utf8().data());
+ }
+
+ syncTransaction.commit();
+
+ // Check to make sure there are no dangling PageURLs - If there are, we want to output one log message but not spam the console potentially every few seconds
+ checkForDanglingPageURLs(false);
+
+ LOG(IconDatabase, "Updating the database took %.4f seconds", currentTime() - timeStamp);
-int64_t IconDatabase::establishIconIDForIconURL(SQLDatabase& db, const String& iconURL, bool createIfNecessary)
-{
- // Get the iconID thats already in this database and return it - or return 0 if we're read-only
- int64_t iconID = getIconIDForIconURLQuery(db, iconURL);
- if (iconID || !createIfNecessary)
- return iconID;
-
- // Create the icon table entry for the iconURL
- return addIconForIconURLQuery(db, iconURL);
+ return didAnyWork;
}
-void IconDatabase::pruneUnretainedIconsOnStartup(Timer<IconDatabase>*)
+void IconDatabase::pruneUnretainedIcons()
{
- if (!isOpen())
- return;
-
- // This function should only be called once per run, and ideally only via the timer
- // on program startup
- ASSERT(!m_initialPruningComplete);
+ ASSERT_ICON_SYNC_THREAD();
-#ifndef NDEBUG
- double timestamp = currentTime();
-#endif
-
- // rdar://4690949 - Need to prune unretained iconURLs here, then prune out all pageURLs that reference
- // nonexistent icons
-
- SQLTransaction pruningTransaction(m_mainDB);
- pruningTransaction.begin();
+ if (!isOpen())
+ return;
- // Wipe all PageURLs that aren't retained
- // Temporary tables in sqlite seem to lose memory permanently so do this by hand instead. This is faster too.
+ // This method should only be called once per run
+ ASSERT(!m_initialPruningComplete);
- Vector<int64_t> pageURLIconIDsToDelete;
+ // This method relies on having read in all page URLs from the database earlier.
+ ASSERT(m_iconURLImportComplete);
// Get the known PageURLs from the db, and record the ID of any that are not in the retain count set.
- SQLStatement pageSQL(m_mainDB, "SELECT url, iconID FROM PageURL");
+ Vector<int64_t> pageIDsToDelete;
+
+ SQLStatement pageSQL(m_syncDB, "SELECT rowid, url FROM PageURL;");
pageSQL.prepare();
+
int result;
while ((result = pageSQL.step()) == SQLResultRow) {
- String pageURL = pageSQL.getColumnText16(0);
- if (pageURL.isEmpty() || !m_pageURLToRetainCount.contains(pageURL))
- pageURLIconIDsToDelete.append(pageSQL.getColumnInt64(1));
+ MutexLocker locker(m_urlAndIconLock);
+ if (!m_pageURLToRecordMap.contains(pageSQL.getColumnText16(1)))
+ pageIDsToDelete.append(pageSQL.getColumnInt64(0));
}
- pageSQL.finalize();
+
if (result != SQLResultDone)
LOG_ERROR("Error reading PageURL table from on-disk DB");
+ pageSQL.finalize();
// Delete page URLs that were in the table, but not in our retain count set.
- size_t numToDelete = pageURLIconIDsToDelete.size();
+ size_t numToDelete = pageIDsToDelete.size();
if (numToDelete) {
- SQLStatement pageDeleteSQL(m_mainDB, "DELETE FROM PageURL WHERE iconID = (?)");
+ SQLTransaction pruningTransaction(m_syncDB);
+ pruningTransaction.begin();
+
+ SQLStatement pageDeleteSQL(m_syncDB, "DELETE FROM PageURL WHERE rowid = (?);");
pageDeleteSQL.prepare();
for (size_t i = 0; i < numToDelete; ++i) {
- pageDeleteSQL.bindInt64(1, pageURLIconIDsToDelete[i]);
- if (pageDeleteSQL.step() != SQLResultDone)
- LOG_ERROR("Unable to delete icon ID %llu from PageURL table", static_cast<unsigned long long>(pageURLIconIDsToDelete[i]));
+ LOG(IconDatabase, "Pruning page with rowid %lli from disk", pageIDsToDelete[i]);
+ pageDeleteSQL.bindInt64(1, pageIDsToDelete[i]);
+ int result = pageDeleteSQL.step();
+ if (result != SQLResultDone)
+ LOG_ERROR("Unabled to delete page with id %lli from disk", pageIDsToDelete[i]);
pageDeleteSQL.reset();
+
+ // If the thread was asked to terminate, we should commit what pruning we've done so far, figuring we can
+ // finish the rest later (hopefully)
+ if (shouldStopThreadActivity()) {
+ pruningTransaction.commit();
+ return;
+ }
}
+ pruningTransaction.commit();
pageDeleteSQL.finalize();
}
+ // Deleting unreferenced icons from the Icon tables has to be atomic -
+ // If the user quits while these are taking place, they might have to wait. Thankfully this will rarely be an issue
+ // A user on a network home directory with a wildly inconsistent database might see quite a pause...
+
+ SQLTransaction pruningTransaction(m_syncDB);
+ pruningTransaction.begin();
+
// Wipe Icons that aren't retained
- if (!m_mainDB.executeCommand("DELETE FROM Icon WHERE Icon.iconID NOT IN (SELECT iconID FROM PageURL);"))
- LOG_ERROR("Failed to execute SQL to prune unretained icons from the on-disk tables");
-
- // Since we lazily retained the pageURLs without getting the iconURLs or retaining the iconURLs,
- // we need to do that now
- SQLStatement sql(m_mainDB, "SELECT PageURL.url, Icon.url FROM PageURL INNER JOIN Icon ON PageURL.iconID=Icon.iconID");
- sql.prepare();
- while((result = sql.step()) == SQLResultRow) {
- String iconURL = sql.getColumnText16(1);
- retainIconURL(iconURL);
- LOG(IconDatabase, "Found a PageURL that mapped to %s", iconURL.ascii().data());
- }
- if (result != SQLResultDone)
- LOG_ERROR("Error reading PageURL->IconURL mappings from on-disk DB");
- sql.finalize();
+ if (!m_syncDB.executeCommand("DELETE FROM IconData WHERE iconID NOT IN (SELECT iconID FROM PageURL);"))
+ LOG_ERROR("Failed to execute SQL to prune unretained icons from the on-disk IconData table");
+ if (!m_syncDB.executeCommand("DELETE FROM IconInfo WHERE iconID NOT IN (SELECT iconID FROM PageURL);"))
+ LOG_ERROR("Failed to execute SQL to prune unretained icons from the on-disk IconInfo table");
pruningTransaction.commit();
- m_initialPruningComplete = true;
-
- // Handle dangling PageURLs, if any
+
checkForDanglingPageURLs(true);
-#ifndef NDEBUG
- timestamp = currentTime() - timestamp;
- if (timestamp <= 1.0)
- LOG(IconDatabase, "Pruning unretained icons took %.4f seconds", timestamp);
- else
- LOG(IconDatabase, "Pruning unretained icons took %.4f seconds - this is much too long!", timestamp);
-
-#endif
-}
-
-void IconDatabase::updateDatabase(Timer<IconDatabase>*)
-{
- syncDatabase();
-}
-
-void IconDatabase::syncDatabase()
-{
-#ifndef NDEBUG
- double timestamp = currentTime();
-#endif
-
- SQLTransaction syncTransaction(*m_currentDB);
- syncTransaction.begin();
-
- // First we'll do the pending additions
- // Starting with the IconDataCaches that need updating/insertion
- for (HashSet<IconDataCache*>::iterator i = m_iconDataCachesPendingUpdate.begin(), end = m_iconDataCachesPendingUpdate.end(); i != end; ++i) {
- (*i)->writeToDatabase(*m_currentDB);
- LOG(IconDatabase, "Wrote IconDataCache for IconURL %s with timestamp of %li to the DB", (*i)->getIconURL().ascii().data(), (*i)->getTimestamp());
- }
- m_iconDataCachesPendingUpdate.clear();
-
- HashSet<String>::iterator i = m_pageURLsPendingAddition.begin(), end = m_pageURLsPendingAddition.end();
- for (; i != end; ++i) {
- setIconURLForPageURLInDatabase(m_pageURLToIconURLMap.get(*i), *i);
- LOG(IconDatabase, "Committed IconURL for PageURL %s to database", (*i).ascii().data());
- }
- m_pageURLsPendingAddition.clear();
-
- // Then we'll do the pending deletions
- // First lets wipe all the pageURLs
- for (i = m_pageURLsPendingDeletion.begin(), end = m_pageURLsPendingDeletion.end(); i != end; ++i) {
- forgetPageURL(*i);
- LOG(IconDatabase, "Deleted PageURL %s", (*i).ascii().data());
- }
- m_pageURLsPendingDeletion.clear();
-
- // Then get rid of all traces of the icons and IconURLs
- IconDataCache* icon;
- for (i = m_iconURLsPendingDeletion.begin(), end = m_iconURLsPendingDeletion.end(); i != end; ++i) {
- // Forget the IconDataCache
- icon = m_iconURLToIconDataCacheMap.get(*i);
- if (icon)
- m_iconURLToIconDataCacheMap.remove(*i);
- delete icon;
-
- // Forget the IconURL from the database
- forgetIconForIconURLFromDatabase(*i);
- LOG(IconDatabase, "Deleted icon %s", (*i).ascii().data());
- }
- m_iconURLsPendingDeletion.clear();
- syncTransaction.commit();
-
- // If the timer was running to cause this update, we can kill the timer as its firing would be redundant
- m_updateTimer.stop();
-
-#ifndef NDEBUG
- timestamp = currentTime() - timestamp;
- if (timestamp <= 1.0)
- LOG(IconDatabase, "Updating the database took %.4f seconds", timestamp);
- else
- LOG(IconDatabase, "Updating the database took %.4f seconds - this is much too long!", timestamp);
-
- // Check to make sure there are no dangling PageURLs - If there are, we want to output one log message but not spam the console potentially every few seconds
- checkForDanglingPageURLs(false);
-#endif
+ m_initialPruningComplete = true;
}
void IconDatabase::checkForDanglingPageURLs(bool pruneIfFound)
{
+ ASSERT_ICON_SYNC_THREAD();
+
// We don't want to keep performing this check and reporting this error if it has already found danglers so we keep track
static bool danglersFound = false;
if (pruneIfFound)
danglersFound = false;
- if (!danglersFound && SQLStatement(*m_currentDB, "SELECT url FROM PageURL WHERE PageURL.iconID NOT IN (SELECT iconID FROM Icon) LIMIT 1;").returnsAtLeastOneResult()) {
+ if (!danglersFound && SQLStatement(m_syncDB, "SELECT url FROM PageURL WHERE PageURL.iconID NOT IN (SELECT iconID FROM IconInfo) LIMIT 1;").returnsAtLeastOneResult()) {
danglersFound = true;
LOG_ERROR("Dangling PageURL entries found");
- if (pruneIfFound && !m_currentDB->executeCommand("DELETE FROM PageURL WHERE iconID NOT IN (SELECT iconID FROM Icon);"))
+ if (pruneIfFound && !m_syncDB.executeCommand("DELETE FROM PageURL WHERE iconID NOT IN (SELECT iconID FROM IconInfo);"))
LOG_ERROR("Unable to prune dangling PageURLs");
}
}
-bool IconDatabase::hasEntryForIconURL(const String& iconURL)
+void IconDatabase::removeAllIconsOnThread()
{
- if (!isOpen() || iconURL.isEmpty())
- return false;
-
- // First check the in memory mapped icons...
- if (m_iconURLToIconDataCacheMap.contains(iconURL))
- return true;
+ ASSERT_ICON_SYNC_THREAD();
- // Then we'll check the main database
- if (hasIconForIconURLQuery(m_mainDB, iconURL))
- return true;
-
- // Finally, the last resort - check the private browsing database
- if (m_privateBrowsingEnabled)
- if (hasIconForIconURLQuery(m_privateBrowsingDB, iconURL))
- return true;
+ LOG(IconDatabase, "Removing all icons on the sync thread");
- // We must not have this iconURL!
- return false;
+ //Delete all the prepared statements so they can start over
+ deleteAllPreparedStatements();
+
+ {
+ MutexLocker locker(m_urlAndIconLock);
+
+ // Clear all in-memory records of pages and icons
+ m_iconURLToRecordMap.clear();
+ // Deleting the PageURLRecords derefs the IconRecords automagically
+ deleteAllValues(m_pageURLToRecordMap);
+ m_pageURLToRecordMap.clear();
+
+ // Clear all in-memory records of things that need to be synced out to disk
+ {
+ MutexLocker locker(m_pendingSyncLock);
+ m_pageURLsPendingSync.clear();
+ m_iconsPendingSync.clear();
+ }
+
+ // Clear all in-memory records of things that need to be read in from disk
+ {
+ MutexLocker locker(m_pendingReadingLock);
+ m_pageURLsPendingImport.clear();
+ m_pageURLsInterestedInIcons.clear();
+ m_iconsPendingReading.clear();
+ m_loadersPendingDecision.clear();
+ }
+ }
+
+ // To reset the on-disk database, we'll wipe all its tables then vacuum it
+ // This is easier and safer than closing it, deleting the file, and recreating from scratch
+ m_syncDB.clearAllTables();
+ m_syncDB.runVacuumCommand();
+ createDatabaseTables(m_syncDB);
+
+ LOG(IconDatabase, "Dispatching notification that we removed all icons");
+ m_client->dispatchDidRemoveAllIcons();
}
-void IconDatabase::setEnabled(bool enabled)
-{
- if (!enabled && isOpen())
- close();
- m_isEnabled = enabled;
+void IconDatabase::deleteAllPreparedStatements()
+{
+ ASSERT_ICON_SYNC_THREAD();
+
+ m_setIconIDForPageURLStatement.set(0);
+ m_removePageURLStatement.set(0);
+ m_getIconIDForIconURLStatement.set(0);
+ m_getImageDataForIconURLStatement.set(0);
+ m_addIconToIconInfoStatement.set(0);
+ m_addIconToIconDataStatement.set(0);
+ m_getImageDataStatement.set(0);
+ m_deletePageURLsForIconURLStatement.set(0);
+ m_deleteIconFromIconInfoStatement.set(0);
+ m_deleteIconFromIconDataStatement.set(0);
+ m_updateIconInfoStatement.set(0);
+ m_updateIconDataStatement.set(0);
+ m_setIconInfoStatement.set(0);
+ m_setIconDataStatement.set(0);
}
-bool IconDatabase::enabled() const
+void* IconDatabase::cleanupSyncThread()
{
- return m_isEnabled;
+ ASSERT_ICON_SYNC_THREAD();
+
+#ifndef NDEBUG
+ double timeStamp = currentTime();
+#endif
+
+ // If the removeIcons flag is set, remove all icons from the db.
+ // Otherwise, do a final write an sync out to disk
+ if (m_removeIconsRequested)
+ removeAllIconsOnThread();
+ else {
+ LOG(IconDatabase, "(THREAD) Doing final writeout and closure of sync thread");
+ writeToDatabase();
+ }
+
+ // Close the database
+ MutexLocker locker(m_syncLock);
+
+ m_completeDatabasePath = String();
+ deleteAllPreparedStatements();
+ m_syncDB.close();
+
+#ifndef NDEBUG
+ LOG(IconDatabase, "(THREAD) Final closure took %.4f seconds", currentTime() - timeStamp);
+#endif
+
+ return 0;
}
bool IconDatabase::imported()
{
- if (!m_isImportedSet) {
- m_imported = importedQuery(m_mainDB);
- m_isImportedSet = true;
+ ASSERT_ICON_SYNC_THREAD();
+
+ if (m_isImportedSet)
+ return m_imported;
+
+ SQLStatement query(m_syncDB, "SELECT IconDatabaseInfo.value FROM IconDatabaseInfo WHERE IconDatabaseInfo.key = \"ImportedSafari2Icons\";");
+ if (query.prepare() != SQLResultOk) {
+ LOG_ERROR("Unable to prepare imported statement");
+ return false;
+ }
+
+ int result = query.step();
+ if (result == SQLResultRow)
+ result = query.getColumnInt(0);
+ else {
+ if (result != SQLResultDone)
+ LOG_ERROR("imported statement failed");
+ result = 0;
}
- return m_imported;
+
+ m_isImportedSet = true;
+ return m_imported = result;
}
void IconDatabase::setImported(bool import)
{
+ ASSERT_ICON_SYNC_THREAD();
+
m_imported = import;
m_isImportedSet = true;
- setImportedQuery(m_mainDB, import);
-}
-
-IconDatabase::~IconDatabase()
-{
- ASSERT_NOT_REACHED();
+
+ String queryString = import ?
+ "INSERT INTO IconDatabaseInfo (key, value) VALUES (\"ImportedSafari2Icons\", 1);" :
+ "INSERT INTO IconDatabaseInfo (key, value) VALUES (\"ImportedSafari2Icons\", 0);";
+
+ SQLStatement query(m_syncDB, queryString);
+
+ if (query.prepare() != SQLResultOk) {
+ LOG_ERROR("Unable to prepare set imported statement");
+ return;
+ }
+
+ if (query.step() != SQLResultDone)
+ LOG_ERROR("set imported statement failed");
}
// readySQLStatement() handles two things
// 1 - If the SQLDatabase& argument is different, the statement must be destroyed and remade. This happens when the user
// switches to and from private browsing
// 2 - Lazy construction of the Statement in the first place, in case we've never made this query before
-inline void readySQLStatement(SQLStatement*& statement, SQLDatabase& db, const String& str)
+inline void readySQLStatement(OwnPtr<SQLStatement>& statement, SQLDatabase& db, const String& str)
{
if (statement && (statement->database() != &db || statement->isExpired())) {
if (statement->isExpired())
- LOG(IconDatabase, "SQLStatement associated with %s is expired", str.ascii().data());
- delete statement;
- statement = 0;
+ LOG(IconDatabase, "SQLStatement associated with %s is expired", str.utf8().data());
+ statement.set(0);
}
if (!statement) {
- statement = new SQLStatement(db, str);
+ statement.set(new SQLStatement(db, str));
int result;
result = statement->prepare();
ASSERT(result == SQLResultOk);
}
}
-// Any common IconDatabase query should be seperated into a fooQuery() and a *m_fooStatement.
-// This way we can lazily construct the SQLStatment for a query on its first use, then reuse the Statement binding
-// the new parameter as needed
-// The statement must be deleted in IconDatabase::close() before the actual SQLDatabase::close() call
-// Also, m_fooStatement must be reset() before fooQuery() returns otherwise we will constantly get "database file locked"
-// errors in various combinations of queries
-
-bool IconDatabase::pageURLTableIsEmptyQuery(SQLDatabase& db)
-{
- // We won't make this use a m_fooStatement because its not really a "common" query
- return !SQLStatement(db, "SELECT iconID FROM PageURL LIMIT 1;").returnsAtLeastOneResult();
-}
-
-PassRefPtr<SharedBuffer> IconDatabase::imageDataForIconURLQuery(SQLDatabase& db, const String& iconURL)
+void IconDatabase::setIconURLForPageURLInSQLDatabase(const String& iconURL, const String& pageURL)
{
- RefPtr<SharedBuffer> imageData;
+ ASSERT_ICON_SYNC_THREAD();
- readySQLStatement(m_imageDataForIconURLStatement, db, "SELECT Icon.data FROM Icon WHERE Icon.url = (?);");
- m_imageDataForIconURLStatement->bindText16(1, iconURL, false);
-
- int result = m_imageDataForIconURLStatement->step();
- if (result == SQLResultRow) {
- Vector<char> data;
- m_imageDataForIconURLStatement->getColumnBlobAsVector(0, data);
- imageData = new SharedBuffer;
- imageData->append(data.data(), data.size());
- } else if (result != SQLResultDone)
- LOG_ERROR("imageDataForIconURLQuery failed");
+ int64_t iconID = getIconIDForIconURLFromSQLDatabase(iconURL);
- m_imageDataForIconURLStatement->reset();
+ if (!iconID)
+ iconID = addIconURLToSQLDatabase(iconURL);
- return imageData.release();
+ if (!iconID) {
+ LOG_ERROR("Failed to establish an ID for iconURL %s", urlForLogging(iconURL).utf8().data());
+ ASSERT(false);
+ return;
+ }
+
+ setIconIDForPageURLInSQLDatabase(iconID, pageURL);
}
-int IconDatabase::timeStampForIconURLQuery(SQLDatabase& db, const String& iconURL)
+void IconDatabase::setIconIDForPageURLInSQLDatabase(int64_t iconID, const String& pageURL)
{
- readySQLStatement(m_timeStampForIconURLStatement, db, "SELECT Icon.stamp FROM Icon WHERE Icon.url = (?);");
- m_timeStampForIconURLStatement->bindText16(1, iconURL, false);
+ ASSERT_ICON_SYNC_THREAD();
+
+ readySQLStatement(m_setIconIDForPageURLStatement, m_syncDB, "INSERT INTO PageURL (url, iconID) VALUES ((?), ?);");
+ m_setIconIDForPageURLStatement->bindText16(1, pageURL, false);
+ m_setIconIDForPageURLStatement->bindInt64(2, iconID);
- int result = m_timeStampForIconURLStatement->step();
- if (result == SQLResultRow)
- result = m_timeStampForIconURLStatement->getColumnInt(0);
- else {
- if (result != SQLResultDone)
- LOG_ERROR("timeStampForIconURLQuery failed");
- result = 0;
+ int result = m_setIconIDForPageURLStatement->step();
+ if (result != SQLResultDone) {
+ ASSERT(false);
+ LOG_ERROR("setIconIDForPageURLQuery failed for url %s", urlForLogging(pageURL).utf8().data());
}
- m_timeStampForIconURLStatement->reset();
- return result;
+ m_setIconIDForPageURLStatement->reset();
}
-String IconDatabase::iconURLForPageURLQuery(SQLDatabase& db, const String& pageURL)
+void IconDatabase::removePageURLFromSQLDatabase(const String& pageURL)
{
- readySQLStatement(m_iconURLForPageURLStatement, db, "SELECT Icon.url FROM Icon, PageURL WHERE PageURL.url = (?) AND Icon.iconID = PageURL.iconID;");
- m_iconURLForPageURLStatement->bindText16(1, pageURL, false);
+ ASSERT_ICON_SYNC_THREAD();
- int result = m_iconURLForPageURLStatement->step();
- String iconURL;
- if (result == SQLResultRow)
- iconURL = m_iconURLForPageURLStatement->getColumnText16(0);
- else if (result != SQLResultDone)
- LOG_ERROR("iconURLForPageURLQuery failed");
-
- m_iconURLForPageURLStatement->reset();
- return iconURL;
-}
-
-void IconDatabase::forgetPageURLQuery(SQLDatabase& db, const String& pageURL)
-{
- readySQLStatement(m_forgetPageURLStatement, db, "DELETE FROM PageURL WHERE url = (?);");
- m_forgetPageURLStatement->bindText16(1, pageURL, false);
+ readySQLStatement(m_removePageURLStatement, m_syncDB, "DELETE FROM PageURL WHERE url = (?);");
+ m_removePageURLStatement->bindText16(1, pageURL, false);
- if (m_forgetPageURLStatement->step() != SQLResultDone)
- LOG_ERROR("forgetPageURLQuery failed");
+ if (m_removePageURLStatement->step() != SQLResultDone)
+ LOG_ERROR("removePageURLFromSQLDatabase failed for url %s", urlForLogging(pageURL).utf8().data());
- m_forgetPageURLStatement->reset();
+ m_removePageURLStatement->reset();
}
-void IconDatabase::setIconIDForPageURLQuery(SQLDatabase& db, int64_t iconID, const String& pageURL)
-{
- readySQLStatement(m_setIconIDForPageURLStatement, db, "INSERT INTO PageURL (url, iconID) VALUES ((?), ?);");
- m_setIconIDForPageURLStatement->bindText16(1, pageURL, false);
- m_setIconIDForPageURLStatement->bindInt64(2, iconID);
-
- if (m_setIconIDForPageURLStatement->step() != SQLResultDone)
- LOG_ERROR("setIconIDForPageURLQuery failed");
-
- m_setIconIDForPageURLStatement->reset();
-}
-int64_t IconDatabase::getIconIDForIconURLQuery(SQLDatabase& db, const String& iconURL)
+int64_t IconDatabase::getIconIDForIconURLFromSQLDatabase(const String& iconURL)
{
- readySQLStatement(m_getIconIDForIconURLStatement, db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = (?);");
+ ASSERT_ICON_SYNC_THREAD();
+
+ readySQLStatement(m_getIconIDForIconURLStatement, m_syncDB, "SELECT IconInfo.iconID FROM IconInfo WHERE IconInfo.url = (?);");
m_getIconIDForIconURLStatement->bindText16(1, iconURL, false);
int64_t result = m_getIconIDForIconURLStatement->step();
result = m_getIconIDForIconURLStatement->getColumnInt64(0);
else {
if (result != SQLResultDone)
- LOG_ERROR("getIconIDForIconURLQuery failed");
+ LOG_ERROR("getIconIDForIconURLFromSQLDatabase failed for url %s", urlForLogging(iconURL).utf8().data());
result = 0;
}
return result;
}
-int64_t IconDatabase::addIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
+int64_t IconDatabase::addIconURLToSQLDatabase(const String& iconURL)
{
- readySQLStatement(m_addIconForIconURLStatement, db, "INSERT INTO Icon (url) VALUES ((?));");
- m_addIconForIconURLStatement->bindText16(1, iconURL, false);
+ ASSERT_ICON_SYNC_THREAD();
- int64_t result = m_addIconForIconURLStatement->step();
- if (result == SQLResultDone)
- result = db.lastInsertRowID();
- else {
- LOG_ERROR("addIconForIconURLQuery failed");
- result = 0;
+ // There would be a transaction here to make sure these two inserts are atomic
+ // In practice the only caller of this method is always wrapped in a transaction itself so placing another
+ // here is unnecessary
+
+ readySQLStatement(m_addIconToIconInfoStatement, m_syncDB, "INSERT INTO IconInfo (url, stamp) VALUES (?, 0);");
+ m_addIconToIconInfoStatement->bindText16(1, iconURL);
+
+ int result = m_addIconToIconInfoStatement->step();
+ m_addIconToIconInfoStatement->reset();
+ if (result != SQLResultDone) {
+ LOG_ERROR("addIconURLToSQLDatabase failed to insert %s into IconInfo", urlForLogging(iconURL).utf8().data());
+ return 0;
}
-
- m_addIconForIconURLStatement->reset();
- return result;
+ int64_t iconID = m_syncDB.lastInsertRowID();
+
+ readySQLStatement(m_addIconToIconDataStatement, m_syncDB, "INSERT INTO IconData (iconID, data) VALUES (?, ?);");
+ m_addIconToIconDataStatement->bindInt64(1, iconID);
+
+ result = m_addIconToIconDataStatement->step();
+ m_addIconToIconDataStatement->reset();
+ if (result != SQLResultDone) {
+ LOG_ERROR("addIconURLToSQLDatabase failed to insert %s into IconData", urlForLogging(iconURL).utf8().data());
+ return 0;
+ }
+
+ return iconID;
}
-bool IconDatabase::hasIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
+PassRefPtr<SharedBuffer> IconDatabase::getImageDataForIconURLFromSQLDatabase(const String& iconURL)
{
- readySQLStatement(m_hasIconForIconURLStatement, db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = (?);");
- m_hasIconForIconURLStatement->bindText16(1, iconURL, false);
-
- int result = m_hasIconForIconURLStatement->step();
-
- if (result != SQLResultRow && result != SQLResultDone)
- LOG_ERROR("hasIconForIconURLQuery failed");
+ ASSERT_ICON_SYNC_THREAD();
+
+ RefPtr<SharedBuffer> imageData;
+
+ readySQLStatement(m_getImageDataForIconURLStatement, m_syncDB, "SELECT IconData.data FROM IconData WHERE IconData.iconID IN (SELECT iconID FROM IconInfo WHERE IconInfo.url = (?));");
+ m_getImageDataForIconURLStatement->bindText16(1, iconURL, false);
+
+ int result = m_getImageDataForIconURLStatement->step();
+ if (result == SQLResultRow) {
+ Vector<char> data;
+ m_getImageDataForIconURLStatement->getColumnBlobAsVector(0, data);
+ imageData = new SharedBuffer;
+ imageData->append(data.data(), data.size());
+ } else if (result != SQLResultDone)
+ LOG_ERROR("getImageDataForIconURLFromSQLDatabase failed for url %s", urlForLogging(iconURL).utf8().data());
- m_hasIconForIconURLStatement->reset();
- return result == SQLResultRow;
+ m_getImageDataForIconURLStatement->reset();
+
+ return imageData.release();
}
-bool IconDatabase::importedQuery(SQLDatabase& db)
+void IconDatabase::removeIconFromSQLDatabase(const String& iconURL)
{
- readySQLStatement(m_importedStatement, db, "SELECT IconDatabaseInfo.value FROM IconDatabaseInfo WHERE IconDatabaseInfo.key = \"ImportedSafari2Icons\";");
+ ASSERT_ICON_SYNC_THREAD();
- int result = m_importedStatement->step();
+ if (iconURL.isEmpty())
+ return;
- if (result == SQLResultRow)
- result = m_importedStatement->getColumnInt(0);
- else {
- if (result != SQLResultDone)
- LOG_ERROR("importedQuery failed");
- result = 0;
+ // There would be a transaction here to make sure these removals are atomic
+ // In practice the only caller of this method is always wrapped in a transaction itself so placing another here is unnecessary
+
+ int64_t iconID = getIconIDForIconURLFromSQLDatabase(iconURL);
+ ASSERT(iconID);
+ if (!iconID) {
+ LOG_ERROR("Unable to get iconID for icon URL %s to delete it from the database", urlForLogging(iconURL).utf8().data());
+ return;
}
-
- m_importedStatement->reset();
- return result;
-}
-
-void IconDatabase::setImportedQuery(SQLDatabase& db, bool imported)
-{
- if (imported)
- readySQLStatement(m_setImportedStatement, db, "INSERT INTO IconDatabaseInfo (key, value) VALUES (\"ImportedSafari2Icons\", 1);");
- else
- readySQLStatement(m_setImportedStatement, db, "INSERT INTO IconDatabaseInfo (key, value) VALUES (\"ImportedSafari2Icons\", 0);");
-
- int result = m_setImportedStatement->step();
-
- if (result != SQLResultDone)
- LOG_ERROR("setImportedQuery failed");
-
- m_setImportedStatement->reset();
+
+ readySQLStatement(m_deletePageURLsForIconURLStatement, m_syncDB, "DELETE FROM PageURL WHERE PageURL.iconID = (?);");
+ m_deletePageURLsForIconURLStatement->bindInt64(1, iconID);
+
+ if (m_deletePageURLsForIconURLStatement->step() != SQLResultDone)
+ LOG_ERROR("m_deletePageURLsForIconURLStatement failed for url %s", urlForLogging(iconURL).utf8().data());
+
+ readySQLStatement(m_deleteIconFromIconInfoStatement, m_syncDB, "DELETE FROM IconInfo WHERE IconInfo.iconID = (?);");
+ m_deleteIconFromIconInfoStatement->bindInt64(1, iconID);
+
+ if (m_deleteIconFromIconInfoStatement->step() != SQLResultDone)
+ LOG_ERROR("m_deleteIconFromIconInfoStatement failed for url %s", urlForLogging(iconURL).utf8().data());
+
+ readySQLStatement(m_deleteIconFromIconDataStatement, m_syncDB, "DELETE FROM IconData WHERE IconData.iconID = (?);");
+ m_deleteIconFromIconDataStatement->bindInt64(1, iconID);
+
+ if (m_deleteIconFromIconDataStatement->step() != SQLResultDone)
+ LOG_ERROR("m_deleteIconFromIconDataStatement failed for url %s", urlForLogging(iconURL).utf8().data());
+
+ m_deletePageURLsForIconURLStatement->reset();
+ m_deleteIconFromIconInfoStatement->reset();
+ m_deleteIconFromIconDataStatement->reset();
}
-bool IconDatabase::checkIntegrity()
+void IconDatabase::writeIconSnapshotToSQLDatabase(const IconSnapshot& snapshot)
{
- SQLStatement integrity(m_mainDB, "PRAGMA integrity_check;");
- if (integrity.prepare() != SQLResultOk) {
- LOG_ERROR("checkIntegrity failed to execute");
- return false;
- }
+ ASSERT_ICON_SYNC_THREAD();
- int resultCode = integrity.step();
- if (resultCode == SQLResultOk)
- return true;
+ if (snapshot.iconURL.isEmpty())
+ return;
- if (resultCode != SQLResultRow)
- return false;
-
- int columns = integrity.columnCount();
- if (columns != 1) {
- LOG_ERROR("Received %i columns performing integrity check, should be 1", columns);
- return false;
+ // A nulled out timestamp and data means this icon is destined to be deleted - do that instead of writing it out
+ if (!snapshot.timestamp && !snapshot.data) {
+ LOG(IconDatabase, "Removing %s from on-disk database", urlForLogging(snapshot.iconURL).utf8().data());
+ removeIconFromSQLDatabase(snapshot.iconURL);
+ return;
}
+
+ // There would be a transaction here to make sure these removals are atomic
+ // In practice the only caller of this method is always wrapped in a transaction itself so placing another here is unnecessary
- String resultText = integrity.getColumnText16(0);
+ // Get the iconID for this url
+ int64_t iconID = getIconIDForIconURLFromSQLDatabase(snapshot.iconURL);
+
+ // If there is already an iconID in place, update the database.
+ // Otherwise, insert new records
+ if (iconID) {
+ readySQLStatement(m_updateIconInfoStatement, m_syncDB, "UPDATE IconInfo SET stamp = ?, url = ? WHERE iconID = ?;");
+ m_updateIconInfoStatement->bindInt64(1, snapshot.timestamp);
+ m_updateIconInfoStatement->bindText16(2, snapshot.iconURL);
+ m_updateIconInfoStatement->bindInt64(3, iconID);
+
+ if (m_updateIconInfoStatement->step() != SQLResultDone)
+ LOG_ERROR("Failed to update icon info for url %s", urlForLogging(snapshot.iconURL).utf8().data());
- // A successful, no-error integrity check will be "ok" - all other strings imply failure
- if (resultText == "ok")
- return true;
-
- LOG_ERROR("Icon database integrity check failed - \n%s", resultText.ascii().data());
- return false;
-}
+ m_updateIconInfoStatement->reset();
+
+ readySQLStatement(m_updateIconDataStatement, m_syncDB, "UPDATE IconData SET data = ? WHERE iconID = ?;");
+ m_updateIconDataStatement->bindInt64(2, iconID);
+
+ // If we *have* image data, bind it to this statement - Otherwise the DB will get "null" for the blob data,
+ // signifying that this icon doesn't have any data
+ if (snapshot.data && snapshot.data->size())
+ m_updateIconDataStatement->bindBlob(1, snapshot.data->data(), snapshot.data->size());
+
+ if (m_updateIconDataStatement->step() != SQLResultDone)
+ LOG_ERROR("Failed to update icon data for url %s", urlForLogging(snapshot.iconURL).utf8().data());
-size_t IconDatabase::pageURLMappingCount() const
-{
- return 0;
-}
+ m_updateIconDataStatement->reset();
+ } else {
+ readySQLStatement(m_setIconInfoStatement, m_syncDB, "INSERT INTO IconInfo (url,stamp) VALUES (?, ?);");
+ m_setIconInfoStatement->bindText16(1, snapshot.iconURL);
+ m_setIconInfoStatement->bindInt64(2, snapshot.timestamp);
-size_t IconDatabase::retainedPageURLCount() const
-{
- return 0;
-}
+ if (m_setIconInfoStatement->step() != SQLResultDone)
+ LOG_ERROR("Failed to set icon info for url %s", urlForLogging(snapshot.iconURL).utf8().data());
+
+ m_setIconInfoStatement->reset();
+
+ int64_t iconID = m_syncDB.lastInsertRowID();
-size_t IconDatabase::iconRecordCount() const
-{
- return 0;
-}
+ readySQLStatement(m_setIconDataStatement, m_syncDB, "INSERT INTO IconData (iconID, data) VALUES (?, ?);");
+ m_setIconDataStatement->bindInt64(1, iconID);
-size_t IconDatabase::iconRecordCountWithData() const
-{
- return 0;
+ // If we *have* image data, bind it to this statement - Otherwise the DB will get "null" for the blob data,
+ // signifying that this icon doesn't have any data
+ if (snapshot.data && snapshot.data->size())
+ m_setIconDataStatement->bindBlob(2, snapshot.data->data(), snapshot.data->size());
+
+ if (m_setIconDataStatement->step() != SQLResultDone)
+ LOG_ERROR("Failed to set icon data for url %s", urlForLogging(snapshot.iconURL).utf8().data());
+
+ m_setIconDataStatement->reset();
+ }
}
} // namespace WebCore