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
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