Bug 541373 (part 1) - Provide a global VACUUM component. r=sdwilsh sr=vlad a=blocking
authorMarco Bonardo <mbonardo@mozilla.com>
Tue, 19 Oct 2010 15:46:49 +0200
changeset 56087 7d13edc2927276fc663646bbe38fd1fd62dc4dd1
parent 56086 ed1eff4526ce30786f12d53b9a9097b13d1a546e
child 56088 0e4c3af3ac59797c231eaca4962616fa39807bf9
push idunknown
push userunknown
push dateunknown
reviewerssdwilsh, vlad, blocking
bugs541373
milestone2.0b8pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 541373 (part 1) - Provide a global VACUUM component. r=sdwilsh sr=vlad a=blocking
db/sqlite3/src/Makefile.in
storage/build/mozStorageCID.h
storage/build/mozStorageModule.cpp
storage/public/Makefile.in
storage/public/mozIStorageConnection.idl
storage/public/mozIStorageVacuumParticipant.idl
storage/public/storage.h
storage/src/Makefile.in
storage/src/VacuumManager.cpp
storage/src/VacuumManager.h
storage/src/mozStorageConnection.cpp
storage/test/unit/test_vacuum.js
storage/test/unit/vacuumParticipant.js
storage/test/unit/vacuumParticipant.manifest
--- a/db/sqlite3/src/Makefile.in
+++ b/db/sqlite3/src/Makefile.in
@@ -96,17 +96,18 @@ CSRCS = \
   sqlite3.c \
   $(NULL)
 
 # -DSQLITE_SECURE_DELETE=1 will cause SQLITE to 0-fill delete data so we
 # don't have to vacuum to make sure the data is not visible in the file.
 # -DSQLITE_ENABLE_FTS3=1 enables the full-text index module.
 # -DSQLITE_CORE=1 statically links that module into the SQLite library.
 # -DSQLITE_DEFAULT_PAGE_SIZE=32768 and SQLITE_MAX_DEFAULT_PAGE_SIZE=32768
-# increases the page size from 1k, see bug 416330.
+# increases the page size from 1k, see bug 416330.  The value must stay in sync
+# with mozIStorageConnection::DEFAULT_PAGE_SIZE.
 # Note: Be sure to update the configure.in checks when these change!
 DEFINES = \
   -DSQLITE_SECURE_DELETE=1 \
   -DSQLITE_THREADSAFE=1 \
   -DSQLITE_CORE=1 \
   -DSQLITE_ENABLE_FTS3=1 \
   -DSQLITE_ENABLE_UNLOCK_NOTIFY=1 \
   -DSQLITE_DEFAULT_PAGE_SIZE=32768 \
--- a/storage/build/mozStorageCID.h
+++ b/storage/build/mozStorageCID.h
@@ -55,9 +55,15 @@
 #define MOZ_STORAGE_SERVICE_CONTRACTID MOZ_STORAGE_CONTRACTID_PREFIX "/service;1"
 
 /* dab3a846-3a59-4fc2-9745-c6ff48776f00 */
 #define MOZ_STORAGE_STATEMENT_WRAPPER_CID \
 { 0xdab3a846, 0x3a59, 0x4fc2, {0x97, 0x45, 0xc6, 0xff, 0x48, 0x77, 0x6f, 0x00} }
 
 #define MOZ_STORAGE_STATEMENT_WRAPPER_CONTRACTID MOZ_STORAGE_CONTRACTID_PREFIX "/statement-wrapper;1"
 
+/* 3b667ee0-d2da-4ccc-9c3d-95f2ca6a8b4c */
+#define VACUUMMANAGER_CID \
+{ 0x3b667ee0, 0xd2da, 0x4ccc, { 0x9c, 0x3d, 0x95, 0xf2, 0xca, 0x6a, 0x8b, 0x4c } }
+
+#define VACUUMMANAGER_CONTRACTID MOZ_STORAGE_CONTRACTID_PREFIX "/vacuum;1"
+
 #endif /* MOZSTORAGECID_H */
--- a/storage/build/mozStorageModule.cpp
+++ b/storage/build/mozStorageModule.cpp
@@ -38,44 +38,55 @@
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsCOMPtr.h"
 #include "mozilla/ModuleUtils.h"
 
 #include "mozStorageService.h"
 #include "mozStorageConnection.h"
 #include "mozStorageStatementWrapper.h"
+#include "VacuumManager.h"
 
 #include "mozStorageCID.h"
 
 namespace mozilla {
 namespace storage {
 
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(Service,
                                          Service::getSingleton)
 NS_GENERIC_FACTORY_CONSTRUCTOR(StatementWrapper)
-
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(VacuumManager,
+                                         VacuumManager::getSingleton)
 
 } // namespace storage
 } // namespace mozilla
 
 NS_DEFINE_NAMED_CID(MOZ_STORAGE_SERVICE_CID);
 NS_DEFINE_NAMED_CID(MOZ_STORAGE_STATEMENT_WRAPPER_CID);
+NS_DEFINE_NAMED_CID(VACUUMMANAGER_CID);
 
 static const mozilla::Module::CIDEntry kStorageCIDs[] = {
     { &kMOZ_STORAGE_SERVICE_CID, false, NULL, mozilla::storage::ServiceConstructor },
     { &kMOZ_STORAGE_STATEMENT_WRAPPER_CID, false, NULL, mozilla::storage::StatementWrapperConstructor },
+    { &kVACUUMMANAGER_CID, false, NULL, mozilla::storage::VacuumManagerConstructor },
     { NULL }
 };
 
 static const mozilla::Module::ContractIDEntry kStorageContracts[] = {
     { MOZ_STORAGE_SERVICE_CONTRACTID, &kMOZ_STORAGE_SERVICE_CID },
     { MOZ_STORAGE_STATEMENT_WRAPPER_CONTRACTID, &kMOZ_STORAGE_STATEMENT_WRAPPER_CID },
+    { VACUUMMANAGER_CONTRACTID, &kVACUUMMANAGER_CID },
+    { NULL }
+};
+
+static const mozilla::Module::CategoryEntry kStorageCategories[] = {
+    { "profile-after-change", "MozStorage Vacuum Manager", VACUUMMANAGER_CONTRACTID },
     { NULL }
 };
 
 static const mozilla::Module kStorageModule = {
     mozilla::Module::kVersion,
     kStorageCIDs,
-    kStorageContracts
+    kStorageContracts,
+    kStorageCategories
 };
 
 NSMODULE_DEFN(mozStorageModule) = &kStorageModule;
--- a/storage/public/Makefile.in
+++ b/storage/public/Makefile.in
@@ -63,16 +63,17 @@ XPIDLSRCS = \
   mozIStorageStatementCallback.idl \
   mozIStoragePendingStatement.idl \
   mozIStorageBindingParamsArray.idl \
   mozIStorageBindingParams.idl \
   mozIStorageCompletionCallback.idl \
   mozIStorageBaseStatement.idl \
   mozIStorageAsyncStatement.idl \
   mozIStorageServiceQuotaManagement.idl \
+  mozIStorageVacuumParticipant.idl \
 	$(NULL)
 
 EXPORTS_NAMESPACES = mozilla
 
 EXPORTS = \
 	mozStorageHelper.h \
 	mozStorage.h \
 	$(NULL)
--- a/storage/public/mozIStorageConnection.idl
+++ b/storage/public/mozIStorageConnection.idl
@@ -59,16 +59,24 @@ interface nsIFile;
  * creating prepared statements, executing SQL, and examining database
  * errors.
  *
  * @threadsafe
  */
 [scriptable, uuid(ad035628-4ffb-42ff-a256-0ed9e410b859)]
 interface mozIStorageConnection : nsISupports {
   /**
+   * The default size for SQLite database pages used by mozStorage for new
+   * databases.
+   * This value must stay in sync with the SQLITE_DEFAULT_PAGE_SIZE define in
+   * /db/sqlite3/src/Makefile.in
+   */
+  const long DEFAULT_PAGE_SIZE = 32768;
+
+  /**
    * Closes a database connection.  Callers must finalize all statements created
    * for this connection prior to calling this method.  It is illegal to use
    * call this method if any asynchronous statements have been executed on this
    * connection.
    *
    * @throws NS_ERROR_UNEXPECTED
    *         If any statement has been executed asynchronously on this object.
    * @throws NS_ERROR_UNEXPECTED
new file mode 100644
--- /dev/null
+++ b/storage/public/mozIStorageVacuumParticipant.idl
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozStorage.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Marco Bonardo <mak77@bonardo.net> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "nsISupports.idl"
+
+interface mozIStorageConnection;
+
+/**
+ * This interface contains the information that the Storage service needs to
+ * vacuum a database.  This interface is created as a service through the
+ * category manager with the category "vacuum-participant".
+ * Please see https://developer.mozilla.org/en/mozIStorageVacuumParticipant for
+ * more information.
+ */
+[scriptable, uuid(8f367508-1d9a-4d3f-be0c-ac11b6dd7dbf)]
+interface mozIStorageVacuumParticipant : nsISupports {
+  /**
+   * The expected page size in bytes for the database.  The vacuum manager will
+   * try to correct the page size during idle based on this value.
+   *
+   * @note If the database is using the WAL journal mode and the current page
+   *       size is not the expected one, the journal mode will be changed to
+   *       TRUNCATE because WAL does not allow page size changes.
+   *       The vacuum manager will try to restore WAL mode, but for this to
+   *       work reliably the participant must ensure to always reset statements.
+   *       If restoring the journal mode should fail it will stick to TRUNCATE.
+   * @note Valid page size values are from 512 to 65536.
+   *       The suggested value is mozIStorageConnection::DEFAULT_PAGE_SIZE.
+   */
+  readonly attribute long expectedDatabasePageSize;
+
+  /**
+   * Connection to the database file to be vacuumed.
+   */
+  readonly attribute mozIStorageConnection databaseConnection;
+
+  /**
+   * Notifies when a vacuum operation begins.  Listeners should avoid using the
+   * database till onEndVacuum is received.
+   *
+   * @return true to proceed with the vacuum, false if the participant wants to
+   *         opt-out for now, it will be retried later.  Useful when participant
+   *         is running some other heavy operation that can't be interrupted.
+   *
+   * @note When a vacuum operation starts or ends it will also dispatch a global
+   *       "heavy-io-task" notification through the observer service with the
+   *       data argument being either "vacuum-begin" or "vacuum-end".
+   */
+  boolean onBeginVacuum();
+
+  /**
+   * Notifies when a vacuum operation ends.
+   *
+   * @param aSucceeded
+   *        reports if the vacuum succeeded or failed.
+   */
+  void onEndVacuum(in boolean aSucceeded);
+};
--- a/storage/public/storage.h
+++ b/storage/public/storage.h
@@ -53,16 +53,17 @@
 #include "mozIStorageResultSet.h"
 #include "mozIStorageRow.h"
 #include "mozIStorageService.h"
 #include "mozIStorageStatement.h"
 #include "mozIStorageStatementCallback.h"
 #include "mozIStorageBindingParamsArray.h"
 #include "mozIStorageBindingParams.h"
 #include "mozIStorageServiceQuotaManagement.h"
+#include "mozIStorageVacuumParticipant.h"
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Native Language Helpers
 
 #include "mozStorageHelper.h"
 
 #include "mozilla/storage/Variant.h"
 
--- a/storage/src/Makefile.in
+++ b/storage/src/Makefile.in
@@ -75,16 +75,17 @@ CPPSRCS = \
   mozStoragePrivateHelpers.cpp \
   mozStorageBindingParamsArray.cpp \
   mozStorageBindingParams.cpp \
   mozStorageAsyncStatement.cpp \
   mozStorageAsyncStatementJSHelper.cpp \
   mozStorageAsyncStatementParams.cpp \
   StorageBaseStatementInternal.cpp \
   SQLCollations.cpp \
+  VacuumManager.cpp \
   $(NULL)
 
 LOCAL_INCLUDES = \
   $(SQLITE_CFLAGS) \
   -I$(topsrcdir)/db/sqlite3/src \
   $(NULL)
 
 # This is the default value.  If we ever change it when compiling sqlite, we
new file mode 100644
--- /dev/null
+++ b/storage/src/VacuumManager.cpp
@@ -0,0 +1,555 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozStorage.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Marco Bonardo <mak77@bonardo.net> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "VacuumManager.h"
+
+#include "mozilla/Services.h"
+#include "nsIObserverService.h"
+#include "nsPrintfCString.h"
+#include "nsIFile.h"
+#include "nsThreadUtils.h"
+#include "prlog.h"
+
+#include "mozStorageConnection.h"
+#include "mozIStorageStatement.h"
+#include "mozIStorageAsyncStatement.h"
+#include "mozIStoragePendingStatement.h"
+#include "mozIStorageError.h"
+
+#define OBSERVER_TOPIC_IDLE_DAILY "idle-daily"
+#define OBSERVER_TOPIC_XPCOM_SHUTDOWN "xpcom-shutdown"
+
+// Used to notify begin and end of a heavy IO task.
+#define OBSERVER_TOPIC_HEAVY_IO "heavy-io-task"
+#define OBSERVER_DATA_VACUUM_BEGIN NS_LITERAL_STRING("vacuum-begin")
+#define OBSERVER_DATA_VACUUM_END NS_LITERAL_STRING("vacuum-end")
+
+// This preferences root will contain last vacuum timestamps (in seconds) for
+// each database.  The database filename is used as a key.
+#define PREF_VACUUM_BRANCH "storage.vacuum.last."
+
+// Time between subsequent vacuum calls for a certain database.
+#define VACUUM_INTERVAL_SECONDS 30 * 86400 // 30 days.
+
+#ifdef PR_LOGGING
+extern PRLogModuleInfo *gStorageLog;
+#endif
+
+namespace mozilla {
+namespace storage {
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+//// BaseCallback
+
+class BaseCallback : public mozIStorageStatementCallback
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_MOZISTORAGESTATEMENTCALLBACK
+  BaseCallback() {}
+protected:
+  virtual ~BaseCallback() {}
+};
+
+NS_IMETHODIMP
+BaseCallback::HandleError(mozIStorageError *aError)
+{
+#ifdef DEBUG
+  PRInt32 result;
+  nsresult rv = aError->GetResult(&result);
+  NS_ENSURE_SUCCESS(rv, rv);
+  nsCAutoString message;
+  rv = aError->GetMessage(message);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCAutoString warnMsg;
+  warnMsg.AppendLiteral("An error occured during async execution: ");
+  warnMsg.AppendInt(result);
+  warnMsg.AppendLiteral(" ");
+  warnMsg.Append(message);
+  NS_WARNING(warnMsg.get());
+#endif
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+BaseCallback::HandleResult(mozIStorageResultSet *aResultSet)
+{
+  // We could get results from PRAGMA statements, but we don't mind them.
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+BaseCallback::HandleCompletion(PRUint16 aReason)
+{
+  // By default BaseCallback will just be silent on completion.
+  return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS1(
+  BaseCallback
+, mozIStorageStatementCallback
+)
+
+//////////////////////////////////////////////////////////////////////////////// 
+//// Vacuumer declaration.
+
+class Vacuumer : public BaseCallback
+{
+public:
+  NS_DECL_MOZISTORAGESTATEMENTCALLBACK
+
+  Vacuumer(mozIStorageVacuumParticipant *aParticipant,
+           nsIPrefBranch *aPrefBranch);
+
+  bool execute();
+  nsresult notifyCompletion(bool aSucceeded);
+
+private:
+  nsCOMPtr<mozIStorageVacuumParticipant> mParticipant;
+  nsCOMPtr<nsIPrefBranch> mPrefBranch;
+  nsCString mDBFilename;
+  nsCOMPtr<mozIStorageConnection> mDBConn;
+  bool mRestoreWAL;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// NotifyCallback
+/**
+ * This class handles an async statement execution and notifies completion
+ * through a passed in callback.
+ * Errors are handled through warnings in HandleError.
+ */
+class NotifyCallback : public BaseCallback
+{
+public:
+  NS_IMETHOD HandleCompletion(PRUint16 aReason);
+
+  NotifyCallback(Vacuumer *aVacuumer, bool aVacuumSucceeded);
+
+private:
+  nsCOMPtr<Vacuumer> mVacuumer;
+  bool mVacuumSucceeded;
+};
+
+NotifyCallback::NotifyCallback(Vacuumer *aVacuumer,
+                               bool aVacuumSucceeded)
+  : mVacuumer(aVacuumer)
+  , mVacuumSucceeded(aVacuumSucceeded)
+{
+}
+
+NS_IMETHODIMP
+NotifyCallback::HandleCompletion(PRUint16 aReason)
+{
+  // We succeeded if both vacuum are WAL restoration succeeded.
+  nsresult rv = mVacuumer->notifyCompletion(mVacuumSucceeded);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Vacuumer implementation.
+
+Vacuumer::Vacuumer(mozIStorageVacuumParticipant *aParticipant,
+                   nsIPrefBranch *aPrefBranch)
+  : mParticipant(aParticipant)
+  , mPrefBranch(aPrefBranch)
+  , mRestoreWAL(false)
+{
+}
+
+bool
+Vacuumer::execute()
+{
+  NS_PRECONDITION(NS_IsMainThread(), "Must be running on the main thread!");
+
+  // Get the connection and check its validity.
+  nsresult rv = mParticipant->GetDatabaseConnection(getter_AddRefs(mDBConn));
+  NS_ENSURE_SUCCESS(rv, false);
+  PRBool ready;
+  if (!mDBConn || NS_FAILED(mDBConn->GetConnectionReady(&ready)) || !ready) {
+    NS_WARNING(NS_LITERAL_CSTRING("Unable to get a connection to vacuum database").get());
+    return false;
+  }
+
+  // Compare current page size with the expected one.  Vacuum can change the
+  // page size value if needed.  Even if a vacuum happened recently, if the
+  // page size can be optimized we should do it.
+  PRInt32 expectedPageSize = 0;
+  rv = mParticipant->GetExpectedDatabasePageSize(&expectedPageSize);
+  if (NS_FAILED(rv) || expectedPageSize < 512 || expectedPageSize > 65536) {
+    NS_WARNING("Invalid page size requested for database, will use default ");
+    NS_WARNING(mDBFilename.get());
+    expectedPageSize = mozIStorageConnection::DEFAULT_PAGE_SIZE;
+  }
+
+  bool canOptimizePageSize;
+  {
+    nsCOMPtr<mozIStorageStatement> stmt;
+    rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+      "PRAGMA page_size"
+    ), getter_AddRefs(stmt));
+    NS_ENSURE_SUCCESS(rv, false);
+    PRBool hasResult;
+    rv = stmt->ExecuteStep(&hasResult);
+    NS_ENSURE_SUCCESS(rv, false);
+    NS_ENSURE_TRUE(hasResult, false);
+    PRInt32 currentPageSize;
+    rv = stmt->GetInt32(0, &currentPageSize);
+    NS_ENSURE_SUCCESS(rv, false);
+    NS_ASSERTION(currentPageSize > 0, "Got invalid page size value?");
+    canOptimizePageSize = currentPageSize != expectedPageSize;
+  }
+
+  // Get the database filename.  Last vacuum time is stored under this name
+  // in PREF_VACUUM_BRANCH.
+  nsCOMPtr<nsIFile> databaseFile;
+  mDBConn->GetDatabaseFile(getter_AddRefs(databaseFile));
+  if (!databaseFile) {
+    NS_WARNING("Trying to vacuum a in-memory database!");
+    return false;
+  }
+  nsAutoString databaseFilename;
+  rv = databaseFile->GetLeafName(databaseFilename);
+  NS_ENSURE_SUCCESS(rv, false);
+  mDBFilename = NS_ConvertUTF16toUTF8(databaseFilename);
+  NS_ASSERTION(!mDBFilename.IsEmpty(), "Database filename cannot be empty");
+
+  // Check interval from last vacuum.
+  PRInt32 now = static_cast<PRInt32>(PR_Now() / PR_USEC_PER_SEC);
+  PRInt32 lastVacuum;
+  rv = mPrefBranch->GetIntPref(mDBFilename.get(), &lastVacuum);
+  if (NS_SUCCEEDED(rv) && (now - lastVacuum) < VACUUM_INTERVAL_SECONDS &&
+      !canOptimizePageSize) {
+    // This database was vacuumed recently and has optimal page size, skip it. 
+    return false;
+  }
+
+  // Notify that we are about to start vacuuming.  The participant can opt-out
+  // if it cannot handle a vacuum at this time, and then we'll move to the next
+  // one.
+  PRBool vacuumGranted = PR_FALSE;
+  rv = mParticipant->OnBeginVacuum(&vacuumGranted);
+  NS_ENSURE_SUCCESS(rv, false);
+  if (!vacuumGranted) {
+    return false;
+  }
+
+  // Notify a heavy IO task is about to start.
+  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+  if (os) {
+    (void)os->NotifyObservers(nsnull, OBSERVER_TOPIC_HEAVY_IO,
+                              OBSERVER_DATA_VACUUM_BEGIN.get());
+  }
+
+  if (canOptimizePageSize) {
+    // Check journal mode.  WAL journaling does not allow vacuum to change page
+    // size, thus we have to temporarily switch the journal mode to TRUNCATE.
+    nsCOMPtr<mozIStorageStatement> stmt;
+    rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+      "PRAGMA journal_mode"
+    ), getter_AddRefs(stmt));
+    NS_ENSURE_SUCCESS(rv, false);
+    PRBool hasResult;
+    rv = stmt->ExecuteStep(&hasResult);
+    NS_ENSURE_SUCCESS(rv, false);
+    NS_ENSURE_TRUE(hasResult, false);
+    nsCAutoString journalMode;
+    rv = stmt->GetUTF8String(0, journalMode);
+    NS_ENSURE_SUCCESS(rv, false);
+    rv = stmt->Reset();
+    NS_ENSURE_SUCCESS(rv, false);
+
+    if (journalMode.EqualsLiteral("wal")) {
+      mRestoreWAL = true;
+      // Set the journal mode to a backwards compatible one.
+      nsCOMPtr<mozIStorageAsyncStatement> stmt;
+      rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+        "PRAGMA journal_mode = TRUNCATE"
+      ), getter_AddRefs(stmt));
+      NS_ENSURE_SUCCESS(rv, false);
+      nsCOMPtr<BaseCallback> callback = new BaseCallback();
+      NS_ENSURE_TRUE(callback, false);
+      nsCOMPtr<mozIStoragePendingStatement> ps;
+      rv = stmt->ExecuteAsync(callback, getter_AddRefs(ps));
+      NS_ENSURE_SUCCESS(rv, false);
+    }
+
+    // WARNING: any statement after we check for journal mode must be async
+    //          to ensure the correct execution order.
+
+    nsCOMPtr<mozIStorageAsyncStatement> pageSizeStmt;
+    rv = mDBConn->CreateAsyncStatement(nsPrintfCString(
+      "PRAGMA page_size = %ld", expectedPageSize
+    ), getter_AddRefs(pageSizeStmt));
+    NS_ENSURE_SUCCESS(rv, false);
+    nsCOMPtr<BaseCallback> callback = new BaseCallback();
+    NS_ENSURE_TRUE(callback, false);
+    nsCOMPtr<mozIStoragePendingStatement> ps;
+    rv = pageSizeStmt->ExecuteAsync(callback, getter_AddRefs(ps));
+    NS_ENSURE_SUCCESS(rv, false);
+  }
+
+  nsCOMPtr<mozIStorageAsyncStatement> stmt;
+  rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+    "VACUUM"
+  ), getter_AddRefs(stmt));
+  NS_ENSURE_SUCCESS(rv, false);
+
+  nsCOMPtr<mozIStoragePendingStatement> ps;
+  rv = stmt->ExecuteAsync(this, getter_AddRefs(ps));
+  NS_ENSURE_SUCCESS(rv, false);
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// mozIStorageStatementCallback
+
+NS_IMETHODIMP
+Vacuumer::HandleError(mozIStorageError *aError)
+{
+#ifdef DEBUG
+  PRInt32 result;
+  nsresult rv = aError->GetResult(&result);
+  NS_ENSURE_SUCCESS(rv, rv);
+  nsCAutoString message;
+  rv = aError->GetMessage(message);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCAutoString warnMsg;
+  warnMsg.AppendLiteral("Unable to vacuum database: ");
+  warnMsg.Append(mDBFilename);
+  warnMsg.AppendLiteral(" - ");
+  warnMsg.AppendInt(result);
+  warnMsg.AppendLiteral(" ");
+  warnMsg.Append(message);
+  NS_WARNING(warnMsg.get());
+#endif
+
+#ifdef PR_LOGGING
+  {
+    PRInt32 result;
+    nsresult rv = aError->GetResult(&result);
+    NS_ENSURE_SUCCESS(rv, rv);
+    nsCAutoString message;
+    rv = aError->GetMessage(message);
+    NS_ENSURE_SUCCESS(rv, rv);
+    PR_LOG(gStorageLog, PR_LOG_ERROR,
+           ("Vacuum failed with error: %d '%s'. Database was: '%s'",
+            result, message.get(), mDBFilename.get()));
+  }
+#endif
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+Vacuumer::HandleResult(mozIStorageResultSet *aResultSet)
+{
+  NS_NOTREACHED("Got a resultset from a vacuum?");
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+Vacuumer::HandleCompletion(PRUint16 aReason)
+{
+  if (aReason == REASON_FINISHED) {
+    // Update last vacuum time.
+    PRInt32 now = static_cast<PRInt32>(PR_Now() / PR_USEC_PER_SEC);
+    NS_ASSERTION(!mDBFilename.IsEmpty(), "Database filename cannot be empty");
+    (void)mPrefBranch->SetIntPref(mDBFilename.get(), now);
+  }
+
+  // Check if we should restore WAL journal mode.
+  if (mRestoreWAL) {
+    // Restoring WAL is expensive, so must be done async.
+    // End of vacuum will be notified once it finishes.
+    nsCOMPtr<mozIStorageAsyncStatement> stmt;
+    (void)mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+      "PRAGMA journal_mode = WAL"
+    ), getter_AddRefs(stmt));
+    nsCOMPtr<NotifyCallback> callback =
+      new NotifyCallback(this, aReason == REASON_FINISHED);
+    // Handle errors now, it's important to notify participant before throwing.
+    if (!stmt || !callback) {
+      notifyCompletion(false);
+      return NS_ERROR_UNEXPECTED;
+    }
+    nsCOMPtr<mozIStoragePendingStatement> ps;
+    stmt->ExecuteAsync(callback, getter_AddRefs(ps));
+  }
+  else {
+    notifyCompletion(aReason == REASON_FINISHED);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+Vacuumer::notifyCompletion(bool aSucceeded)
+{
+  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+  if (os) {
+    os->NotifyObservers(nsnull, OBSERVER_TOPIC_HEAVY_IO,
+                        OBSERVER_DATA_VACUUM_END.get());
+  }
+
+  nsresult rv = mParticipant->OnEndVacuum(aSucceeded);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+} // Anonymous namespace.
+
+////////////////////////////////////////////////////////////////////////////////
+//// VacuumManager
+
+NS_IMPL_ISUPPORTS1(
+  VacuumManager
+, nsIObserver
+)
+
+VacuumManager *
+VacuumManager::gVacuumManager = nsnull;
+
+VacuumManager *
+VacuumManager::getSingleton()
+{
+  if (gVacuumManager) {
+    NS_ADDREF(gVacuumManager);
+    return gVacuumManager;
+  }
+  gVacuumManager = new VacuumManager();
+  if (gVacuumManager) {
+    NS_ADDREF(gVacuumManager);
+    if (NS_FAILED(gVacuumManager->initialize())) {
+      NS_RELEASE(gVacuumManager);
+    }
+  }
+  return gVacuumManager;
+}
+
+VacuumManager::VacuumManager()
+  : mParticipants("vacuum-participant")
+{
+  NS_ASSERTION(!gVacuumManager,
+               "Attempting to create two instances of the service!");
+  gVacuumManager = this;
+}
+
+nsresult
+VacuumManager::initialize()
+{
+  NS_PRECONDITION(NS_IsMainThread(), "Must be running on the main thread!");
+
+  // Observe idle-daily to run a single vacuum per day.
+  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+  NS_ENSURE_STATE(os);
+  nsresult rv = os->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, PR_FALSE);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = os->AddObserver(this, OBSERVER_TOPIC_XPCOM_SHUTDOWN, PR_FALSE);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Used to store last vacuum times.
+  nsCOMPtr<nsIPrefService> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+  NS_ENSURE_STATE(prefs);
+  prefs->GetBranch(PREF_VACUUM_BRANCH, getter_AddRefs(mPrefBranch));
+  NS_ENSURE_STATE(mPrefBranch);
+
+  return NS_OK;
+}
+
+VacuumManager::~VacuumManager()
+{
+  // Remove the static reference to the service.  Check to make sure its us
+  // in case somebody creates an extra instance of the service.
+  NS_ASSERTION(gVacuumManager == this,
+               "Deleting a non-singleton instance of the service");
+  if (gVacuumManager == this) {
+    gVacuumManager = nsnull;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIObserver
+
+NS_IMETHODIMP
+VacuumManager::Observe(nsISupports *aSubject,
+                       const char *aTopic,
+                       const PRUnichar *aData)
+{
+  if (strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY) == 0) {
+    // Try to run vacuum on all registered entries.  Will stop at the first
+    // successful one.
+    const nsCOMArray<mozIStorageVacuumParticipant> &entries =
+      mParticipants.GetEntries();
+    // If there are more entries than what a month can contain, we could end up
+    // skipping some, since we run daily.  So we use a starting index.
+    PRInt32 startIndex = 0, index;
+    (void)mPrefBranch->GetIntPref(NS_LITERAL_CSTRING("index").get(), &startIndex);
+    if (startIndex >= entries.Count()) {
+      startIndex = 0;
+    }
+    for (index = startIndex; index < entries.Count(); ++index) {
+      nsCOMPtr<Vacuumer> vacuum = new Vacuumer(entries[index], mPrefBranch);
+      NS_ENSURE_STATE(vacuum);
+      // Only vacuum one database per day.
+      if (vacuum->execute()) {
+        break;
+      }
+    }
+    (void)mPrefBranch->SetIntPref(NS_LITERAL_CSTRING("index").get(), index);
+    return NS_OK;
+  }
+  else if (strcmp(aTopic, OBSERVER_TOPIC_XPCOM_SHUTDOWN) == 0) {
+    nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+    if (os) {
+      (void)os->RemoveObserver(this, OBSERVER_TOPIC_IDLE_DAILY);
+      (void)os->RemoveObserver(this, OBSERVER_TOPIC_XPCOM_SHUTDOWN);
+    }
+  }
+  return NS_OK;
+}
+
+} // namespace storage
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/storage/src/VacuumManager.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozStorage.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Marco Bonardo <mak77@bonardo.net> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef mozilla_storage_VacuumManager_h__
+#define mozilla_storage_VacuumManager_h__
+
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "mozIStorageStatementCallback.h"
+#include "mozIStorageVacuumParticipant.h"
+#include "nsCategoryCache.h"
+#include "nsIPrefService.h"
+
+namespace mozilla {
+namespace storage {
+
+class VacuumManager : public nsIObserver
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+
+  VacuumManager();
+
+  /**
+   * Obtains the VacuumManager object.
+   */
+  static VacuumManager * getSingleton();
+
+  /**
+   * Initializes the VacuumManager object.  Must be called just once.
+   */
+  nsresult initialize();
+
+private:
+  ~VacuumManager();
+
+  static VacuumManager *gVacuumManager;
+
+  // Cache of components registered in "vacuum-participant" category.
+  nsCategoryCache<mozIStorageVacuumParticipant> mParticipants;
+
+  // Pref branch for PREF_VACUUM_BRANCH.
+  nsCOMPtr<nsIPrefBranch> mPrefBranch;
+};
+
+} // namespace storage
+} // namespace mozilla
+
+#endif
--- a/storage/src/mozStorageConnection.cpp
+++ b/storage/src/mozStorageConnection.cpp
@@ -418,18 +418,19 @@ Connection::initialize(nsIFile *aDatabas
   nsCAutoString leafName(":memory");
   if (aDatabaseFile)
     (void)aDatabaseFile->GetNativeLeafName(leafName);
   PR_LOG(gStorageLog, PR_LOG_NOTICE, ("Opening connection to '%s' (%p)",
                                       leafName.get(), this));
 #endif
   // Switch db to preferred page size in case the user vacuums.
   sqlite3_stmt *stmt;
-  srv = prepareStmt(mDBConn, NS_LITERAL_CSTRING("PRAGMA page_size = 32768"),
-                    &stmt);
+  nsCAutoString pageSizeQuery(NS_LITERAL_CSTRING("PRAGMA page_size = "));
+  pageSizeQuery.AppendInt(DEFAULT_PAGE_SIZE);
+  srv = prepareStmt(mDBConn, pageSizeQuery, &stmt);
   if (srv == SQLITE_OK) {
     (void)stepStmt(stmt);
     (void)::sqlite3_finalize(stmt);
   }
 
   // Register our built-in SQL functions.
   srv = registerFunctions(mDBConn);
   if (srv != SQLITE_OK) {
new file mode 100644
--- /dev/null
+++ b/storage/test/unit/test_vacuum.js
@@ -0,0 +1,333 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This file tests the Vacuum Manager.
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Loads a test component that will register as a vacuum-participant.
+ * If other participants are found they will be unregistered, to avoid conflicts
+ * with the test itself.
+ */
+function load_test_vacuum_component()
+{
+  const CATEGORY_NAME = "vacuum-participant";
+
+  do_load_manifest("vacuumParticipant.manifest");
+
+  // This is a lazy check, there could be more participants than just this test
+  // we just mind that the test exists though.
+  const EXPECTED_ENTRIES = ["vacuumParticipant"];
+  let catMan = Cc["@mozilla.org/categorymanager;1"].
+               getService(Ci.nsICategoryManager);
+  let found = false;
+  let entries = catMan.enumerateCategory(CATEGORY_NAME);
+  while (entries.hasMoreElements()) {
+    let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
+    print("Check if the found category entry (" + entry + ") is expected.");
+    if (EXPECTED_ENTRIES.indexOf(entry) != -1) {
+      print("Check that only one test entry exists.");
+      do_check_false(found);
+      found = true;
+    }
+    else {
+      // Temporary unregister other participants for this test.
+      catMan.deleteCategoryEntry("vacuum-participant", entry, false);
+    }
+  }
+  print("Check the test entry exists.");
+  do_check_true(found);
+}
+
+/**
+ * Sends a fake idle-daily notification to the VACUUM Manager.
+ */
+function synthesize_idle_daily()
+{
+  let vm = Cc["@mozilla.org/storage/vacuum;1"].getService(Ci.nsIObserver);
+  vm.observe(null, "idle-daily", null);
+}
+
+/**
+ * Returns a new nsIFile reference for a profile database.
+ * @param filename for the database, excluded the .sqlite extension.
+ */
+function new_db_file(name)
+{
+  let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+  file.append(name + ".sqlite");
+  return file;
+}
+
+function run_test()
+{
+  do_test_pending();
+
+  // Change initial page size.  Do it immediately since it would require an
+  // additional vacuum op to do it later.  As a bonux this makes the page size
+  // change test really fast since it only has to check results.
+  let conn = getDatabase(new_db_file("testVacuum"));
+  conn.executeSimpleSQL("PRAGMA page_size = 1024");
+  print("Check current page size.");
+  let stmt = conn.createStatement("PRAGMA page_size");
+  try {
+    while (stmt.executeStep()) {
+      do_check_eq(stmt.row.page_size, 1024);
+    }
+  }
+  finally {
+    stmt.finalize();
+  }
+
+  load_test_vacuum_component();
+
+  run_next_test();
+}
+
+function run_next_test()
+{
+  if (TESTS.length == 0) {
+    Services.obs.notifyObservers(null, "test-options", "dispose");
+    do_test_finished();
+  }
+  else {
+    // Set last VACUUM to a date in the past.
+    Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite",
+                              parseInt(Date.now() / 1000 - 31 * 86400));
+    do_execute_soon(TESTS.shift());
+  }
+}
+
+const TESTS = [
+
+function test_common_vacuum()
+{
+  print("\n*** Test that a VACUUM correctly happens and all notifications are fired.");
+  // Wait for VACUUM begin.
+  let beginVacuumReceived = false;
+  Services.obs.addObserver(function (aSubject, aTopic, aData) {
+    Services.obs.removeObserver(arguments.callee, aTopic, false);
+    beginVacuumReceived = true;
+  }, "test-begin-vacuum", false);
+
+  // Wait for heavy IO notifications.
+  let heavyIOTaskBeginReceived = false;
+  let heavyIOTaskEndReceived = false;
+  Services.obs.addObserver(function (aSubject, aTopic, aData) {
+    if (heavyIOTaskBeginReceived && heavyIOTaskEndReceived) {
+      Services.obs.removeObserver(arguments.callee, aTopic, false);
+    }
+
+    if (aData == "vacuum-begin") {
+      heavyIOTaskBeginReceived = true;
+    }
+    else if (aData == "vacuum-end") {
+      heavyIOTaskEndReceived = true;
+    }
+  }, "heavy-io-task", false);
+
+  // Wait for VACUUM end.
+  Services.obs.addObserver(function (aSubject, aTopic, aData) {
+    Services.obs.removeObserver(arguments.callee, aTopic, false);
+    print("Check we received onBeginVacuum");
+    do_check_true(beginVacuumReceived);
+    print("Check we received heavy-io-task notifications");
+    do_check_true(heavyIOTaskBeginReceived);
+    do_check_true(heavyIOTaskEndReceived);
+    print("Received onEndVacuum");
+    run_next_test();
+  }, "test-end-vacuum", false);
+
+  synthesize_idle_daily();
+},
+
+function test_skipped_if_recent_vacuum()
+{
+  print("\n*** Test that a VACUUM is skipped if it was run recently.");
+  Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite",
+                            parseInt(Date.now() / 1000));
+
+  // Wait for VACUUM begin.
+  let vacuumObserver = {
+    gotNotification: false,
+    observe: function VO_observe(aSubject, aTopic, aData) {
+      this.gotNotification = true;
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+  }
+  Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+  // Check after a couple seconds that no VACUUM has been run.
+  do_timeout(2000, function () {
+    print("Check VACUUM did not run.");
+    do_check_false(vacuumObserver.gotNotification);
+    Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+    run_next_test();
+  });
+
+  synthesize_idle_daily();
+},
+
+function test_page_size_change()
+{
+  print("\n*** Test that a VACUUM changes page_size");
+
+  // We did setup the database with a small page size, the previous vacuum
+  // should have updated it.
+  print("Check that page size was updated.");
+  let conn = getDatabase(new_db_file("testVacuum"));
+  let stmt = conn.createStatement("PRAGMA page_size");
+  try {
+    while (stmt.executeStep()) {
+      do_check_eq(stmt.row.page_size,  Ci.mozIStorageConnection.DEFAULT_PAGE_SIZE);
+    }
+  }
+  finally {
+    stmt.finalize();
+  }
+
+  run_next_test();
+},
+
+function test_skipped_optout_vacuum()
+{
+  print("\n*** Test that a VACUUM is skipped if the participant wants to opt-out.");
+  Services.obs.notifyObservers(null, "test-options", "opt-out");
+
+  // Wait for VACUUM begin.
+  let vacuumObserver = {
+    gotNotification: false,
+    observe: function VO_observe(aSubject, aTopic, aData) {
+      this.gotNotification = true;
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+  }
+  Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+  // Check after a couple seconds that no VACUUM has been run.
+  do_timeout(2000, function () {
+    print("Check VACUUM did not run.");
+    do_check_false(vacuumObserver.gotNotification);
+    Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+    run_next_test();
+  });
+
+  synthesize_idle_daily();
+},
+
+function test_page_size_change_with_wal()
+{
+  print("\n*** Test that a VACUUM changes page_size with WAL mode");
+  Services.obs.notifyObservers(null, "test-options", "wal");
+
+  // Set a small page size.
+  let conn = getDatabase(new_db_file("testVacuum2"));
+  conn.executeSimpleSQL("PRAGMA page_size = 1024");
+  let stmt = conn.createStatement("PRAGMA page_size");
+  try {
+    while (stmt.executeStep()) {
+      do_check_eq(stmt.row.page_size, 1024);
+    }
+  }
+  finally {
+    stmt.finalize();
+  }
+
+  // Use WAL journal mode.
+  conn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+  stmt = conn.createStatement("PRAGMA journal_mode");
+  try {
+    while (stmt.executeStep()) {
+      do_check_eq(stmt.row.journal_mode, "wal");
+    }
+  }
+  finally {
+    stmt.finalize();
+  }
+
+  // Wait for VACUUM end.
+  let vacuumObserver = {
+    observe: function VO_observe(aSubject, aTopic, aData) {
+      Services.obs.removeObserver(this, aTopic);
+      print("Check page size has been updated.");
+      let stmt = conn.createStatement("PRAGMA page_size");
+      try {
+        while (stmt.executeStep()) {
+          do_check_eq(stmt.row.page_size, Ci.mozIStorageConnection.DEFAULT_PAGE_SIZE);
+        }
+      }
+      finally {
+        stmt.finalize();
+      }
+
+      print("Check journal mode has been restored.");
+      stmt = conn.createStatement("PRAGMA journal_mode");
+      try {
+        while (stmt.executeStep()) {
+          do_check_eq(stmt.row.journal_mode, "wal");
+        }
+      }
+      finally {
+        stmt.finalize();
+      }
+
+      run_next_test();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+  }
+  Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false);
+
+  synthesize_idle_daily();
+},
+
+function test_memory_database_crash()
+{
+  print("\n*** Test that we don't crash trying to vacuum a memory database");
+  Services.obs.notifyObservers(null, "test-options", "memory");
+
+  // Wait for VACUUM begin.
+  let vacuumObserver = {
+    gotNotification: false,
+    observe: function VO_observe(aSubject, aTopic, aData) {
+      this.gotNotification = true;
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+  }
+  Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+  // Check after a couple seconds that no VACUUM has been run.
+  do_timeout(2000, function () {
+    print("Check VACUUM did not run.");
+    do_check_false(vacuumObserver.gotNotification);
+    Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+    run_next_test();
+  });
+
+  synthesize_idle_daily();
+},
+
+/* Test temporarily disabled due to bug 599098.
+function test_wal_restore_fail()
+{
+  print("\n*** Test that a failing WAL restoration notifies failure");
+  Services.obs.notifyObservers(null, "test-options", "wal-fail");
+
+  // Wait for VACUUM end.
+  let vacuumObserver = {
+    observe: function VO_observe(aSubject, aTopic, aData) {
+      Services.obs.removeObserver(vacuumObserver, "test-end-vacuum");
+      print("Check WAL restoration failed.");
+      do_check_false(aData);
+      run_next_test();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+  }
+  Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false);
+
+  synthesize_idle_daily();
+},
+*/
+];
new file mode 100644
--- /dev/null
+++ b/storage/test/unit/vacuumParticipant.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This testing component is used in test_vacuum* tests.
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Returns a new nsIFile reference for a profile database.
+ * @param filename for the database, excluded the .sqlite extension.
+ */
+function new_db_file(name)
+{
+  let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+  file.append(name + ".sqlite");
+  return file;
+}
+
+/**
+ * Opens and returns a connection to the provided database file.
+ * @param nsIFile interface to the database file.
+ */
+function getDatabase(aFile)
+{
+  return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService)
+                                             .openDatabase(aFile);
+}
+
+function vacuumParticipant()
+{
+  this._dbConn = getDatabase(new_db_file("testVacuum"));
+  Services.obs.addObserver(this, "test-options", false);
+}
+
+vacuumParticipant.prototype =
+{
+  classDescription: "vacuumParticipant",
+  classID: Components.ID("{52aa0b22-b82f-4e38-992a-c3675a3355d2}"),
+  contractID: "@unit.test.com/test-vacuum-participant;1",
+
+  get expectedDatabasePageSize() Ci.mozIStorageConnection.DEFAULT_PAGE_SIZE,
+  get databaseConnection() this._dbConn,
+
+  _grant: true,
+  onBeginVacuum: function TVP_onBeginVacuum()
+  {
+    if (!this._grant) {
+      this._grant = true;
+      return false;
+    }
+    Services.obs.notifyObservers(null, "test-begin-vacuum", null);
+    return true;
+  },
+  onEndVacuum: function TVP_EndVacuum(aSucceeded)
+  {
+    if (this._stmt) {
+      this._stmt.finalize();
+    }
+    Services.obs.notifyObservers(null, "test-end-vacuum", aSucceeded);
+  },
+
+  observe: function TVP_observe(aSubject, aTopic, aData)
+  {
+    if (aData == "opt-out") {
+      this._grant = false;
+    }
+    else if (aData == "wal") {
+      try {
+        this._dbConn.close();
+      }
+      catch(e) {}
+      this._dbConn = getDatabase(new_db_file("testVacuum2"));
+    }
+    else if (aData == "wal-fail") {
+      try {
+        this._dbConn.close();
+      }
+      catch(e) {}
+      this._dbConn = getDatabase(new_db_file("testVacuum3"));
+      // Use WAL journal mode.
+      this._dbConn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+      // Create a not finalized statement.
+      this._stmt = this._dbConn.createStatement("SELECT :test");
+      this._stmt.params.test = 1;
+      this._stmt.executeStep();
+    }
+    else if (aData == "memory") {
+      try {
+        this._dbConn.close();
+      }
+      catch(e) {}
+      this._dbConn = Cc["@mozilla.org/storage/service;1"].
+                     getService(Ci.mozIStorageService).
+                     openSpecialDatabase("memory");
+    }
+    else if (aData == "dispose") {
+      Services.obs.removeObserver(this, "test-options");
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.mozIStorageVacuumParticipant
+  , Ci.nsIObserver
+  ])
+};
+
+let gComponentsArray = [vacuumParticipant];
+let NSGetFactory = XPCOMUtils.generateNSGetFactory(gComponentsArray);
new file mode 100644
--- /dev/null
+++ b/storage/test/unit/vacuumParticipant.manifest
@@ -0,0 +1,3 @@
+component {52aa0b22-b82f-4e38-992a-c3675a3355d2} vacuumParticipant.js
+contract @unit.test.com/test-vacuum-participant;1 {52aa0b22-b82f-4e38-992a-c3675a3355d2}
+category vacuum-participant vacuumParticipant @unit.test.com/test-vacuum-participant;1