From 53e5ee9ab302389c142ae0819991b72e09d6e2b0 Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Tue, 19 Oct 2010 10:58:33 -0700 Subject: [PATCH] Bug 596377 - 'IndexedDB: Move usage and clearing IO off the main thread'. r=sicking. --- browser/base/content/pageinfo/permissions.js | 51 +- dom/indexedDB/IDBDatabase.cpp | 26 +- dom/indexedDB/IDBDatabase.h | 2 - dom/indexedDB/IDBFactory.cpp | 4 +- dom/indexedDB/IndexedDatabaseManager.cpp | 530 ++++++++++++++----- dom/indexedDB/IndexedDatabaseManager.h | 85 ++- dom/indexedDB/Makefile.in | 1 + dom/indexedDB/TransactionThreadPool.cpp | 90 ++-- dom/indexedDB/TransactionThreadPool.h | 42 +- dom/indexedDB/nsIIndexedDatabaseManager.idl | 44 +- layout/build/nsLayoutModule.cpp | 2 +- 11 files changed, 667 insertions(+), 210 deletions(-) diff --git a/browser/base/content/pageinfo/permissions.js b/browser/base/content/pageinfo/permissions.js index e0f03127cf8b..9fb551b0c8e2 100644 --- a/browser/base/content/pageinfo/permissions.js +++ b/browser/base/content/pageinfo/permissions.js @@ -125,6 +125,10 @@ function onUnloadPermission() var os = Components.classes["@mozilla.org/observer-service;1"] .getService(Components.interfaces.nsIObserverService); os.removeObserver(permissionObserver, "perm-changed"); + + var dbManager = Components.classes["@mozilla.org/dom/indexeddb/manager;1"] + .getService(nsIIndexedDatabaseManager); + dbManager.cancelGetUsageForURI(gPermURI, onIndexedDBUsageCallback); } function initRow(aPartId) @@ -195,27 +199,16 @@ function setRadioState(aPartId, aValue) function initIndexedDBRow() { + var dbManager = Components.classes["@mozilla.org/dom/indexeddb/manager;1"] + .getService(nsIIndexedDatabaseManager); + dbManager.getUsageForURI(gPermURI, onIndexedDBUsageCallback); + var status = document.getElementById("indexedDBStatus"); var button = document.getElementById("indexedDBClear"); - var usage = Components.classes["@mozilla.org/dom/indexeddb/manager;1"] - .getService(nsIIndexedDatabaseManager) - .getUsageForURI(gPermURI); - if (usage) { - if (!("DownloadUtils" in window)) { - Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); - } - status.value = - gBundle.getFormattedString("indexedDBUsage", - DownloadUtils.convertByteUnits(usage)); - status.removeAttribute("hidden"); - button.removeAttribute("hidden"); - } - else { - status.value = ""; - status.setAttribute("hidden", "true"); - button.setAttribute("hidden", "true"); - } + status.value = ""; + status.setAttribute("hidden", "true"); + button.setAttribute("hidden", "true"); } function onIndexedDBClear() @@ -230,3 +223,25 @@ function onIndexedDBClear() permissionManager.remove(gPermURI.host, "indexedDB-unlimited"); initIndexedDBRow(); } + +function onIndexedDBUsageCallback(uri, usage) +{ + if (!uri.equals(gPermURI)) { + throw new Error("Callback received for bad URI: " + uri); + } + + if (usage) { + if (!("DownloadUtils" in window)) { + Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); + } + + var status = document.getElementById("indexedDBStatus"); + var button = document.getElementById("indexedDBClear"); + + status.value = + gBundle.getFormattedString("indexedDBUsage", + DownloadUtils.convertByteUnits(usage)); + status.removeAttribute("hidden"); + button.removeAttribute("hidden"); + } +} diff --git a/dom/indexedDB/IDBDatabase.cpp b/dom/indexedDB/IDBDatabase.cpp index 2f4604c50047..45847ffcb627 100644 --- a/dom/indexedDB/IDBDatabase.cpp +++ b/dom/indexedDB/IDBDatabase.cpp @@ -250,7 +250,7 @@ IDBDatabase::Create(nsIScriptContext* aScriptContext, db->mConnection.swap(aConnection); - IndexedDatabaseManager* mgr = IndexedDatabaseManager::GetInstance(); + IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); NS_ASSERTION(mgr, "This should never be null!"); if (!mgr->RegisterDatabase(db)) { @@ -276,7 +276,7 @@ IDBDatabase::IDBDatabase() IDBDatabase::~IDBDatabase() { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); - IndexedDatabaseManager* mgr = IndexedDatabaseManager::GetInstance(); + IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); if (mgr) { mgr->UnregisterDatabase(this); } @@ -287,7 +287,7 @@ IDBDatabase::~IDBDatabase() CloseConnection(); - if (mDatabaseId) { + if (mDatabaseId && !mInvalidated) { DatabaseInfo* info; if (!DatabaseInfo::Get(mDatabaseId, &info)) { NS_ERROR("This should never fail!"); @@ -398,6 +398,16 @@ IDBDatabase::Invalidate() NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); PR_AtomicSet(&mInvalidated, 1); CloseConnection(); + + DatabaseInfo* info; + if (!DatabaseInfo::Get(mDatabaseId, &info)) { + NS_ERROR("This should never fail!"); + } + + NS_ASSERTION(info->referenceCount, "Bad reference count!"); + if (--info->referenceCount == 0) { + DatabaseInfo::Remove(mDatabaseId); + } } bool @@ -406,16 +416,6 @@ IDBDatabase::IsInvalidated() return !!mInvalidated; } -void -IDBDatabase::WaitForConnectionReleased() -{ - NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); - TransactionThreadPool* threadPool = TransactionThreadPool::Get(); - if (threadPool) { - threadPool->WaitForAllTransactionsToComplete(this); - } -} - NS_IMPL_CYCLE_COLLECTION_CLASS(IDBDatabase) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBDatabase, diff --git a/dom/indexedDB/IDBDatabase.h b/dom/indexedDB/IDBDatabase.h index 04a7efa02f34..4a2c76fe3b26 100644 --- a/dom/indexedDB/IDBDatabase.h +++ b/dom/indexedDB/IDBDatabase.h @@ -122,8 +122,6 @@ public: void Invalidate(); bool IsInvalidated(); - void WaitForConnectionReleased(); - private: IDBDatabase(); ~IDBDatabase(); diff --git a/dom/indexedDB/IDBFactory.cpp b/dom/indexedDB/IDBFactory.cpp index 194b3c461405..b5890b9b05e6 100644 --- a/dom/indexedDB/IDBFactory.cpp +++ b/dom/indexedDB/IDBFactory.cpp @@ -727,9 +727,7 @@ IDBFactory::Open(const nsAString& aName, nsRefPtr permissionHelper = new CheckPermissionsHelper(openHelper, thread, innerWindow, origin); - nsRefPtr mgr = - already_AddRefed( - IndexedDatabaseManager::GetOrCreateInstance()); + nsRefPtr mgr = IndexedDatabaseManager::GetOrCreate(); NS_ENSURE_TRUE(mgr, NS_ERROR_FAILURE); rv = mgr->WaitForClearAndDispatch(origin, permissionHelper); diff --git a/dom/indexedDB/IndexedDatabaseManager.cpp b/dom/indexedDB/IndexedDatabaseManager.cpp index eef8cec370e0..b6057d37bc2d 100644 --- a/dom/indexedDB/IndexedDatabaseManager.cpp +++ b/dom/indexedDB/IndexedDatabaseManager.cpp @@ -42,14 +42,26 @@ #include "nsIFile.h" #include "nsIObserverService.h" #include "nsISimpleEnumerator.h" +#include "nsITimer.h" #include "mozilla/Services.h" #include "nsContentUtils.h" #include "nsThreadUtils.h" #include "nsXPCOM.h" +#include "nsXPCOMPrivate.h" #include "IDBDatabase.h" #include "IDBFactory.h" +#include "LazyIdleThread.h" +#include "TransactionThreadPool.h" + +// The amount of time, in milliseconds, that our IO thread will stay alive +// after the last event it processes. +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +// The amount of time, in milliseconds, that we will wait for active database +// transactions on shutdown before aborting them. +#define DEFAULT_SHUTDOWN_TIMER_MS 30000 USING_INDEXEDDB_NAMESPACE using namespace mozilla::services; @@ -58,7 +70,7 @@ namespace { bool gShutdown = false; -// Holds a reference! +// Does not hold a reference. IndexedDatabaseManager* gInstance = nsnull; // Adds all databases in the hash to the given array. @@ -72,8 +84,8 @@ EnumerateToTArray(const nsACString& aKey, NS_ASSERTION(aValue, "Null pointer!"); NS_ASSERTION(aUserArg, "Null pointer!"); - nsTArray >* array = - static_cast >* >(aUserArg); + nsTArray* array = + static_cast*>(aUserArg); if (!array->AppendElements(*aValue)) { NS_WARNING("Out of memory!"); @@ -98,10 +110,9 @@ IndexedDatabaseManager::~IndexedDatabaseManager() gInstance = nsnull; } -// Returns a raw pointer that carries an owning reference! Lame, but the -// singleton factory macros force this. -IndexedDatabaseManager* -IndexedDatabaseManager::GetOrCreateInstance() +// static +already_AddRefed +IndexedDatabaseManager::GetOrCreate() { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); @@ -110,32 +121,62 @@ IndexedDatabaseManager::GetOrCreateInstance() return nsnull; } - if (!gInstance) { - nsRefPtr instance(new IndexedDatabaseManager()); + nsRefPtr instance(gInstance); + + if (!instance) { + instance = new IndexedDatabaseManager(); if (!instance->mLiveDatabases.Init()) { NS_WARNING("Out of memory!"); return nsnull; } - // We need to know when to release the singleton. + // Make a timer here to avoid potential failures later. We don't actually + // initialize the timer until shutdown. + instance->mShutdownTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + NS_ENSURE_TRUE(instance->mShutdownTimer, nsnull); + nsCOMPtr obs = GetObserverService(); + NS_ENSURE_TRUE(obs, nsnull); + + // We need this callback to know when to shut down all our threads. nsresult rv = obs->AddObserver(instance, NS_XPCOM_SHUTDOWN_OBSERVER_ID, PR_FALSE); NS_ENSURE_SUCCESS(rv, nsnull); - instance.forget(&gInstance); + // We don't really need this callback but we want the observer service to + // hold us alive until XPCOM shutdown. That way other consumers can continue + // to use this service until shutdown. + rv = obs->AddObserver(instance, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, + PR_FALSE); + NS_ENSURE_SUCCESS(rv, nsnull); + + // Make a lazy thread for any IO we need (like clearing or enumerating the + // contents of indexedDB database directories). + instance->mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS); + + // The observer service will hold our last reference, don't AddRef here. + gInstance = instance; } - NS_IF_ADDREF(gInstance); + return instance.forget(); +} + +// static +IndexedDatabaseManager* +IndexedDatabaseManager::Get() +{ + // Does not return an owning reference. return gInstance; } -// Does not return an owning reference. +// static IndexedDatabaseManager* -IndexedDatabaseManager::GetInstance() +IndexedDatabaseManager::FactoryCreate() { - return gInstance; + // Returns a raw pointer that carries an owning reference! Lame, but the + // singleton factory macros force this. + return GetOrCreate().get(); } // Called when an IDBDatabase is constructed. @@ -187,6 +228,38 @@ IndexedDatabaseManager::UnregisterDatabase(IDBDatabase* aDatabase) NS_ERROR("Didn't know anything about this database!"); } +// Called when OriginClearRunnable has finished its Run() method. +void +IndexedDatabaseManager::OnOriginClearComplete(OriginClearRunnable* aRunnable) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aRunnable, "Null pointer!"); + NS_ASSERTION(!aRunnable->mThread, "Thread should be null!"); + NS_ASSERTION(aRunnable->mDatabasesWaiting.IsEmpty(), "Databases waiting?!"); + NS_ASSERTION(aRunnable->mDelayedRunnables.IsEmpty(), + "Delayed runnables should have been dispatched already!"); + + if (!mOriginClearRunnables.RemoveElement(aRunnable)) { + NS_ERROR("Don't know anything about this runnable!"); + } +} + +// Called when AsyncUsageRunnable has finished its Run() method. +void +IndexedDatabaseManager::OnUsageCheckComplete(AsyncUsageRunnable* aRunnable) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aRunnable, "Null pointer!"); + NS_ASSERTION(!aRunnable->mURI, "Should have been cleared!"); + NS_ASSERTION(!aRunnable->mCallback, "Should have been cleared!"); + + if (!mUsageRunnables.RemoveElement(aRunnable)) { + NS_ERROR("Don't know anything about this runnable!"); + } +} + +// Waits until it is safe for a new database to be created with the given origin +// before dispatching the given runnable. nsresult IndexedDatabaseManager::WaitForClearAndDispatch(const nsACString& aOrigin, nsIRunnable* aRunnable) @@ -197,12 +270,12 @@ IndexedDatabaseManager::WaitForClearAndDispatch(const nsACString& aOrigin, // See if we're currently clearing database files for this origin. If so then // queue the runnable for later dispatch after we're done clearing. - PRUint32 count = mOriginClearData.Length(); + PRUint32 count = mOriginClearRunnables.Length(); for (PRUint32 index = 0; index < count; index++) { - OriginClearData& data = mOriginClearData[index]; - if (data.origin == aOrigin) { + nsRefPtr& data = mOriginClearRunnables[index]; + if (data->mOrigin == aOrigin) { nsCOMPtr* newPtr = - data.delayedRunnables.AppendElement(aRunnable); + data->mDelayedRunnables.AppendElement(aRunnable); NS_ENSURE_TRUE(newPtr, NS_ERROR_OUT_OF_MEMORY); return NS_OK; @@ -218,71 +291,76 @@ NS_IMPL_ISUPPORTS2(IndexedDatabaseManager, nsIIndexedDatabaseManager, nsIObserver) NS_IMETHODIMP -IndexedDatabaseManager::GetUsageForURI(nsIURI* aURI, - PRUint64* _retval) +IndexedDatabaseManager::GetUsageForURI( + nsIURI* aURI, + nsIIndexedDatabaseUsageCallback* aCallback) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aCallback); // Figure out which origin we're dealing with. nsCString origin; nsresult rv = nsContentUtils::GetASCIIOrigin(aURI, origin); NS_ENSURE_SUCCESS(rv, rv); - // Non-standard URIs can't create databases anyway, so return 0. + nsRefPtr runnable = + new AsyncUsageRunnable(aURI, origin, aCallback); + + nsRefPtr* newRunnable = + mUsageRunnables.AppendElement(runnable); + NS_ENSURE_TRUE(newRunnable, NS_ERROR_OUT_OF_MEMORY); + + // Non-standard URIs can't create databases anyway so fire the callback + // immediately. if (origin.EqualsLiteral("null")) { - *_retval = 0; + rv = NS_DispatchToCurrentThread(runnable); + NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } - // Get the directory where we may be storing database files for this origin. - nsCOMPtr directory; - rv = IDBFactory::GetDirectoryForOrigin(origin, getter_AddRefs(directory)); - NS_ENSURE_SUCCESS(rv, rv); - - PRBool exists; - rv = directory->Exists(&exists); - NS_ENSURE_SUCCESS(rv, rv); - - PRUint64 usage = 0; - - // If the directory exists then enumerate all the files inside, adding up the - // sizes to get the final usage statistic. - if (exists) { - nsCOMPtr entries; - rv = directory->GetDirectoryEntries(getter_AddRefs(entries)); - NS_ENSURE_SUCCESS(rv, rv); - - if (entries) { - PRBool hasMore; - while (NS_SUCCEEDED(entries->HasMoreElements(&hasMore)) && hasMore) { - nsCOMPtr entry; - rv = entries->GetNext(getter_AddRefs(entry)); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr file(do_QueryInterface(entry)); - NS_ASSERTION(file, "Don't know what this is!"); - - PRInt64 fileSize; - rv = file->GetFileSize(&fileSize); - NS_ENSURE_SUCCESS(rv, rv); - - NS_ASSERTION(fileSize > 0, "Negative size?!"); - - // Watch for overflow! - if (NS_UNLIKELY((LL_MAXINT - usage) <= PRUint64(fileSize))) { - NS_WARNING("Database sizes exceed max we can report!"); - usage = LL_MAXINT; - } - else { - usage += fileSize; - } - } + // See if we're currently clearing the databases for this origin. If so then + // we pretend that we've already deleted everything. + for (PRUint32 index = 0; index < mOriginClearRunnables.Length(); index++) { + if (mOriginClearRunnables[index]->mOrigin == origin) { + rv = NS_DispatchToCurrentThread(runnable); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; } } - *_retval = usage; + // Otherwise dispatch to the IO thread to actually compute the usage. + rv = mIOThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +IndexedDatabaseManager::CancelGetUsageForURI( + nsIURI* aURI, + nsIIndexedDatabaseUsageCallback* aCallback) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aCallback); + + // See if one of our pending callbacks matches both the URI and the callback + // given. Cancel an remove it if so. + for (PRUint32 index = 0; index < mUsageRunnables.Length(); index++) { + nsRefPtr& runnable = mUsageRunnables[index]; + + PRBool equals; + nsresult rv = runnable->mURI->Equals(aURI, &equals); + NS_ENSURE_SUCCESS(rv, rv); + + if (equals && SameCOMIdentity(aCallback, runnable->mCallback)) { + runnable->Cancel(); + break; + } + } return NS_OK; } @@ -305,9 +383,9 @@ IndexedDatabaseManager::ClearDatabasesForURI(nsIURI* aURI) // If we're already clearing out files for this origin then return // immediately. - PRUint32 clearDataCount = mOriginClearData.Length(); + PRUint32 clearDataCount = mOriginClearRunnables.Length(); for (PRUint32 index = 0; index < clearDataCount; index++) { - if (mOriginClearData[index].origin == origin) { + if (mOriginClearRunnables[index]->mOrigin == origin) { return NS_OK; } } @@ -325,61 +403,37 @@ IndexedDatabaseManager::ClearDatabasesForURI(nsIURI* aURI) } } - // Make a new entry for this origin in mOriginClearData. Don't return early - // while mOriginClearData has this origin in it! - OriginClearData* data = mOriginClearData.AppendElement(); - NS_ENSURE_TRUE(data, NS_ERROR_OUT_OF_MEMORY); + nsRefPtr runnable = + new OriginClearRunnable(origin, mIOThread, liveDatabases); - data->origin = origin; + NS_ASSERTION(liveDatabases.IsEmpty(), "Should have swapped!"); - if (!liveDatabases.IsEmpty()) { - PRUint32 count = liveDatabases.Length(); + // Make a new entry for this origin in mOriginClearRunnables. + nsRefPtr* newRunnable = + mOriginClearRunnables.AppendElement(runnable); + NS_ENSURE_TRUE(newRunnable, NS_ERROR_OUT_OF_MEMORY); + + if (!runnable->mDatabasesWaiting.IsEmpty()) { + PRUint32 count = runnable->mDatabasesWaiting.Length(); // Invalidate all the live databases first. for (PRUint32 index = 0; index < count; index++) { - liveDatabases[index]->Invalidate(); + runnable->mDatabasesWaiting[index]->Invalidate(); } - // Now wait for them to finish. + // Now set up our callbacks so that we know when they have finished. + TransactionThreadPool* pool = TransactionThreadPool::Get(); for (PRUint32 index = 0; index < count; index++) { - liveDatabases[index]->WaitForConnectionReleased(); - } - } - - // Now that all our databases have released their connections to their files - // we can go ahead and delete the directory. Don't return early while - // mOriginClearData has this origin in it! - nsCOMPtr directory; - rv = IDBFactory::GetDirectoryForOrigin(origin, getter_AddRefs(directory)); - if (NS_SUCCEEDED(rv)) { - PRBool exists; - rv = directory->Exists(&exists); - if (NS_SUCCEEDED(rv) && exists) { - rv = directory->Remove(PR_TRUE); - } - } - - // Remove this origin's entry from mOriginClearData. Dispatch any runnables - // that were queued along the way. - clearDataCount = mOriginClearData.Length(); - for (PRUint32 clearDataIndex = 0; clearDataIndex < clearDataCount; - clearDataIndex++) { - OriginClearData& data = mOriginClearData[clearDataIndex]; - if (data.origin == origin) { - nsTArray >& runnables = data.delayedRunnables; - PRUint32 runnableCount = runnables.Length(); - for (PRUint32 runnableIndex = 0; runnableIndex < runnableCount; - runnableIndex++) { - NS_DispatchToCurrentThread(runnables[runnableIndex]); + if (!pool->WaitForAllTransactionsToComplete( + runnable->mDatabasesWaiting[index], + OriginClearRunnable::DatabaseCompleteCallback, + runnable)) { + NS_WARNING("Out of memory!"); + return NS_ERROR_OUT_OF_MEMORY; } - mOriginClearData.RemoveElementAt(clearDataIndex); - break; } } - // Now we can finally return errors. - NS_ENSURE_SUCCESS(rv, rv); - return NS_OK; } @@ -390,31 +444,265 @@ IndexedDatabaseManager::Observe(nsISupports* aSubject, { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); - if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID)) { // Setting this flag prevents the servic from being recreated and prevents // further databases from being created. gShutdown = true; - // Grab all live databases, for all origins. Need references to keep them - // alive while we wait on them. - nsAutoTArray, 50> liveDatabases; + // Make sure to join with our IO thread. + if (NS_FAILED(mIOThread->Shutdown())) { + NS_WARNING("Failed to shutdown IO thread!"); + } + + // Kick off the shutdown timer. + if (NS_FAILED(mShutdownTimer->Init(this, DEFAULT_SHUTDOWN_TIMER_MS, + nsITimer::TYPE_ONE_SHOT))) { + NS_WARNING("Failed to initialize shutdown timer!"); + } + + // This will spin the event loop while we wait on all the database threads + // to close. Our timer may fire during that loop. + TransactionThreadPool::Shutdown(); + + // Cancel the timer regardless of whether it actually fired. + if (NS_FAILED(mShutdownTimer->Cancel())) { + NS_WARNING("Failed to cancel shutdown timer!"); + } + + return NS_OK; + } + + if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) { + NS_WARNING("Some database operations are taking longer than expected " + "during shutdown and will be aborted!"); + + // Grab all live databases, for all origins. + nsAutoTArray liveDatabases; mLiveDatabases.EnumerateRead(EnumerateToTArray, &liveDatabases); - // Wait for all live databases to finish. + // Invalidate them all. if (!liveDatabases.IsEmpty()) { PRUint32 count = liveDatabases.Length(); for (PRUint32 index = 0; index < count; index++) { - liveDatabases[index]->WaitForConnectionReleased(); + liveDatabases[index]->Invalidate(); } } - mLiveDatabases.Clear(); + return NS_OK; + } - // This may kill us. - gInstance->Release(); + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + // We're dying now. return NS_OK; } NS_NOTREACHED("Unknown topic!"); return NS_ERROR_UNEXPECTED; } + +// Called by the TransactionThreadPool when the given database has completed all +// of its transactions. +void +IndexedDatabaseManager::OriginClearRunnable::OnDatabaseComplete( + IDBDatabase* aDatabase) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aDatabase, "Null pointer!"); + NS_ASSERTION(mThread, "This shouldn't be cleared yet!"); + + // Remove the database from the list of databases that we're waiting on. + if (!mDatabasesWaiting.RemoveElement(aDatabase)) { + NS_ERROR("Don't know anything about this database!"); + } + + // Now dispatch this runnable to the IO thread if the list is empty. + if (mDatabasesWaiting.IsEmpty()) { + if (NS_FAILED(mThread->Dispatch(this, NS_DISPATCH_NORMAL))) { + NS_WARNING("Can't dispatch to IO thread!"); + } + + // We no longer need to keep the thread alive. + mThread = nsnull; + } +} + +NS_IMPL_THREADSAFE_ISUPPORTS1(IndexedDatabaseManager::OriginClearRunnable, + nsIRunnable) + +// Runs twice, first on the IO thread, then again on the main thread. While on +// the IO thread the runnable will actually remove the origin's database files +// and the directory that contains them before dispatching itself back to the +// main thread. When on the main thread the runnable will dispatch any queued +// runnables and then notify the IndexedDatabaseManager that the job has been +// completed. +NS_IMETHODIMP +IndexedDatabaseManager::OriginClearRunnable::Run() +{ + if (NS_IsMainThread()) { + NS_ASSERTION(!mThread, "Should have been cleared already!"); + + // Dispatch any queued runnables that we collected while we were waiting. + for (PRUint32 index = 0; index < mDelayedRunnables.Length(); index++) { + if (NS_FAILED(NS_DispatchToCurrentThread(mDelayedRunnables[index]))) { + NS_WARNING("Failed to dispatch delayed runnable!"); + } + } + mDelayedRunnables.Clear(); + + // Tell the IndexedDatabaseManager that we're done. + IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); + if (mgr) { + mgr->OnOriginClearComplete(this); + } + + return NS_OK; + } + + // Remove the directory that contains all our databases. + nsCOMPtr directory; + nsresult rv = IDBFactory::GetDirectoryForOrigin(mOrigin, + getter_AddRefs(directory)); + if (NS_SUCCEEDED(rv)) { + PRBool exists; + rv = directory->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) { + rv = directory->Remove(PR_TRUE); + } + } + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to remove directory!"); + + // Switch back to the main thread to complete the sequence. + rv = NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +IndexedDatabaseManager::AsyncUsageRunnable::AsyncUsageRunnable( + nsIURI* aURI, + const nsACString& aOrigin, + nsIIndexedDatabaseUsageCallback* aCallback) +: mURI(aURI), + mOrigin(aOrigin), + mCallback(aCallback), + mUsage(0), + mCanceled(0) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aURI, "Null pointer!"); + NS_ASSERTION(!aOrigin.IsEmpty(), "Empty origin!"); + NS_ASSERTION(aCallback, "Null pointer!"); +} + +// Sets the canceled flag so that the callback is never called. +void +IndexedDatabaseManager::AsyncUsageRunnable::Cancel() +{ + if (PR_AtomicSet(&mCanceled, 1)) { + NS_ERROR("Canceled more than once?!"); + } +} + +// Runs twice, first on the IO thread, then again on the main thread. While on +// the IO thread the runnable will calculate the size of all files in the +// origin's directory before dispatching itself back to the main thread. When on +// the main thread the runnable will call the callback and then notify the +// IndexedDatabaseManager that the job has been completed. +nsresult +IndexedDatabaseManager::AsyncUsageRunnable::RunInternal() +{ + if (NS_IsMainThread()) { + // Call the callback unless we were canceled. + if (!mCanceled) { + mCallback->OnUsageResult(mURI, mUsage); + } + + // Clean up. + mURI = nsnull; + mCallback = nsnull; + + // And tell the IndexedDatabaseManager that we're done. + IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); + if (mgr) { + mgr->OnUsageCheckComplete(this); + } + + return NS_OK; + } + + if (mCanceled) { + return NS_OK; + } + + // Get the directory that contains all the database files we care about. + nsCOMPtr directory; + nsresult rv = IDBFactory::GetDirectoryForOrigin(mOrigin, + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + PRBool exists; + rv = directory->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + // If the directory exists then enumerate all the files inside, adding up the + // sizes to get the final usage statistic. + if (exists && !mCanceled) { + nsCOMPtr entries; + rv = directory->GetDirectoryEntries(getter_AddRefs(entries)); + NS_ENSURE_SUCCESS(rv, rv); + + if (entries) { + PRBool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && + hasMore && !mCanceled) { + nsCOMPtr entry; + rv = entries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr file(do_QueryInterface(entry)); + NS_ASSERTION(file, "Don't know what this is!"); + + PRInt64 fileSize; + rv = file->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(fileSize > 0, "Negative size?!"); + + // Watch for overflow! + if (NS_UNLIKELY((LL_MAXINT - mUsage) <= PRUint64(fileSize))) { + NS_WARNING("Database sizes exceed max we can report!"); + mUsage = LL_MAXINT; + } + else { + mUsage += fileSize; + } + } + NS_ENSURE_SUCCESS(rv, rv); + } + } + return NS_OK; +} + +NS_IMPL_THREADSAFE_ISUPPORTS1(IndexedDatabaseManager::AsyncUsageRunnable, + nsIRunnable) + +// Calls the RunInternal method and makes sure that we always dispatch to the +// main thread in case of an error. +NS_IMETHODIMP +IndexedDatabaseManager::AsyncUsageRunnable::Run() +{ + nsresult rv = RunInternal(); + + if (!NS_IsMainThread()) { + if (NS_FAILED(rv)) { + mUsage = 0; + } + + if (NS_FAILED(NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL))) { + NS_WARNING("Failed to dispatch to main thread!"); + } + } + + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} diff --git a/dom/indexedDB/IndexedDatabaseManager.h b/dom/indexedDB/IndexedDatabaseManager.h index 28646d633a28..1dfd1f6e684f 100644 --- a/dom/indexedDB/IndexedDatabaseManager.h +++ b/dom/indexedDB/IndexedDatabaseManager.h @@ -41,9 +41,13 @@ #define mozilla_dom_indexeddb_indexeddatabasemanager_h__ #include "mozilla/dom/indexedDB/IndexedDatabase.h" +#include "mozilla/dom/indexedDB/IDBDatabase.h" #include "nsIIndexedDatabaseManager.h" #include "nsIObserver.h" +#include "nsIRunnable.h" +#include "nsIThread.h" +#include "nsIURI.h" #include "nsClassHashtable.h" #include "nsHashKeys.h" @@ -51,23 +55,23 @@ #define INDEXEDDB_MANAGER_CONTRACTID \ "@mozilla.org/dom/indexeddb/manager;1" -class nsIRunnable; +class nsITimer; BEGIN_INDEXEDDB_NAMESPACE -class IDBDatabase; - class IndexedDatabaseManager : public nsIIndexedDatabaseManager, public nsIObserver { friend class IDBDatabase; public: - // Returns an owning reference! - static IndexedDatabaseManager* GetOrCreateInstance(); + static already_AddRefed GetOrCreate(); // Returns a non-owning reference. - static IndexedDatabaseManager* GetInstance(); + static IndexedDatabaseManager* Get(); + + // Returns an owning reference! No one should call this but the factory. + static IndexedDatabaseManager* FactoryCreate(); NS_DECL_ISUPPORTS NS_DECL_NSIINDEXEDDATABASEMANAGER @@ -83,17 +87,78 @@ private: bool RegisterDatabase(IDBDatabase* aDatabase); void UnregisterDatabase(IDBDatabase* aDatabase); - struct OriginClearData + // Responsible for clearing the database files for a particular origin on the + // IO thread. + class OriginClearRunnable : public nsIRunnable { - nsCString origin; - nsTArray > delayedRunnables; + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIRUNNABLE + + static void DatabaseCompleteCallback(IDBDatabase* aDatabase, + void* aClosure) + { + nsRefPtr runnable = + static_cast(aClosure); + runnable->OnDatabaseComplete(aDatabase); + } + + void OnDatabaseComplete(IDBDatabase* aDatabase); + + OriginClearRunnable(const nsACString& aOrigin, + nsIThread* aThread, + nsTArray >& aDatabasesWaiting) + : mOrigin(aOrigin), + mThread(aThread) + { + mDatabasesWaiting.SwapElements(aDatabasesWaiting); + } + + nsCString mOrigin; + nsCOMPtr mThread; + nsTArray > mDatabasesWaiting; + nsTArray > mDelayedRunnables; }; + inline void OnOriginClearComplete(OriginClearRunnable* aRunnable); + + // Responsible for calculating the amount of space taken up by databases of a + // certain origin. Runs on the IO thread. + class AsyncUsageRunnable : public nsIRunnable + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIRUNNABLE + + AsyncUsageRunnable(nsIURI* aURI, + const nsACString& aOrigin, + nsIIndexedDatabaseUsageCallback* aCallback); + + void Cancel(); + + inline nsresult RunInternal(); + + nsCOMPtr mURI; + nsCString mOrigin; + nsCOMPtr mCallback; + PRUint64 mUsage; + PRInt32 mCanceled; + }; + + inline void OnUsageCheckComplete(AsyncUsageRunnable* aRunnable); + // Maintains a list of live databases per origin. nsClassHashtable > mLiveDatabases; // Maintains a list of origins that are currently being cleared. - nsAutoTArray mOriginClearData; + nsAutoTArray, 1> mOriginClearRunnables; + + // Maintains a list of origins that we're currently enumerating to gather + // usage statistics. + nsAutoTArray, 1> mUsageRunnables; + + nsCOMPtr mIOThread; + nsCOMPtr mShutdownTimer; }; END_INDEXEDDB_NAMESPACE diff --git a/dom/indexedDB/Makefile.in b/dom/indexedDB/Makefile.in index bb6e1d989874..ef828c1e9e6a 100644 --- a/dom/indexedDB/Makefile.in +++ b/dom/indexedDB/Makefile.in @@ -86,6 +86,7 @@ EXPORTS_mozilla/dom/indexedDB = \ $(NULL) LOCAL_INCLUDES = \ + -I$(topsrcdir)/xpcom/build \ -I$(topsrcdir)/dom/base \ -I$(topsrcdir)/dom/src/storage \ -I$(topsrcdir)/content/base/src \ diff --git a/dom/indexedDB/TransactionThreadPool.cpp b/dom/indexedDB/TransactionThreadPool.cpp index 43552766781f..98bccb27229e 100644 --- a/dom/indexedDB/TransactionThreadPool.cpp +++ b/dom/indexedDB/TransactionThreadPool.cpp @@ -116,7 +116,8 @@ TransactionThreadPool::TransactionThreadPool() TransactionThreadPool::~TransactionThreadPool() { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); - NS_ASSERTION(!gInstance, "More than one instance!"); + NS_ASSERTION(gInstance == this, "Different instances!"); + gInstance = nsnull; } // static @@ -125,20 +126,12 @@ TransactionThreadPool::GetOrCreate() { if (!gInstance && !gShutdown) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); - nsRefPtr pool(new TransactionThreadPool()); + nsAutoPtr pool(new TransactionThreadPool()); nsresult rv = pool->Init(); NS_ENSURE_SUCCESS(rv, nsnull); - nsCOMPtr obs = - do_GetService(NS_OBSERVERSERVICE_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, nsnull); - - rv = obs->AddObserver(pool, "xpcom-shutdown-threads", PR_FALSE); - NS_ENSURE_SUCCESS(rv, nsnull); - - // The observer service now owns us. - gInstance = pool; + gInstance = pool.forget(); } return gInstance; } @@ -162,6 +155,7 @@ TransactionThreadPool::Shutdown() if (NS_FAILED(gInstance->Cleanup())) { NS_WARNING("Failed to shutdown thread pool!"); } + delete gInstance; gInstance = nsnull; } } @@ -247,6 +241,19 @@ TransactionThreadPool::FinishTransaction(IDBTransaction* aTransaction) } #endif mTransactionsInProgress.Remove(databaseId); + + // See if we need to fire any complete callbacks. + for (PRUint32 index = 0; index < mCompleteRunnables.Length(); index++) { + nsRefPtr& runnable = + mCompleteRunnables[index]; + if (runnable->mDatabase == aTransaction->Database()) { + if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) { + NS_WARNING("Failed to dispatch to current thread?!"); + } + mCompleteRunnables.RemoveElementAt(index); + index--; + } + } } else { // We need to rebuild the locked object store list. @@ -483,37 +490,37 @@ TransactionThreadPool::Dispatch(IDBTransaction* aTransaction, return mThreadPool->Dispatch(transactionInfo->queue, NS_DISPATCH_NORMAL); } -void -TransactionThreadPool::WaitForAllTransactionsToComplete(IDBDatabase* aDatabase) +bool +TransactionThreadPool::WaitForAllTransactionsToComplete( + IDBDatabase* aDatabase, + DatabaseCompleteCallback aCallback, + void* aUserData) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(aDatabase, "Null pointer!"); + NS_ASSERTION(aCallback, "Null pointer!"); + nsRefPtr runnable = + new DatabaseCompleteCallbackRunnable(aDatabase, aCallback, aUserData); + + // See if this database has any active transactions. If not then we can + // dispatch the callback immediately. const PRUint32 databaseId = aDatabase->Id(); - nsIThread* currentThread = NS_GetCurrentThread(); - - // As soon as all of the transactions for this database are complete its - // entry in mTransactionsInProgress will be removed, so just loop while - // checking. - while (mTransactionsInProgress.Get(databaseId, nsnull)) { - if (NS_FAILED(NS_ProcessNextEvent(currentThread, PR_TRUE))) { - NS_WARNING("Failed to process next event?!"); + if (!mTransactionsInProgress.Get(databaseId, nsnull)) { + if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) { + NS_WARNING("Failed to dispatch to current thread?!"); + return false; } + return true; } -} -NS_IMPL_THREADSAFE_ISUPPORTS1(TransactionThreadPool, nsIObserver) + // The database has active transactions, wait and fire the callback later. + if (!mCompleteRunnables.AppendElement(runnable)) { + NS_WARNING("Out of memory!"); + return false; + } -NS_IMETHODIMP -TransactionThreadPool::Observe(nsISupports* /* aSubject */, - const char* aTopic, - const PRUnichar* /* aData */) -{ - NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); - NS_ASSERTION(!strcmp("xpcom-shutdown-threads", aTopic), "Wrong topic!"); - - Shutdown(); - - return NS_OK; + return true; } TransactionThreadPool:: @@ -637,3 +644,18 @@ FinishTransactionRunnable::Run() return NS_OK; } + +NS_IMPL_ISUPPORTS1(TransactionThreadPool::DatabaseCompleteCallbackRunnable, + nsIRunnable) + +NS_IMETHODIMP +TransactionThreadPool::DatabaseCompleteCallbackRunnable::Run() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // Call our callback. + nsRefPtr database; + database.swap(mDatabase); + mCallback(database, mUserData); + return NS_OK; +} diff --git a/dom/indexedDB/TransactionThreadPool.h b/dom/indexedDB/TransactionThreadPool.h index 4ea0ed739919..adc8000d8fb5 100644 --- a/dom/indexedDB/TransactionThreadPool.h +++ b/dom/indexedDB/TransactionThreadPool.h @@ -44,6 +44,7 @@ #include "IndexedDatabase.h" #include "nsIObserver.h" +#include "nsIRunnable.h" #include "mozilla/Mutex.h" #include "mozilla/CondVar.h" @@ -53,7 +54,6 @@ #include "IDBTransaction.h" -class nsIRunnable; class nsIThreadPool; BEGIN_INDEXEDDB_NAMESPACE @@ -61,17 +61,18 @@ BEGIN_INDEXEDDB_NAMESPACE class FinishTransactionRunnable; class QueuedDispatchInfo; -class TransactionThreadPool : public nsIObserver +class TransactionThreadPool { + friend class nsAutoPtr; friend class FinishTransactionRunnable; public: - NS_DECL_ISUPPORTS - NS_DECL_NSIOBSERVER - // returns a non-owning ref! static TransactionThreadPool* GetOrCreate(); + + // returns a non-owning ref! static TransactionThreadPool* Get(); + static void Shutdown(); nsresult Dispatch(IDBTransaction* aTransaction, @@ -79,7 +80,12 @@ public: bool aFinish, nsIRunnable* aFinishRunnable); - void WaitForAllTransactionsToComplete(IDBDatabase* aDatabase); + typedef void (*DatabaseCompleteCallback)(IDBDatabase* aDatabase, + void* aClosure); + + bool WaitForAllTransactionsToComplete(IDBDatabase* aDatabase, + DatabaseCompleteCallback aCallback, + void* aUserData); protected: class TransactionQueue : public nsIRunnable @@ -136,6 +142,28 @@ protected: bool finish; }; + class DatabaseCompleteCallbackRunnable : public nsIRunnable + { + friend class TransactionThreadPool; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIRUNNABLE + + DatabaseCompleteCallbackRunnable(IDBDatabase* aDatabase, + DatabaseCompleteCallback aCallback, + void* aUserData) + : mDatabase(aDatabase), + mCallback(aCallback), + mUserData(aUserData) + { } + + private: + nsRefPtr mDatabase; + DatabaseCompleteCallback mCallback; + void* mUserData; + }; + TransactionThreadPool(); ~TransactionThreadPool(); @@ -160,6 +188,8 @@ protected: mTransactionsInProgress; nsTArray mDelayedDispatchQueue; + + nsTArray > mCompleteRunnables; }; END_INDEXEDDB_NAMESPACE diff --git a/dom/indexedDB/nsIIndexedDatabaseManager.idl b/dom/indexedDB/nsIIndexedDatabaseManager.idl index 9839d5078b2e..e1d36b5b407b 100644 --- a/dom/indexedDB/nsIIndexedDatabaseManager.idl +++ b/dom/indexedDB/nsIIndexedDatabaseManager.idl @@ -41,10 +41,50 @@ interface nsIURI; -[scriptable, uuid(dba13d2e-ea4f-4011-9b66-a8b0e61a7f84)] +[scriptable, function, uuid(17675af5-0569-4f5b-987f-ff4bb60f73ee)] +interface nsIIndexedDatabaseUsageCallback : nsISupports +{ + /** + * + */ + void onUsageResult(in nsIURI aURI, + in unsigned long long aUsage); +}; + +[scriptable, uuid(415f5684-6c84-4a8b-b777-d01f5df778f2)] interface nsIIndexedDatabaseManager : nsISupports { - unsigned long long getUsageForURI(in nsIURI aURI); + /** + * Schedules an asynchronous callback that will return the total amount of + * disk space being used by databases for the given origin. + * + * @param aURI + * The URI whose usage is being queried. + * @param aCallback + * The callback that will be called when the usage is available. + */ + void getUsageForURI(in nsIURI aURI, + in nsIIndexedDatabaseUsageCallback aCallback); + /** + * Cancels an asynchronous usage check initiated by a previous call to + * getUsageForURI(). + * + * @param aURI + * The URI whose usage is being queried. + * @param aCallback + * The callback that will be called when the usage is available. + */ + void cancelGetUsageForURI(in nsIURI aURI, + in nsIIndexedDatabaseUsageCallback aCallback); + + + /** + * Removes all databases stored for the given URI. The files may not be + * deleted immediately depending on prohibitive concurrent operations. + * + * @param aURI + * The URI whose databases are to be cleared. + */ void clearDatabasesForURI(in nsIURI aURI); }; diff --git a/layout/build/nsLayoutModule.cpp b/layout/build/nsLayoutModule.cpp index 56793e763ea4..672bb1974e8a 100644 --- a/layout/build/nsLayoutModule.cpp +++ b/layout/build/nsLayoutModule.cpp @@ -325,7 +325,7 @@ NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsDOMStorageManager, nsDOMStorageManager::GetInstance) NS_GENERIC_FACTORY_CONSTRUCTOR(nsChannelPolicy) NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(IndexedDatabaseManager, - IndexedDatabaseManager::GetOrCreateInstance) + IndexedDatabaseManager::FactoryCreate) #if defined(XP_UNIX) || \ defined(_WINDOWS) || \ defined(machintosh) || \