Bug 1371677 - Delay the database connection in the history service as far as possible. r=adw
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 09 Jun 2017 18:51:09 +0200
changeset 368248 43bfc32f2623cb8c09b97016fbde25d387d07640
parent 368247 42a5f04365621b56895b2fe1dadcbb709e485687
child 368249 1e38bde37558e4eb1c5066a918914f640f0720a1
push id46251
push usermak77@bonardo.net
push dateTue, 11 Jul 2017 12:59:49 +0000
treeherderautoland@43bfc32f2623 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1371677
milestone56.0a1
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 1371677 - Delay the database connection in the history service as far as possible. r=adw Makes initing Places services cheaper, by delaying the connection creation to the first time it's actually needed. Same way, delays reading the bookmark roots at the first time they are requested. Deprecates the concept of lazy observers, since they are no more needed, we can just use addObserver. Simplifies the startup path: always sends "places-init-complete" (both as a category and a topic) when the connection starts and adds a "locked" database state when we can't get a working connection. Makes PlacesCategoriesStarter register for the new category, since it's cheaper than being a bookmarks observer. Fixes a couple race conditions in keywords and expiration due to new startup timings. Removes a test in test_keywords.js that is no more easily feasible, since it'd requires a pre-build places.sqlite that should be kept up-to-date at every version. MozReview-Commit-ID: 6ccPUZ651m0
browser/base/content/browser-places.js
browser/components/nsBrowserGlue.js
browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
browser/components/places/tests/unit/test_browserGlue_distribution.js
browser/components/places/tests/unit/test_browserGlue_prefs.js
browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/Helpers.cpp
toolkit/components/places/Helpers.h
toolkit/components/places/History.cpp
toolkit/components/places/History.h
toolkit/components/places/PlacesCategoriesStarter.js
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/Shutdown.cpp
toolkit/components/places/nsAnnotationService.cpp
toolkit/components/places/nsFaviconService.cpp
toolkit/components/places/nsINavHistoryService.idl
toolkit/components/places/nsLivemarkService.js
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavBookmarks.h
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/nsPlacesExpiration.js
toolkit/components/places/tests/bookmarks/test_keywords.js
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js
toolkit/components/places/tests/unit/test_history_catobs.js
toolkit/components/places/tests/unit/test_history_clear.js
toolkit/components/places/tests/unit/test_history_notifications.js
toolkit/components/places/tests/unit/test_keywords.js
toolkit/components/places/tests/unit/xpcshell.ini
toolkit/components/places/toolkitplaces.manifest
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1678,17 +1678,17 @@ var BookmarkingUI = {
   _itemGuids: new Set(),
   uninit: function BUI_uninit() {
     this.updateBookmarkPageMenuItem(true);
     CustomizableUI.removeListener(this);
 
     this._uninitView();
 
     if (this._hasBookmarksObserver) {
-      PlacesUtils.removeLazyBookmarkObserver(this);
+      PlacesUtils.bookmarks.removeObserver(this);
     }
 
     if (this._pendingUpdate) {
       delete this._pendingUpdate;
     }
   },
 
   onLocationChange: function BUI_onLocationChange() {
@@ -1723,17 +1723,17 @@ var BookmarkingUI = {
            this._itemGuids = guids;
          }
 
          this._updateStar();
 
          // Start observing bookmarks if needed.
          if (!this._hasBookmarksObserver) {
            try {
-             PlacesUtils.addLazyBookmarkObserver(this);
+             PlacesUtils.bookmarks.addObserver(this);
              this._hasBookmarksObserver = true;
            } catch (ex) {
              Components.utils.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
            }
          }
 
          delete this._pendingUpdate;
        });
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -267,20 +267,16 @@ function BrowserGlue() {
 /*
  * OS X has the concept of zero-window sessions and therefore ignores the
  * browser-lastwindow-close-* topics.
  */
 const OBSERVE_LASTWINDOW_CLOSE_TOPICS = AppConstants.platform != "macosx";
 
 BrowserGlue.prototype = {
   _saveSession: false,
-  _isPlacesInitObserver: false,
-  _isPlacesLockedObserver: false,
-  _isPlacesShutdownObserver: false,
-  _isPlacesDatabaseLocked: false,
   _migrationImportsDefaultBookmarks: false,
 
   _setPrefToSaveSession: function BG__setPrefToSaveSession(aForce) {
     if (!this._saveSession && !aForce)
       return;
 
     Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true);
 
@@ -379,31 +375,19 @@ BrowserGlue.prototype = {
         this._onDisplaySyncURIs(subject);
         break;
       case "session-save":
         this._setPrefToSaveSession(true);
         subject.QueryInterface(Ci.nsISupportsPRBool);
         subject.data = true;
         break;
       case "places-init-complete":
+        Services.obs.removeObserver(this, "places-init-complete");
         if (!this._migrationImportsDefaultBookmarks)
           this._initPlaces(false);
-
-        Services.obs.removeObserver(this, "places-init-complete");
-        this._isPlacesInitObserver = false;
-        // no longer needed, since history was initialized completely.
-        Services.obs.removeObserver(this, "places-database-locked");
-        this._isPlacesLockedObserver = false;
-        break;
-      case "places-database-locked":
-        this._isPlacesDatabaseLocked = true;
-        // Stop observing, so further attempts to load history service
-        // will not show the prompt.
-        Services.obs.removeObserver(this, "places-database-locked");
-        this._isPlacesLockedObserver = false;
         break;
       case "idle":
         this._backupBookmarks();
         break;
       case "distribution-customization-complete":
         Services.obs.removeObserver(this, "distribution-customization-complete");
         // Customization has finished, we don't need the customizer anymore.
         delete this._distributionCustomizer;
@@ -530,19 +514,16 @@ BrowserGlue.prototype = {
     os.addObserver(this, "weave:service:ready");
     os.addObserver(this, "fxaccounts:onverified");
     os.addObserver(this, "fxaccounts:device_connected");
     os.addObserver(this, "fxaccounts:verify_login");
     os.addObserver(this, "fxaccounts:device_disconnected");
     os.addObserver(this, "weave:engine:clients:display-uris");
     os.addObserver(this, "session-save");
     os.addObserver(this, "places-init-complete");
-    this._isPlacesInitObserver = true;
-    os.addObserver(this, "places-database-locked");
-    this._isPlacesLockedObserver = true;
     os.addObserver(this, "distribution-customization-complete");
     os.addObserver(this, "handle-xul-text-link");
     os.addObserver(this, "profile-before-change");
     os.addObserver(this, "keyword-search");
     os.addObserver(this, "browser-search-engine-modified");
     os.addObserver(this, "restart-in-safe-mode");
     os.addObserver(this, "flash-plugin-hang");
     os.addObserver(this, "xpi-signature-changed");
@@ -584,20 +565,19 @@ BrowserGlue.prototype = {
     if (this._bookmarksBackupIdleTime) {
       this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
       delete this._bookmarksBackupIdleTime;
     }
     if (this._mediaTelemetryIdleObserver) {
       this._idleService.removeIdleObserver(this._mediaTelemetryIdleObserver, MEDIA_TELEMETRY_IDLE_TIME_SEC);
       delete this._mediaTelemetryIdleObserver;
     }
-    if (this._isPlacesInitObserver)
+    try {
       os.removeObserver(this, "places-init-complete");
-    if (this._isPlacesLockedObserver)
-      os.removeObserver(this, "places-database-locked");
+    } catch (ex) { /* Could have been removed already */ }
     os.removeObserver(this, "handle-xul-text-link");
     os.removeObserver(this, "profile-before-change");
     os.removeObserver(this, "keyword-search");
     os.removeObserver(this, "browser-search-engine-modified");
     os.removeObserver(this, "flash-plugin-hang");
     os.removeObserver(this, "xpi-signature-changed");
     os.removeObserver(this, "sync-ui-state:update");
   },
@@ -1075,22 +1055,16 @@ BrowserGlue.prototype = {
     }
 
     this._initServiceDiscovery();
 
     // Show update notification, if needed.
     if (Services.prefs.prefHasUserValue("app.update.postupdate"))
       this._showUpdateNotification();
 
-    // Load the "more info" page for a locked places.sqlite
-    // This property is set earlier by places-database-locked topic.
-    if (this._isPlacesDatabaseLocked) {
-      this._showPlacesLockedNotificationBox();
-    }
-
     ExtensionsUI.init();
 
     let signingRequired;
     if (AppConstants.MOZ_REQUIRE_SIGNING) {
       signingRequired = true;
     } else {
       signingRequired = Services.prefs.getBoolPref("xpinstall.signatures.required");
     }
@@ -1483,16 +1457,28 @@ BrowserGlue.prototype = {
    */
   _initPlaces: function BG__initPlaces(aInitialMigrationPerformed) {
     // We must instantiate the history service since it will tell us if we
     // need to import or restore bookmarks due to first-run, corruption or
     // forced migration (due to a major schema change).
     // If the database is corrupt or has been newly created we should
     // import bookmarks.
     let dbStatus = PlacesUtils.history.databaseStatus;
+
+    // Show a notification with a "more info" link for a locked places.sqlite.
+    if (dbStatus == PlacesUtils.history.DATABASE_STATUS_LOCKED) {
+      // Note: initPlaces should always happen when the first window is ready,
+      // in any case, better safe than sorry.
+      this._firstWindowReady.then(() => {
+        this._showPlacesLockedNotificationBox();
+        Services.obs.notifyObservers(null, "places-browser-init-complete");
+      });
+      return;
+    }
+
     let importBookmarks = !aInitialMigrationPerformed &&
                           (dbStatus == PlacesUtils.history.DATABASE_STATUS_CREATE ||
                            dbStatus == PlacesUtils.history.DATABASE_STATUS_CORRUPT);
 
     // Check if user or an extension has required to import bookmarks.html
     let importBookmarksHTML = false;
     try {
       importBookmarksHTML =
--- a/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
+++ b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
@@ -4,30 +4,26 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  * Tests that nsBrowserGlue correctly exports bookmarks.html at shutdown if
  * browser.bookmarks.autoExportHTML is set to true.
  */
 
-function run_test() {
-  run_next_test();
-}
-
 add_task(async function() {
   remove_bookmarks_html();
 
   Services.prefs.setBoolPref("browser.bookmarks.autoExportHTML", true);
   do_register_cleanup(() => Services.prefs.clearUserPref("browser.bookmarks.autoExportHTML"));
 
   // Initialize nsBrowserGlue before Places.
   Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
 
   // Initialize Places through the History Service.
-  Cc["@mozilla.org/browser/nav-history-service;1"]
-    .getService(Ci.nsINavHistoryService);
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CREATE);
 
   Services.obs.addObserver(function observer() {
     Services.obs.removeObserver(observer, "profile-before-change");
     check_bookmarks_html();
   }, "profile-before-change");
 });
--- a/browser/components/places/tests/unit/test_browserGlue_distribution.js
+++ b/browser/components/places/tests/unit/test_browserGlue_distribution.js
@@ -46,23 +46,22 @@ do_register_cleanup(function() {
   }
   Assert.ok(!iniFile.exists());
 });
 
 add_task(async function() {
   // Disable Smart Bookmarks creation.
   Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1);
 
+  let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver)
   // Initialize Places through the History Service and check that a new
   // database has been created.
   Assert.equal(PlacesUtils.history.databaseStatus,
                PlacesUtils.history.DATABASE_STATUS_CREATE);
-
   // Force distribution.
-  let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver)
   glue.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_DISTRIBUTION_CUSTOMIZATION);
 
   // Test will continue on customization complete notification.
   await promiseTopicObserved(TOPIC_CUSTOMIZATION_COMPLETE);
 
   // Check the custom bookmarks exist on menu.
   let menuItem = await PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
--- a/browser/components/places/tests/unit/test_browserGlue_prefs.js
+++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js
@@ -12,50 +12,47 @@ const PREF_SMART_BOOKMARKS_VERSION = "br
 const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
 
 const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
 const TOPICDATA_FORCE_PLACES_INIT = "force-places-init";
 
 var bg = Cc["@mozilla.org/browser/browserglue;1"].
          getService(Ci.nsIObserver);
 
-function run_test() {
+add_task(async function setup() {
   // Create our bookmarks.html from bookmarks.glue.html.
   create_bookmarks_html("bookmarks.glue.html");
 
   remove_all_JSON_backups();
 
   // Create our JSON backup from bookmarks.glue.json.
   create_JSON_backup("bookmarks.glue.json");
 
-  run_next_test();
-}
+  do_register_cleanup(function() {
+    remove_bookmarks_html();
+    remove_all_JSON_backups();
 
-do_register_cleanup(function() {
-  remove_bookmarks_html();
-  remove_all_JSON_backups();
-
-  return PlacesUtils.bookmarks.eraseEverything();
+    return PlacesUtils.bookmarks.eraseEverything();
+  });
 });
 
 function simulatePlacesInit() {
   do_print("Simulate Places init");
   // Force nsBrowserGlue::_initPlaces().
   bg.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_FORCE_PLACES_INIT);
   return promiseTopicObserved("places-browser-init-complete");
 }
 
 add_task(async function test_checkPreferences() {
   // Initialize Places through the History Service and check that a new
   // database has been created.
+  let promiseComplete = promiseTopicObserved("places-browser-init-complete");
   Assert.equal(PlacesUtils.history.databaseStatus,
                PlacesUtils.history.DATABASE_STATUS_CREATE);
-
-  // Wait for Places init notification.
-  await promiseTopicObserved("places-browser-init-complete");
+  await promiseComplete;
 
   // Ensure preferences status.
   Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
 
   Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
   Assert.throws(() => Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
 });
 
--- a/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
+++ b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
@@ -34,21 +34,22 @@ function countFolderChildren(aFolderItem
   rootNode.containerOpen = false;
   return cc;
 }
 
 add_task(async function setup() {
   // Initialize browserGlue, but remove it's listener to places-init-complete.
   Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
 
-  // Initialize Places.
-  PlacesUtils.history;
-
-  // Wait for Places init notification.
-  await promiseTopicObserved("places-browser-init-complete");
+  // Initialize Places through the History Service and check that a new
+  // database has been created.
+  let promiseComplete = promiseTopicObserved("places-browser-init-complete");
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CREATE);
+  await promiseComplete;
 
   // Ensure preferences status.
   Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
   Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
   Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
 });
 
 add_task(async function test_version_0() {
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -408,16 +408,17 @@ Database::Database()
   , mMainThreadAsyncStatements(mMainConn)
   , mAsyncThreadStatements(mMainConn)
   , mDBPageSize(0)
   , mDatabaseStatus(nsINavHistoryService::DATABASE_STATUS_OK)
   , mClosed(false)
   , mClientsShutdown(new ClientsShutdownBlocker())
   , mConnectionShutdown(new ConnectionShutdownBlocker(this))
   , mMaxUrlLength(0)
+  , mCacheObservers(TOPIC_PLACES_INIT_COMPLETE)
 {
   MOZ_ASSERT(!XRE_IsContentProcess(),
              "Cannot instantiate Places in the content process");
   // Attempting to create two instances of the service?
   MOZ_ASSERT(!gDatabase);
   gDatabase = this;
 }
 
@@ -465,34 +466,41 @@ Database::IsShutdownStarted() const
   if (!mConnectionShutdown) {
     // We have already broken the cycle between `this` and `mConnectionShutdown`.
     return true;
   }
   return mConnectionShutdown->IsStarted();
 }
 
 already_AddRefed<mozIStorageAsyncStatement>
-Database::GetAsyncStatement(const nsACString& aQuery) const
+Database::GetAsyncStatement(const nsACString& aQuery)
 {
-  if (IsShutdownStarted()) {
+  if (IsShutdownStarted() || NS_FAILED(EnsureConnection())) {
     return nullptr;
   }
+
   MOZ_ASSERT(NS_IsMainThread());
   return mMainThreadAsyncStatements.GetCachedStatement(aQuery);
 }
 
 already_AddRefed<mozIStorageStatement>
-Database::GetStatement(const nsACString& aQuery) const
+Database::GetStatement(const nsACString& aQuery)
 {
   if (IsShutdownStarted()) {
     return nullptr;
   }
   if (NS_IsMainThread()) {
+    if (NS_FAILED(EnsureConnection())) {
+      return nullptr;
+    }
     return mMainThreadStatements.GetCachedStatement(aQuery);
   }
+  // In the async case, the connection must have been started on the main-thread
+  // already.
+  MOZ_ASSERT(mMainConn);
   return mAsyncThreadStatements.GetCachedStatement(aQuery);
 }
 
 already_AddRefed<nsIAsyncShutdownClient>
 Database::GetClientsShutdown()
 {
   MOZ_ASSERT(mClientsShutdown);
   return mClientsShutdown->GetClient();
@@ -508,112 +516,19 @@ Database::GetDatabase()
   return GetSingleton();
 }
 
 nsresult
 Database::Init()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  bool initSucceeded = false;
-  auto guard = MakeScopeExit([&]() {
-    if (!initSucceeded) {
-      // If we bail out early here without doing anything else, the Database object
-      // will leak due to the cycle between the shutdown blockers and itself, so
-      // let's break the cycle first.
-      Shutdown(false);
-    }
-  });
-
-  nsCOMPtr<mozIStorageService> storage =
-    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
-  NS_ENSURE_STATE(storage);
-
-  // Init the database file and connect to it.
-  bool databaseCreated = false;
-  nsresult rv = InitDatabaseFile(storage, &databaseCreated);
-  if (NS_SUCCEEDED(rv) && databaseCreated) {
-    mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CREATE;
-  }
-  else if (rv == NS_ERROR_FILE_CORRUPTED) {
-    // The database is corrupt, backup and replace it with a new one.
-    mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
-    rv = BackupAndReplaceDatabaseFile(storage);
-    // Fallback to catch-all handler, that notifies a database locked failure.
-  }
-
-  // If the database connection still cannot be opened, it may just be locked
-  // by third parties.  Send out a notification and interrupt initialization.
-  if (NS_FAILED(rv)) {
-    RefPtr<PlacesEvent> lockedEvent = new PlacesEvent(TOPIC_DATABASE_LOCKED);
-    (void)NS_DispatchToMainThread(lockedEvent);
-    return rv;
-  }
-
-  // Ensure the icons database exists.
-  rv = EnsureFaviconsDatabaseFile(storage);
-  if (NS_FAILED(rv)) {
-    RefPtr<PlacesEvent> lockedEvent = new PlacesEvent(TOPIC_DATABASE_LOCKED);
-    (void)NS_DispatchToMainThread(lockedEvent);
-    return rv;
-  }
-
-  // Initialize the database schema.  In case of failure the existing schema is
-  // is corrupt or incoherent, thus the database should be replaced.
-  bool databaseMigrated = false;
-  rv = SetupDatabaseConnection(storage);
-  if (NS_SUCCEEDED(rv)) {
-    // Failing to initialize the schema always indicates a corruption.
-    if (NS_FAILED(InitSchema(&databaseMigrated))) {
-      rv = NS_ERROR_FILE_CORRUPTED;
-    }
-  }
-  if (NS_FAILED(rv)) {
-    mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
-    // Some errors may not indicate a database corruption, for those cases we
-    // just bail out without throwing away a possibly valid places.sqlite.
-    if (rv == NS_ERROR_FILE_CORRUPTED) {
-      rv = BackupAndReplaceDatabaseFile(storage);
-      NS_ENSURE_SUCCESS(rv, rv);
-      // Try to initialize the new database again.
-      rv = SetupDatabaseConnection(storage);
-      NS_ENSURE_SUCCESS(rv, rv);
-      rv = InitSchema(&databaseMigrated);
-    }
-    // Bail out if we couldn't fix the database.
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  if (databaseMigrated) {
-    mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_UPGRADED;
-  }
-
-  if (mDatabaseStatus != nsINavHistoryService::DATABASE_STATUS_OK) {
-    rv = updateSQLiteStatistics(MainConn());
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  // Initialize here all the items that are not part of the on-disk database,
-  // like views, temp triggers or temp tables.  The database should not be
-  // considered corrupt if any of the following fails.
-
-  rv = InitTempEntities();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // Notify we have finished database initialization.
-  // Enqueue the notification, so if we init another service that requires
-  // nsNavHistoryService we don't recursive try to get it.
-  RefPtr<PlacesEvent> completeEvent =
-    new PlacesEvent(TOPIC_PLACES_INIT_COMPLETE);
-  rv = NS_DispatchToMainThread(completeEvent);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // At this point we know the Database object points to a valid connection
-  // and we need to setup async shutdown.
-  initSucceeded = true;
+  // DO NOT FAIL HERE, otherwise we would never break the cycle between this
+  // object and the shutdown blockers, causing unexpected leaks.
+
   {
     // First of all Places clients should block profile-change-teardown.
     nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileChangeTeardownPhase();
     MOZ_ASSERT(shutdownPhase);
     if (shutdownPhase) {
       DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
         static_cast<nsIAsyncShutdownBlocker*>(mClientsShutdown.get()),
         NS_LITERAL_STRING(__FILE__),
@@ -637,17 +552,133 @@ Database::Init()
     }
   }
 
   // Finally observe profile shutdown notifications.
   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
   if (os) {
     (void)os->AddObserver(this, TOPIC_PROFILE_CHANGE_TEARDOWN, true);
   }
-
+  return NS_OK;
+}
+
+nsresult
+Database::EnsureConnection()
+{
+  // Run this only once.
+  if (mMainConn ||
+      mDatabaseStatus == nsINavHistoryService::DATABASE_STATUS_LOCKED) {
+    return NS_OK;
+  }
+  // Don't try to create a database too late.
+  if (IsShutdownStarted()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  MOZ_ASSERT(NS_IsMainThread(),
+             "Database initialization must happen on the main-thread");
+
+  {
+    bool initSucceeded = false;
+    auto notify = MakeScopeExit([&] () {
+      // If the database connection cannot be opened, it may just be locked
+      // by third parties.  Set a locked state.
+      if (!initSucceeded) {
+        mMainConn = nullptr;
+        mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_LOCKED;
+      }
+      // Notify at the next tick, to avoid re-entrancy problems.
+      NS_DispatchToMainThread(
+        NewRunnableMethod("places::Database::EnsureConnection()",
+                          this, &Database::NotifyConnectionInitalized)
+      );
+    });
+
+    nsCOMPtr<mozIStorageService> storage =
+      do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+    NS_ENSURE_STATE(storage);
+
+    // Init the database file and connect to it.
+    bool databaseCreated = false;
+    nsresult rv = InitDatabaseFile(storage, &databaseCreated);
+    if (NS_SUCCEEDED(rv) && databaseCreated) {
+      mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CREATE;
+    }
+    else if (rv == NS_ERROR_FILE_CORRUPTED) {
+      // The database is corrupt, backup and replace it with a new one.
+      mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
+      rv = BackupAndReplaceDatabaseFile(storage);
+      // Fallback to catch-all handler.
+    }
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Ensure the icons database exists.
+    rv = EnsureFaviconsDatabaseFile(storage);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Initialize the database schema.  In case of failure the existing schema is
+    // is corrupt or incoherent, thus the database should be replaced.
+    bool databaseMigrated = false;
+    rv = SetupDatabaseConnection(storage);
+    if (NS_SUCCEEDED(rv)) {
+      // Failing to initialize the schema always indicates a corruption.
+      if (NS_FAILED(InitSchema(&databaseMigrated))) {
+        rv = NS_ERROR_FILE_CORRUPTED;
+      }
+    }
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
+      // Some errors may not indicate a database corruption, for those cases we
+      // just bail out without throwing away a possibly valid places.sqlite.
+      if (rv == NS_ERROR_FILE_CORRUPTED) {
+        rv = BackupAndReplaceDatabaseFile(storage);
+        NS_ENSURE_SUCCESS(rv, rv);
+        // Try to initialize the new database again.
+        rv = SetupDatabaseConnection(storage);
+        NS_ENSURE_SUCCESS(rv, rv);
+        rv = InitSchema(&databaseMigrated);
+      }
+      // Bail out if we couldn't fix the database.
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+
+    if (databaseMigrated) {
+      mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_UPGRADED;
+    }
+
+    if (mDatabaseStatus == nsINavHistoryService::DATABASE_STATUS_UPGRADED ||
+        mDatabaseStatus == nsINavHistoryService::DATABASE_STATUS_CREATE) {
+      MOZ_ALWAYS_SUCCEEDS(updateSQLiteStatistics(mMainConn));
+    }
+
+    // Initialize here all the items that are not part of the on-disk database,
+    // like views, temp triggers or temp tables.  The database should not be
+    // considered corrupt if any of the following fails.
+
+    rv = InitTempEntities();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    initSucceeded = true;
+  }
+  return NS_OK;
+}
+
+nsresult
+Database::NotifyConnectionInitalized()
+{
+  // Notify about Places initialization.
+  nsCOMArray<nsIObserver> entries;
+  mCacheObservers.GetEntries(entries);
+  for (int32_t idx = 0; idx < entries.Count(); ++idx) {
+    MOZ_ALWAYS_SUCCEEDS(entries[idx]->Observe(nullptr, TOPIC_PLACES_INIT_COMPLETE, nullptr));
+  }
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    MOZ_ALWAYS_SUCCEEDS(obs->NotifyObservers(nullptr, TOPIC_PLACES_INIT_COMPLETE, nullptr));
+  }
   return NS_OK;
 }
 
 nsresult
 Database::EnsureFaviconsDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -666,55 +697,57 @@ Database::EnsureFaviconsDatabaseFile(nsC
     return NS_OK;
   }
 
   // Open the database file, this will also create it.
   nsCOMPtr<mozIStorageConnection> conn;
   rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(conn));
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Ensure we'll close the connection when done.
-  auto cleanup = MakeScopeExit([&] () {
-    // We cannot use AsyncClose() here, because by the time we try to ATTACH
-    // this database, its transaction could be still be running and that would
-    // cause the ATTACH query to fail.
-    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(conn->Close()));
-  });
-
-  int32_t defaultPageSize;
-  rv = conn->GetDefaultPageSize(&defaultPageSize);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = SetupDurability(conn, defaultPageSize);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // Enable incremental vacuum for this database. Since it will contain even
-  // large blobs and can be cleared with history, it's worth to have it.
-  // Note that it will be necessary to manually use PRAGMA incremental_vacuum.
-  rv = conn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
-    "PRAGMA auto_vacuum = INCREMENTAL"
-  ));
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // We are going to update the database, so everything from now on should be
-  // in a transaction for performances.
-  mozStorageTransaction transaction(conn, false);
-  rv = conn->ExecuteSimpleSQL(CREATE_MOZ_ICONS);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = conn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ICONS_ICONURLHASH);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = conn->ExecuteSimpleSQL(CREATE_MOZ_PAGES_W_ICONS);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = conn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PAGES_W_ICONS_ICONURLHASH);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = conn->ExecuteSimpleSQL(CREATE_MOZ_ICONS_TO_PAGES);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = transaction.Commit();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // The scope exit will take care of closing the connection.
+  {
+    // Ensure we'll close the connection when done.
+    auto cleanup = MakeScopeExit([&] () {
+      // We cannot use AsyncClose() here, because by the time we try to ATTACH
+      // this database, its transaction could be still be running and that would
+      // cause the ATTACH query to fail.
+      MOZ_ALWAYS_TRUE(NS_SUCCEEDED(conn->Close()));
+    });
+
+    int32_t defaultPageSize;
+    rv = conn->GetDefaultPageSize(&defaultPageSize);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = SetupDurability(conn, defaultPageSize);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Enable incremental vacuum for this database. Since it will contain even
+    // large blobs and can be cleared with history, it's worth to have it.
+    // Note that it will be necessary to manually use PRAGMA incremental_vacuum.
+    rv = conn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "PRAGMA auto_vacuum = INCREMENTAL"
+    ));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // We are going to update the database, so everything from now on should be
+    // in a transaction for performances.
+    mozStorageTransaction transaction(conn, false);
+    rv = conn->ExecuteSimpleSQL(CREATE_MOZ_ICONS);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = conn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ICONS_ICONURLHASH);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = conn->ExecuteSimpleSQL(CREATE_MOZ_PAGES_W_ICONS);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = conn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PAGES_W_ICONS_ICONURLHASH);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = conn->ExecuteSimpleSQL(CREATE_MOZ_ICONS_TO_PAGES);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = transaction.Commit();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // The scope exit will take care of closing the connection.
+  }
 
   return NS_OK;
 }
 
 nsresult
 Database::InitDatabaseFile(nsCOMPtr<mozIStorageService>& aStorage,
                            bool* aNewDatabaseCreated)
 {
@@ -2502,17 +2535,17 @@ Database::CreateMobileRoot()
 
   rv = addAnnoStmt->Execute();
   if (NS_FAILED(rv)) return -1;
 
   return rootId;
 }
 
 void
-Database::Shutdown(bool aInitSucceeded)
+Database::Shutdown()
 {
   // As the last step in the shutdown path, finalize the database handle.
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mClosed);
 
   // Break cycles with the shutdown blockers.
   mClientsShutdown = nullptr;
   nsCOMPtr<mozIStorageCompletionCallback> connectionShutdown = mConnectionShutdown.forget();
@@ -2520,17 +2553,17 @@ Database::Shutdown(bool aInitSucceeded)
   if (!mMainConn) {
     // The connection has never been initialized. Just mark it as closed.
     mClosed = true;
     (void)connectionShutdown->Complete(NS_OK, nullptr);
     return;
   }
 
 #ifdef DEBUG
-  if (aInitSucceeded) {
+  {
     bool hasResult;
     nsCOMPtr<mozIStorageStatement> stmt;
 
     // Sanity check for missing guids.
     nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
       "SELECT 1 "
       "FROM moz_places "
       "WHERE guid IS NULL "
@@ -2589,16 +2622,17 @@ Database::Shutdown(bool aInitSucceeded)
           mAsyncThreadStatements,
           NS_ISUPPORTS_CAST(nsIObserver*, this)
         );
   DispatchToAsyncThread(event);
 
   mClosed = true;
 
   (void)mMainConn->AsyncClose(connectionShutdown);
+  mMainConn = nullptr;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIObserver
 
 NS_IMETHODIMP
 Database::Observe(nsISupports *aSubject,
                   const char *aTopic,
@@ -2653,18 +2687,18 @@ Database::Observe(nsISupports *aSubject,
         shutdownPhase->RemoveBlocker(mClientsShutdown.get());
       }
       (void)mClientsShutdown->BlockShutdown(nullptr);
     }
 
     // Spin the events loop until the clients are done.
     // Note, this is just for tests, specifically test_clearHistory_shutdown.js
     SpinEventLoopUntil([&]() {
-	return mClientsShutdown->State() == PlacesShutdownBlocker::States::RECEIVED_DONE;
-      });
+      return mClientsShutdown->State() == PlacesShutdownBlocker::States::RECEIVED_DONE;
+    });
 
     {
       nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileBeforeChangePhase();
       if (shutdownPhase) {
         shutdownPhase->RemoveBlocker(mConnectionShutdown.get());
       }
       (void)mConnectionShutdown->BlockShutdown(nullptr);
     }
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -10,25 +10,24 @@
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIObserver.h"
 #include "nsIAsyncShutdown.h"
 #include "mozilla/storage.h"
 #include "mozilla/storage/StatementCache.h"
 #include "mozilla/Attributes.h"
 #include "nsIEventTarget.h"
 #include "Shutdown.h"
+#include "nsCategoryCache.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
 #define DATABASE_SCHEMA_VERSION 38
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
-// Fired when initialization fails due to a locked database.
-#define TOPIC_DATABASE_LOCKED "places-database-locked"
 // This topic is received when the profile is about to be lost.  Places does
 // initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
 // Any shutdown work that requires the Places APIs should happen here.
 #define TOPIC_PROFILE_CHANGE_TEARDOWN "profile-change-teardown"
 // Fired when Places is shutting down.  Any code should stop accessing Places
 // APIs after this notification.  If you need to listen for Places shutdown
 // you should only use this notification, next ones are intended only for
 // internal Places use.
@@ -90,45 +89,57 @@ public:
   /**
    * Getter to use when instantiating the class.
    *
    * @return Singleton instance of this class.
    */
   static already_AddRefed<Database> GetDatabase();
 
   /**
+   * Actually initialized the connection on first need.
+   */
+  nsresult EnsureConnection();
+
+  /**
+   * Notifies that the connection has been initialized.
+   */
+  nsresult NotifyConnectionInitalized();
+
+  /**
    * Returns last known database status.
    *
    * @return one of the nsINavHistoryService::DATABASE_STATUS_* constants.
    */
-  uint16_t GetDatabaseStatus() const
+  uint16_t GetDatabaseStatus()
   {
+    mozilla::Unused << EnsureConnection();
     return mDatabaseStatus;
   }
 
   /**
    * Returns a pointer to the storage connection.
    *
    * @return The connection handle.
    */
-  mozIStorageConnection* MainConn() const
+  mozIStorageConnection* MainConn()
   {
+    mozilla::Unused << EnsureConnection();
     return mMainConn;
   }
 
   /**
    * Dispatches a runnable to the connection async thread, to be serialized
    * with async statements.
    *
    * @param aEvent
    *        The runnable to be dispatched.
    */
-  void DispatchToAsyncThread(nsIRunnable* aEvent) const
+  void DispatchToAsyncThread(nsIRunnable* aEvent)
   {
-    if (mClosed) {
+    if (mClosed || NS_FAILED(EnsureConnection())) {
       return;
     }
     nsCOMPtr<nsIEventTarget> target = do_GetInterface(mMainConn);
     if (target) {
       (void)target->Dispatch(aEvent, NS_DISPATCH_NORMAL);
     }
   }
 
@@ -141,71 +152,69 @@ public:
    * @param aQuery
    *        SQL query literal.
    * @return The cached statement.
    * @note Always null check the result.
    * @note Always use a scoper to reset the statement.
    */
   template<int N>
   already_AddRefed<mozIStorageStatement>
-  GetStatement(const char (&aQuery)[N]) const
+  GetStatement(const char (&aQuery)[N])
   {
     nsDependentCString query(aQuery, N - 1);
     return GetStatement(query);
   }
 
   /**
    * Gets a cached synchronous statement.
    *
    * @param aQuery
    *        nsCString of SQL query.
    * @return The cached statement.
    * @note Always null check the result.
    * @note Always use a scoper to reset the statement.
    */
-  already_AddRefed<mozIStorageStatement>  GetStatement(const nsACString& aQuery) const;
+  already_AddRefed<mozIStorageStatement>  GetStatement(const nsACString& aQuery);
 
   /**
    * Gets a cached asynchronous statement.
    *
    * @param aQuery
    *        SQL query literal.
    * @return The cached statement.
    * @note Always null check the result.
    * @note AsyncStatements are automatically reset on execution.
    */
   template<int N>
   already_AddRefed<mozIStorageAsyncStatement>
-  GetAsyncStatement(const char (&aQuery)[N]) const
+  GetAsyncStatement(const char (&aQuery)[N])
   {
     nsDependentCString query(aQuery, N - 1);
     return GetAsyncStatement(query);
   }
 
   /**
    * Gets a cached asynchronous statement.
    *
    * @param aQuery
    *        nsCString of SQL query.
    * @return The cached statement.
    * @note Always null check the result.
    * @note AsyncStatements are automatically reset on execution.
    */
-  already_AddRefed<mozIStorageAsyncStatement> GetAsyncStatement(const nsACString& aQuery) const;
+  already_AddRefed<mozIStorageAsyncStatement> GetAsyncStatement(const nsACString& aQuery);
 
   uint32_t MaxUrlLength();
 
 protected:
   /**
    * Finalizes the cached statements and closes the database connection.
    * A TOPIC_PLACES_CONNECTION_CLOSED notification is fired when done.
-   *
-   * @param Whether database init succeeded.
    */
-  void Shutdown(bool aInitSucceeded);
+  void Shutdown();
 
   bool IsShutdownStarted() const;
 
   /**
    * Initializes the database file.  If the database does not exist or is
    * corrupt, a new one is created.  In case of corruption it also creates a
    * backup copy of the database.
    *
@@ -340,14 +349,17 @@ private:
   RefPtr<ClientsShutdownBlocker> mClientsShutdown;
   RefPtr<ConnectionShutdownBlocker> mConnectionShutdown;
 
   // Maximum length of a stored url.
   // For performance reasons we don't store very long urls in history, since
   // they are slower to search through and cause abnormal database growth,
   // affecting the awesomebar fetch time.
   uint32_t mMaxUrlLength;
+
+  // Used to initialize components on places startup.
+  nsCategoryCache<nsIObserver> mCacheObservers;
 };
 
 } // namespace places
 } // namespace mozilla
 
 #endif // mozilla_places_Database_h_
--- a/toolkit/components/places/Helpers.cpp
+++ b/toolkit/components/places/Helpers.cpp
@@ -316,47 +316,16 @@ GetHiddenState(bool aIsRedirect,
                uint32_t aTransitionType)
 {
   return aTransitionType == nsINavHistoryService::TRANSITION_FRAMED_LINK ||
          aTransitionType == nsINavHistoryService::TRANSITION_EMBED ||
          aIsRedirect;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
-//// PlacesEvent
-
-PlacesEvent::PlacesEvent(const char* aTopic)
-  : Runnable("places::PlacesEvent")
-  , mTopic(aTopic)
-{
-}
-
-NS_IMETHODIMP
-PlacesEvent::Run()
-{
-  Notify();
-  return NS_OK;
-}
-
-void
-PlacesEvent::Notify()
-{
-  NS_ASSERTION(NS_IsMainThread(), "Must only be used on the main thread!");
-  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
-  if (obs) {
-    (void)obs->NotifyObservers(nullptr, mTopic, nullptr);
-  }
-}
-
-NS_IMPL_ISUPPORTS_INHERITED0(
-  PlacesEvent
-, Runnable
-)
-
-////////////////////////////////////////////////////////////////////////////////
 //// AsyncStatementCallbackNotifier
 
 NS_IMETHODIMP
 AsyncStatementCallbackNotifier::HandleCompletion(uint16_t aReason)
 {
   if (aReason != mozIStorageStatementCallback::REASON_FINISHED)
     return NS_ERROR_UNEXPECTED;
 
--- a/toolkit/components/places/Helpers.h
+++ b/toolkit/components/places/Helpers.h
@@ -225,33 +225,16 @@ protected:
  * @param aTransitionType
  *        The transition type of the visit.
  * @return true if this visit should be hidden.
  */
 bool GetHiddenState(bool aIsRedirect,
                     uint32_t aTransitionType);
 
 /**
- * Notifies a specified topic via the observer service.
- */
-class PlacesEvent : public Runnable
-{
-public:
-  NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_NSIRUNNABLE
-
-  explicit PlacesEvent(const char* aTopic);
-protected:
-  ~PlacesEvent() {}
-  void Notify();
-
-  const char* const mTopic;
-};
-
-/**
  * Used to notify a topic to system observers on async execute completion.
  */
 class AsyncStatementCallbackNotifier : public AsyncStatementCallback
 {
 public:
   explicit AsyncStatementCallbackNotifier(const char* aTopic)
     : mTopic(aTopic)
   {
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -2345,21 +2345,36 @@ History::GetSingleton()
 
   NS_ADDREF(gService);
   return gService;
 }
 
 mozIStorageConnection*
 History::GetDBConn()
 {
+  MOZ_ASSERT(NS_IsMainThread());
   if (mShuttingDown)
     return nullptr;
   if (!mDB) {
     mDB = Database::GetDatabase();
     NS_ENSURE_TRUE(mDB, nullptr);
+    // This must happen on the main-thread, so when we try to use the connection
+    // later it's initialized.
+    mDB->EnsureConnection();
+    NS_ENSURE_TRUE(mDB, nullptr);
+  }
+  return mDB->MainConn();
+}
+
+const mozIStorageConnection*
+History::GetConstDBConn()
+{
+  MOZ_ASSERT(mDB || mShuttingDown);
+  if (mShuttingDown || !mDB) {
+    return nullptr;
   }
   return mDB->MainConn();
 }
 
 void
 History::Shutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -2406,16 +2421,17 @@ History::IsRecentlyVisitedURI(nsIURI* aU
 ////////////////////////////////////////////////////////////////////////////////
 //// IHistory
 
 NS_IMETHODIMP
 History::VisitURI(nsIURI* aURI,
                   nsIURI* aLastVisitedURI,
                   uint32_t aFlags)
 {
+  MOZ_ASSERT(NS_IsMainThread());
   NS_ENSURE_ARG(aURI);
 
   if (mShuttingDown) {
     return NS_OK;
   }
 
   if (XRE_IsContentProcess()) {
     URIParams uri;
@@ -2622,16 +2638,17 @@ History::UnregisterVisitedCallback(nsIUR
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 History::SetURITitle(nsIURI* aURI, const nsAString& aTitle)
 {
+  MOZ_ASSERT(NS_IsMainThread());
   NS_ENSURE_ARG(aURI);
 
   if (mShuttingDown) {
     return NS_OK;
   }
 
   if (XRE_IsContentProcess()) {
     URIParams uri;
--- a/toolkit/components/places/History.h
+++ b/toolkit/components/places/History.h
@@ -111,25 +111,27 @@ public:
    * manager only.
    */
   static History* GetSingleton();
 
   template<int N>
   already_AddRefed<mozIStorageStatement>
   GetStatement(const char (&aQuery)[N])
   {
-    mozIStorageConnection* dbConn = GetDBConn();
+    // May be invoked on both threads.
+    const mozIStorageConnection* dbConn = GetConstDBConn();
     NS_ENSURE_TRUE(dbConn, nullptr);
     return mDB->GetStatement(aQuery);
   }
 
   already_AddRefed<mozIStorageStatement>
   GetStatement(const nsACString& aQuery)
   {
-    mozIStorageConnection* dbConn = GetDBConn();
+    // May be invoked on both threads.
+    const mozIStorageConnection* dbConn = GetConstDBConn();
     NS_ENSURE_TRUE(dbConn, nullptr);
     return mDB->GetStatement(aQuery);
   }
 
   bool IsShuttingDown() const {
     return mShuttingDown;
   }
   Mutex& GetShutdownMutex() {
@@ -143,21 +145,29 @@ public:
   void AppendToRecentlyVisitedURIs(nsIURI* aURI);
 
 private:
   virtual ~History();
 
   void InitMemoryReporter();
 
   /**
-   * Obtains a read-write database connection.
+   * Obtains a read-write database connection, initializing the connection
+   * if needed. Must be invoked on the main thread.
    */
   mozIStorageConnection* GetDBConn();
 
   /**
+   * Obtains a read-write database connection, but won't try to initialize it.
+   * May be invoked on both threads, but first one must invoke GetDBConn() on
+   * the main-thread at least once.
+   */
+  const mozIStorageConnection* GetConstDBConn();
+
+  /**
    * The database handle.  This is initialized lazily by the first call to
    * GetDBConn(), so never use it directly, or, if you really need, always
    * invoke GetDBConn() before.
    */
   RefPtr<mozilla::places::Database> mDB;
 
   RefPtr<ConcurrentStatementsHolder> mConcurrentStatementsHolder;
 
--- a/toolkit/components/places/PlacesCategoriesStarter.js
+++ b/toolkit/components/places/PlacesCategoriesStarter.js
@@ -1,101 +1,70 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * vim: sw=2 ts=2 sts=2 expandtab
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-// Constants
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
+const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
 
 // Fired by TelemetryController when async telemetry data should be collected.
 const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
 
 // Seconds between maintenance runs.
 const MAINTENANCE_INTERVAL_SECONDS = 7 * 86400;
 
-// Imports
-
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
-Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
                                   "resource://gre/modules/PlacesDBUtils.jsm");
 
 /**
  * This component can be used as a starter for modules that have to run when
  * certain categories are invoked.
  */
 function PlacesCategoriesStarter() {
   Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY);
   Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN);
-
-  // nsINavBookmarkObserver implementation.
-  let notify = () => {
-    if (!this._notifiedBookmarksSvcReady) {
-      // TODO (bug 1145424): for whatever reason, even if we remove this
-      // component from the category (and thus from the category cache we use
-      // to notify), we keep being notified.
-      this._notifiedBookmarksSvcReady = true;
-      // For perf reasons unregister from the category, since no further
-      // notifications are needed.
-      Cc["@mozilla.org/categorymanager;1"]
-        .getService(Ci.nsICategoryManager)
-        .deleteCategoryEntry("bookmark-observers", "PlacesCategoriesStarter", false);
-      // Directly notify PlacesUtils, to ensure it catches the notification.
-      PlacesUtils.observe(null, "bookmarks-service-ready", null);
-    }
-  };
-
-  [ "onItemAdded", "onItemRemoved", "onItemChanged", "onBeginUpdateBatch",
-    "onEndUpdateBatch", "onItemVisited", "onItemMoved"
-  ].forEach(aMethod => this[aMethod] = notify);
 }
 
 PlacesCategoriesStarter.prototype = {
-  // nsIObserver
-
   observe: function PCS_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case PlacesUtils.TOPIC_SHUTDOWN:
         Services.obs.removeObserver(this, PlacesUtils.TOPIC_SHUTDOWN);
         Services.obs.removeObserver(this, TOPIC_GATHER_TELEMETRY);
         if (Cu.isModuleLoaded("resource://gre/modules/PlacesDBUtils.jsm")) {
           PlacesDBUtils.shutdown();
         }
         break;
       case TOPIC_GATHER_TELEMETRY:
         PlacesDBUtils.telemetry();
         break;
+      case PlacesUtils.TOPIC_INIT_COMPLETE:
+        // Init keywords so it starts listening to bookmarks notifications.
+        PlacesUtils.keywords;
+        break;
       case "idle-daily":
         // Once a week run places.sqlite maintenance tasks.
         let lastMaintenance =
           Services.prefs.getIntPref("places.database.lastMaintenance", 0);
         let nowSeconds = parseInt(Date.now() / 1000);
         if (lastMaintenance < nowSeconds - MAINTENANCE_INTERVAL_SECONDS) {
           PlacesDBUtils.maintenanceOnIdle();
         }
         break;
       default:
         throw new Error("Trying to handle an unknown category.");
     }
   },
 
-  // nsISupports
-
   classID: Components.ID("803938d5-e26d-4453-bf46-ad4b26e41114"),
-
   _xpcom_factory: XPCOMUtils.generateSingletonFactory(PlacesCategoriesStarter),
-
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
-    Ci.nsINavBookmarkObserver
   ])
 };
 
-// Module Registration
-
 var components = [PlacesCategoriesStarter];
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -611,32 +611,16 @@ this.PlacesUtils = {
   // nsIObserver
   observe: function PU_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case this.TOPIC_SHUTDOWN:
         Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
         while (this._shutdownFunctions.length > 0) {
           this._shutdownFunctions.shift().apply(this);
         }
-        if (this._bookmarksServiceObserversQueue.length > 0) {
-          // Since we are shutting down, there's no reason to add the observers.
-          this._bookmarksServiceObserversQueue.length = 0;
-        }
-        break;
-      case "bookmarks-service-ready":
-        this._bookmarksServiceReady = true;
-        while (this._bookmarksServiceObserversQueue.length > 0) {
-          let observerInfo = this._bookmarksServiceObserversQueue.shift();
-          this.bookmarks.addObserver(observerInfo.observer, observerInfo.weak);
-        }
-
-        // Initialize the keywords cache to start observing bookmarks
-        // notifications.  This is needed as far as we support both the old and
-        // the new bookmarking APIs at the same time.
-        gKeywordsCachePromise.catch(Cu.reportError);
         break;
     }
   },
 
   onPageAnnotationSet() {},
   onPageAnnotationRemoved() {},
 
 
@@ -1553,73 +1537,56 @@ this.PlacesUtils = {
    * }));
    *
    * @param {string} name The name of the operation. Used for debugging, logging
    *   and crash reporting.
    * @param {function(db)} task A function that takes as argument a Sqlite.jsm
    *   connection and returns a Promise. Shutdown is guaranteed to not interrupt
    *   execution of `task`.
    */
-  withConnectionWrapper: (name, task) => {
+  async withConnectionWrapper(name, task) {
     if (!name) {
       throw new TypeError("Expecting a user-readable name");
     }
-    return (async function() {
-      let db = await gAsyncDBWrapperPromised;
-      return db.executeBeforeShutdown(name, task);
-    })();
+    let db = await gAsyncDBWrapperPromised;
+    return db.executeBeforeShutdown(name, task);
   },
 
   /**
    * Lazily adds a bookmarks observer, waiting for the bookmarks service to be
    * alive before registering the observer.  This is especially useful in the
    * startup path, to avoid initializing the service just to add an observer.
    *
    * @param aObserver
    *        Object implementing nsINavBookmarkObserver
    * @param [optional]aWeakOwner
    *        Whether to use weak ownership.
    *
    * @note Correct functionality of lazy observers relies on the fact Places
    *       notifies categories before real observers, and uses
    *       PlacesCategoriesStarter component to kick-off the registration.
    */
-  _bookmarksServiceReady: false,
-  _bookmarksServiceObserversQueue: [],
-  addLazyBookmarkObserver:
-  function PU_addLazyBookmarkObserver(aObserver, aWeakOwner) {
-    if (this._bookmarksServiceReady) {
-      this.bookmarks.addObserver(aObserver, aWeakOwner === true);
-      return;
-    }
-    this._bookmarksServiceObserversQueue.push({ observer: aObserver,
-                                                weak: aWeakOwner === true });
+  addLazyBookmarkObserver(aObserver, aWeakOwner) {
+    Deprecated.warning(`PlacesUtils.addLazyBookmarkObserver() is deprecated.
+                        Please use PlacesUtils.bookmarks.addObserver()`,
+                       "https://bugzilla.mozilla.org/show_bug.cgi?id=1371677");
+    this.bookmarks.addObserver(aObserver, aWeakOwner === true);
   },
 
   /**
    * Removes a bookmarks observer added through addLazyBookmarkObserver.
    *
    * @param aObserver
    *        Object implementing nsINavBookmarkObserver
    */
-  removeLazyBookmarkObserver:
-  function PU_removeLazyBookmarkObserver(aObserver) {
-    if (this._bookmarksServiceReady) {
-      this.bookmarks.removeObserver(aObserver);
-      return;
-    }
-    let index = -1;
-    for (let i = 0;
-         i < this._bookmarksServiceObserversQueue.length && index == -1; i++) {
-      if (this._bookmarksServiceObserversQueue[i].observer === aObserver)
-        index = i;
-    }
-    if (index != -1) {
-      this._bookmarksServiceObserversQueue.splice(index, 1);
-    }
+  removeLazyBookmarkObserver(aObserver) {
+    Deprecated.warning(`PlacesUtils.removeLazyBookmarkObserver() is deprecated.
+                        Please use PlacesUtils.bookmarks.removeObserver()`,
+                       "https://bugzilla.mozilla.org/show_bug.cgi?id=1371677");
+    this.bookmarks.removeObserver(aObserver);
   },
 
   /**
    * Sets the character-set for a URI.
    *
    * @param {nsIURI} aURI
    * @param {String} aCharset character-set value.
    * @return {Promise}
@@ -2105,17 +2072,20 @@ XPCOMUtils.defineLazyServiceGetter(Place
 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "tagging",
                                    "@mozilla.org/browser/tagging-service;1",
                                    "nsITaggingService");
 
 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "livemarks",
                                    "@mozilla.org/browser/livemark-service;2",
                                    "mozIAsyncLivemarks");
 
-XPCOMUtils.defineLazyGetter(PlacesUtils, "keywords", () => Keywords);
+XPCOMUtils.defineLazyGetter(PlacesUtils, "keywords", () => {
+  gKeywordsCachePromise.catch(Cu.reportError);
+  return Keywords;
+});
 
 XPCOMUtils.defineLazyGetter(PlacesUtils, "transactionManager", function() {
   let tm = Cc["@mozilla.org/transactionmanager;1"].
            createInstance(Ci.nsITransactionManager);
   tm.AddListener(PlacesUtils);
   this.registerShutdownFunction(function() {
     // Clear all references to local transactions in the transaction manager,
     // this prevents from leaking it.
@@ -2429,51 +2399,16 @@ var Keywords = {
 // Set by the keywords API to distinguish notifications fired by the old API.
 // Once the old API will be gone, we can remove this and stop observing.
 var gIgnoreKeywordNotifications = false;
 
 XPCOMUtils.defineLazyGetter(this, "gKeywordsCachePromise", () =>
   PlacesUtils.withConnectionWrapper("PlacesUtils: gKeywordsCachePromise",
     async function(db) {
       let cache = new Map();
-      let rows = await db.execute(
-        `SELECT keyword, url, post_data
-         FROM moz_keywords k
-         JOIN moz_places h ON h.id = k.place_id
-        `);
-      let brokenKeywords = [];
-      for (let row of rows) {
-        let keyword = row.getResultByName("keyword");
-        try {
-          let entry = { keyword,
-                        url: new URL(row.getResultByName("url")),
-                        postData: row.getResultByName("post_data") };
-          cache.set(keyword, entry);
-        } catch (ex) {
-          // The url is invalid, don't load the keyword and remove it, or it
-          // would break the whole keywords API.
-          brokenKeywords.push(keyword);
-        }
-      }
-      if (brokenKeywords.length) {
-        await db.execute(
-          `DELETE FROM moz_keywords
-           WHERE keyword IN (${brokenKeywords.map(JSON.stringify).join(",")})
-          `);
-      }
-
-      // Helper to get a keyword from an href.
-      function keywordsForHref(href) {
-        let keywords = [];
-        for (let [ key, val ] of cache) {
-          if (val.url.href == href)
-            keywords.push(key);
-        }
-        return keywords;
-      }
 
       // Start observing changes to bookmarks. For now we are going to keep that
       // relation for backwards compatibility reasons, but mostly because we are
       // lacking a UI to manage keywords directly.
       let observer = {
         QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
         onBeginUpdateBatch() {},
         onEndUpdateBatch() {},
@@ -2503,39 +2438,32 @@ XPCOMUtils.defineLazyGetter(this, "gKeyw
 
         onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid,
                       parentGuid, oldVal) {
           if (gIgnoreKeywordNotifications) {
             return;
           }
 
           if (prop == "keyword") {
-            this._onKeywordChanged(guid, val).catch(Cu.reportError);
+            this._onKeywordChanged(guid, val, oldVal).catch(Cu.reportError);
           } else if (prop == "uri") {
             this._onUrlChanged(guid, val, oldVal).catch(Cu.reportError);
           }
         },
 
-        async _onKeywordChanged(guid, keyword) {
-          let bookmark = await PlacesUtils.bookmarks.fetch(guid);
-          // Due to mixed sync/async operations, by this time the bookmark could
-          // have disappeared and we already handle removals in onItemRemoved.
-          if (!bookmark) {
-            return;
-          }
-
+        async _onKeywordChanged(guid, keyword, href) {
           if (keyword.length == 0) {
             // We are removing a keyword.
-            let keywords = keywordsForHref(bookmark.url.href)
+            let keywords = keywordsForHref(href)
             for (let kw of keywords) {
               cache.delete(kw);
             }
           } else {
             // We are adding a new keyword.
-            cache.set(keyword, { keyword, url: bookmark.url });
+            cache.set(keyword, { keyword, url: new URL(href) });
           }
         },
 
         async _onUrlChanged(guid, url, oldUrl) {
           // Check if the old url is associated with keywords.
           let entries = [];
           await PlacesUtils.keywords.fetch({ url: oldUrl }, e => entries.push(e));
           if (entries.length == 0) {
@@ -2550,16 +2478,54 @@ XPCOMUtils.defineLazyGetter(this, "gKeyw
           }
         },
       };
 
       PlacesUtils.bookmarks.addObserver(observer);
       PlacesUtils.registerShutdownFunction(() => {
         PlacesUtils.bookmarks.removeObserver(observer);
       });
+
+      let rows = await db.execute(
+        `SELECT keyword, url, post_data
+         FROM moz_keywords k
+         JOIN moz_places h ON h.id = k.place_id
+        `);
+      let brokenKeywords = [];
+      for (let row of rows) {
+        let keyword = row.getResultByName("keyword");
+        try {
+          let entry = { keyword,
+                        url: new URL(row.getResultByName("url")),
+                        postData: row.getResultByName("post_data") };
+          cache.set(keyword, entry);
+        } catch (ex) {
+          // The url is invalid, don't load the keyword and remove it, or it
+          // would break the whole keywords API.
+          brokenKeywords.push(keyword);
+        }
+      }
+
+      if (brokenKeywords.length) {
+        await db.execute(
+          `DELETE FROM moz_keywords
+           WHERE keyword IN (${brokenKeywords.map(JSON.stringify).join(",")})
+          `);
+      }
+
+      // Helper to get a keyword from an href.
+      function keywordsForHref(href) {
+        let keywords = [];
+        for (let [ key, val ] of cache) {
+          if (val.url.href == href)
+            keywords.push(key);
+        }
+        return keywords;
+      }
+
       return cache;
     }
 ));
 
 // Sometime soon, likely as part of the transition to mozIAsyncBookmarks,
 // itemIds will be deprecated in favour of GUIDs, which play much better
 // with multiple undo/redo operations.  Because these GUIDs are already stored,
 // and because we don't want to revise the transactions API once more when this
--- a/toolkit/components/places/Shutdown.cpp
+++ b/toolkit/components/places/Shutdown.cpp
@@ -186,17 +186,17 @@ ConnectionShutdownBlocker::BlockShutdown
 
   // At this stage, any use of this database is forbidden. Get rid of
   // `gDatabase`. Note, however, that the database could be
   // resurrected.  This can happen in particular during tests.
   MOZ_ASSERT(Database::gDatabase == nullptr || Database::gDatabase == mDatabase);
   Database::gDatabase = nullptr;
 
   // Database::Shutdown will invoke Complete once the connection is closed.
-  mDatabase->Shutdown(true);
+  mDatabase->Shutdown();
   mState = CALLED_STORAGESHUTDOWN;
   return NS_OK;
 }
 
 // mozIStorageCompletionCallback
 NS_IMETHODIMP
 ConnectionShutdownBlocker::Complete(nsresult, nsISupports*)
 {
--- a/toolkit/components/places/nsAnnotationService.cpp
+++ b/toolkit/components/places/nsAnnotationService.cpp
@@ -1994,17 +1994,21 @@ nsAnnotationService::Observe(nsISupports
                                          EXPIRE_SESSION);
       NS_ENSURE_SUCCESS(rv, rv);
 
       mozIStorageBaseStatement *stmts[] = {
         pageAnnoStmt.get()
       , itemAnnoStmt.get()
       };
 
+      nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+      if (!conn) {
+        return NS_ERROR_UNEXPECTED;
+      }
       nsCOMPtr<mozIStoragePendingStatement> ps;
-      rv = mDB->MainConn()->ExecuteAsync(stmts, ArrayLength(stmts), nullptr,
+      rv = conn->ExecuteAsync(stmts, ArrayLength(stmts), nullptr,
                                          getter_AddRefs(ps));
       NS_ENSURE_SUCCESS(rv, rv);
     }
   }
 
   return NS_OK;
 }
--- a/toolkit/components/places/nsFaviconService.cpp
+++ b/toolkit/components/places/nsFaviconService.cpp
@@ -197,21 +197,25 @@ nsFaviconService::ExpireAllFavicons()
   );
   NS_ENSURE_STATE(unlinkIconsStmt);
 
   mozIStorageBaseStatement* stmts[] = {
     removePagesStmt.get()
   , removeIconsStmt.get()
   , unlinkIconsStmt.get()
   };
+  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+  if (!conn) {
+    return NS_ERROR_UNEXPECTED;
+  }
   nsCOMPtr<mozIStoragePendingStatement> ps;
   RefPtr<ExpireFaviconsStatementCallbackNotifier> callback =
     new ExpireFaviconsStatementCallbackNotifier();
-  return mDB->MainConn()->ExecuteAsync(stmts, ArrayLength(stmts),
-                                       callback, getter_AddRefs(ps));
+  return conn->ExecuteAsync(stmts, ArrayLength(stmts),
+                                        callback, getter_AddRefs(ps));
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsITimerCallback
 
 NS_IMETHODIMP
 nsFaviconService::Notify(nsITimer* timer)
 {
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -1260,31 +1260,36 @@ interface nsINavHistoryService : nsISupp
   const unsigned long TRANSITION_RELOAD = 9;
 
   /**
    * Set when database is coherent
    */
   const unsigned short DATABASE_STATUS_OK = 0;
 
   /**
-   * Set when database did not exist and we created a new one
+   * Set when database did not exist and we created a new one.
    */
   const unsigned short DATABASE_STATUS_CREATE = 1;
 
   /**
-   * Set when database was corrupt and we replaced it
+   * Set when database was corrupt and we replaced it with a new one.
    */
   const unsigned short DATABASE_STATUS_CORRUPT = 2;
 
   /**
-   * Set when database schema has been upgraded
+   * Set when database schema has been upgraded.
    */
   const unsigned short DATABASE_STATUS_UPGRADED = 3;
 
   /**
+   * Set when database couldn't be opened.
+   */
+  const unsigned short DATABASE_STATUS_LOCKED = 4;
+
+  /**
    * Returns the current database status
    */
   readonly attribute unsigned short databaseStatus;
 
   /**
    * True if there is any history. This can be used in UI to determine whether
    * the "clear history" button should be enabled or not. This is much better
    * than using BrowserHistory.count since that can be very slow if there is
--- a/toolkit/components/places/nsLivemarkService.js
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -105,17 +105,17 @@ function toDate(time) {
 
 // LivemarkService
 
 function LivemarkService() {
   // Cleanup on shutdown.
   Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);
 
   // Observe bookmarks but don't init the service just for that.
-  PlacesUtils.addLazyBookmarkObserver(this, true);
+  PlacesUtils.bookmarks.addObserver(this, true);
 }
 
 LivemarkService.prototype = {
   // This is just an helper for code readability.
   _promiseLivemarksMap: () => gLivemarksCachePromised,
 
   _reloading: false,
   _startReloadTimer(livemarksMap, forceUpdate, reloaded) {
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -209,19 +209,16 @@ nsNavBookmarks::Init()
   NS_ENSURE_STATE(mDB);
 
   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
   if (os) {
     (void)os->AddObserver(this, TOPIC_PLACES_SHUTDOWN, true);
     (void)os->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true);
   }
 
-  nsresult rv = ReadRoots();
-  NS_ENSURE_SUCCESS(rv, rv);
-
   mCanNotify = true;
 
   // Observe annotations.
   nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
   NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY);
   annosvc->AddObserver(this);
 
   // Allows us to notify on title changes. MUST BE LAST so it is impossible
@@ -232,20 +229,28 @@ nsNavBookmarks::Init()
   history->AddObserver(this, true);
 
   // DO NOT PUT STUFF HERE that can fail. See observer comment above.
 
   return NS_OK;
 }
 
 nsresult
-nsNavBookmarks::ReadRoots()
+nsNavBookmarks::EnsureRoots()
 {
+  if (mRoot)
+    return NS_OK;
+
+  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+  if (!conn) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
   nsCOMPtr<mozIStorageStatement> stmt;
-  nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING(
+  nsresult rv = conn->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT guid, id FROM moz_bookmarks WHERE guid IN ( "
       "'root________', 'menu________', 'toolbar_____', "
       "'tags________', 'unfiled_____', 'mobile______' )"
   ), getter_AddRefs(stmt));
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool hasResult;
   while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
@@ -370,56 +375,68 @@ nsNavBookmarks::AdjustSeparatorsSyncCoun
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetPlacesRoot(int64_t* aRoot)
 {
+  nsresult rv = EnsureRoots();
+  NS_ENSURE_SUCCESS(rv, rv);
   *aRoot = mRoot;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetBookmarksMenuFolder(int64_t* aRoot)
 {
+  nsresult rv = EnsureRoots();
+  NS_ENSURE_SUCCESS(rv, rv);
   *aRoot = mMenuRoot;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetToolbarFolder(int64_t* aFolderId)
 {
+  nsresult rv = EnsureRoots();
+  NS_ENSURE_SUCCESS(rv, rv);
   *aFolderId = mToolbarRoot;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetTagsFolder(int64_t* aRoot)
 {
+  nsresult rv = EnsureRoots();
+  NS_ENSURE_SUCCESS(rv, rv);
   *aRoot = mTagsRoot;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetUnfiledBookmarksFolder(int64_t* aRoot)
 {
+  nsresult rv = EnsureRoots();
+  NS_ENSURE_SUCCESS(rv, rv);
   *aRoot = mUnfiledRoot;
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetMobileFolder(int64_t* aRoot)
 {
+  nsresult rv = EnsureRoots();
+  NS_ENSURE_SUCCESS(rv, rv);
   *aRoot = mMobileRoot;
   return NS_OK;
 }
 
 
 nsresult
 nsNavBookmarks::InsertBookmarkInDB(int64_t aPlaceId,
                                    enum ItemType aItemType,
@@ -533,17 +550,20 @@ nsNavBookmarks::InsertBookmarkInDB(int64
     // Update last modified date of the ancestors.
     // TODO (bug 408991): Doing this for all ancestors would be slow without a
     //                    nested tree, so for now update only the parent.
     rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, aParentId,
                              aDateAdded);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  bool isTagging = aGrandParentId == mTagsRoot;
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  bool isTagging = aGrandParentId == tagsRootId;
   if (isTagging) {
     // If we're tagging a bookmark, increment the change counter for all
     // bookmarks with the URI.
     rv = AddSyncChangesForBookmarksWithURI(aURI, syncChangeDelta);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // Mark all affected separators as changed
@@ -626,34 +646,37 @@ nsNavBookmarks::InsertBookmark(int64_t a
   TruncateTitle(aTitle, title);
 
   rv = InsertBookmarkInDB(placeId, BOOKMARK, aFolder, index, title, dateAdded,
                           0, folderGuid, grandParentId, aURI, aSource,
                           aNewBookmarkId, guid);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // If not a tag, recalculate frecency for this entry, since it changed.
-  if (grandParentId != mTagsRoot) {
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (grandParentId != tagsRootId) {
     rv = history->UpdateFrecency(placeId);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                              SKIP_TAGS(grandParentId == mTagsRoot),
                              OnItemAdded(*aNewBookmarkId, aFolder, index,
                                          TYPE_BOOKMARK, aURI, title, dateAdded,
                                          guid, folderGuid, aSource));
 
   // If the bookmark has been added to a tag container, notify all
   // bookmark-folder result nodes which contain a bookmark for the new
   // bookmark's url.
-  if (grandParentId == mTagsRoot) {
+  if (grandParentId == tagsRootId) {
     // Notify a tags change to all bookmarks for this URI.
     nsTArray<BookmarkData> bookmarks;
     rv = GetBookmarksForURI(aURI, bookmarks);
     NS_ENSURE_SUCCESS(rv, rv);
 
     for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
       // Check that bookmarks doesn't include the current tag itemId.
       MOZ_ASSERT(bookmarks[i].id != *aNewBookmarkId);
@@ -687,18 +710,21 @@ nsNavBookmarks::RemoveItem(int64_t aItem
 
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aItemId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mozStorageTransaction transaction(mDB->MainConn(), false);
 
   // First, if not a tag, remove item annotations.
-  bool isUntagging = bookmark.grandParentId == mTagsRoot;
-  if (bookmark.parentId != mTagsRoot && !isUntagging) {
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  bool isUntagging = bookmark.grandParentId == tagsRootId;
+  if (bookmark.parentId != tagsRootId && !isUntagging) {
     nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
     NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY);
     rv = annosvc->RemoveItemAnnotations(bookmark.id, aSource);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   if (bookmark.type == TYPE_FOLDER) {
     // Remove all of the folder's children.
@@ -749,41 +775,41 @@ nsNavBookmarks::RemoveItem(int64_t aItem
   }
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIURI> uri;
   if (bookmark.type == TYPE_BOOKMARK) {
     // If not a tag, recalculate frecency for this entry, since it changed.
-    if (bookmark.grandParentId != mTagsRoot) {
+    if (bookmark.grandParentId != tagsRootId) {
       nsNavHistory* history = nsNavHistory::GetHistoryService();
       NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
       rv = history->UpdateFrecency(bookmark.placeId);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     // A broken url should not interrupt the removal process.
     (void)NS_NewURI(getter_AddRefs(uri), bookmark.url);
     // We cannot assert since some automated tests are checking this path.
     NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveItem");
   }
 
   NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                             SKIP_TAGS(bookmark.parentId == mTagsRoot ||
-                                       bookmark.grandParentId == mTagsRoot),
+                             SKIP_TAGS(bookmark.parentId == tagsRootId ||
+                                       bookmark.grandParentId == tagsRootId),
                              OnItemRemoved(bookmark.id,
                                            bookmark.parentId,
                                            bookmark.position,
                                            bookmark.type,
                                            uri,
                                            bookmark.guid,
                                            bookmark.parentGuid,
                                            aSource));
 
-  if (bookmark.type == TYPE_BOOKMARK && bookmark.grandParentId == mTagsRoot &&
+  if (bookmark.type == TYPE_BOOKMARK && bookmark.grandParentId == tagsRootId &&
       uri) {
     // If the removed bookmark was child of a tag container, notify a tags
     // change to all bookmarks for this URI.
     nsTArray<BookmarkData> bookmarks;
     rv = GetBookmarksForURI(uri, bookmarks);
     NS_ENSURE_SUCCESS(rv, rv);
 
     for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
@@ -882,18 +908,22 @@ nsNavBookmarks::CreateContainerWithID(in
   rv = InsertBookmarkInDB(-1, FOLDER, aParent, index,
                           title, dateAdded, 0, folderGuid, grandParentId,
                           nullptr, aSource, aNewFolder, guid);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                             SKIP_TAGS(aParent == mTagsRoot),
+                             SKIP_TAGS(aParent == tagsRootId),
                              OnItemAdded(*aNewFolder, aParent, index, FOLDER,
                                          nullptr, title, dateAdded, guid,
                                          folderGuid, aSource));
 
   *aIndex = index;
   return NS_OK;
 }
 
@@ -1168,20 +1198,23 @@ nsNavBookmarks::GetDescendantChildren(in
 
 
 NS_IMETHODIMP
 nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId, uint16_t aSource)
 {
   AUTO_PROFILER_LABEL("nsNavBookmarks::RemoveFolderChilder", OTHER);
 
   NS_ENSURE_ARG_MIN(aFolderId, 1);
-  NS_ENSURE_ARG(aFolderId != mRoot);
+  int64_t rootId;
+  nsresult rv = GetPlacesRoot(&rootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  NS_ENSURE_ARG(aFolderId != rootId);
 
   BookmarkData folder;
-  nsresult rv = FetchItemInfo(aFolderId, folder);
+  rv = FetchItemInfo(aFolderId, folder);
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_ARG(folder.type == TYPE_FOLDER);
 
   // Fill folder children array recursively.
   nsTArray<BookmarkData> folderChildrenArray;
   rv = GetDescendantChildren(folder.id, folder.guid, folder.parentId,
                              folderChildrenArray);
   NS_ENSURE_SUCCESS(rv, rv);
@@ -1211,42 +1244,50 @@ nsNavBookmarks::RemoveFolderChildren(int
   mozStorageStatementScoper deleteStatementScoper(deleteStatement);
 
   rv = deleteStatement->BindInt64ByName(NS_LITERAL_CSTRING("parent"), folder.id);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = deleteStatement->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Clean up orphan items annotations.
-  rv = mDB->MainConn()->ExecuteSimpleSQL(
+  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+  if (!conn) {
+    return NS_ERROR_UNEXPECTED;
+  }
+  rv = conn->ExecuteSimpleSQL(
     NS_LITERAL_CSTRING(
       "DELETE FROM moz_items_annos "
       "WHERE id IN ("
         "SELECT a.id from moz_items_annos a "
         "LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
         "WHERE b.id ISNULL)"));
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Set the lastModified date.
   rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, folder.id,
                            RoundedPRNow());
   NS_ENSURE_SUCCESS(rv, rv);
 
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   if (syncChangeDelta) {
     nsTArray<TombstoneData> tombstones(folderChildrenArray.Length());
     PRTime dateRemoved = RoundedPRNow();
 
     for (uint32_t i = 0; i < folderChildrenArray.Length(); ++i) {
       BookmarkData& child = folderChildrenArray[i];
       if (NeedsTombstone(child)) {
         // Write tombstones for synced children.
         TombstoneData childTombstone = {child.guid, dateRemoved};
         tombstones.AppendElement(childTombstone);
       }
-      bool isUntagging = child.grandParentId == mTagsRoot;
+      bool isUntagging = child.grandParentId == tagsRootId;
       if (isUntagging) {
         // Bump the change counter for all tagged bookmarks when removing a tag
         // folder.
         rv = AddSyncChangesForBookmarksWithURL(child.url, syncChangeDelta);
         NS_ENSURE_SUCCESS(rv, rv);
       }
     }
 
@@ -1259,40 +1300,40 @@ nsNavBookmarks::RemoveFolderChildren(int
 
   // Call observers in reverse order to serve children before their parent.
   for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) {
     BookmarkData& child = folderChildrenArray[i];
 
     nsCOMPtr<nsIURI> uri;
     if (child.type == TYPE_BOOKMARK) {
       // If not a tag, recalculate frecency for this entry, since it changed.
-      if (child.grandParentId != mTagsRoot) {
+      if (child.grandParentId != tagsRootId) {
         nsNavHistory* history = nsNavHistory::GetHistoryService();
         NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
         rv = history->UpdateFrecency(child.placeId);
         NS_ENSURE_SUCCESS(rv, rv);
       }
       // A broken url should not interrupt the removal process.
       (void)NS_NewURI(getter_AddRefs(uri), child.url);
       // We cannot assert since some automated tests are checking this path.
       NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveFolderChildren");
     }
 
     NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                               ((child.grandParentId == mTagsRoot) ? SkipTags : SkipDescendants),
+                               ((child.grandParentId == tagsRootId) ? SkipTags : SkipDescendants),
                                OnItemRemoved(child.id,
                                              child.parentId,
                                              child.position,
                                              child.type,
                                              uri,
                                              child.guid,
                                              child.parentGuid,
                                              aSource));
 
-    if (child.type == TYPE_BOOKMARK && child.grandParentId == mTagsRoot &&
+    if (child.type == TYPE_BOOKMARK && child.grandParentId == tagsRootId &&
         uri) {
       // If the removed bookmark was a child of a tag container, notify all
       // bookmark-folder result nodes which contain a bookmark for the removed
       // bookmark's url.
       nsTArray<BookmarkData> bookmarks;
       rv = GetBookmarksForURI(uri, bookmarks);
       NS_ENSURE_SUCCESS(rv, rv);
 
@@ -1462,17 +1503,20 @@ nsNavBookmarks::MoveItem(int64_t aItemId
 
     // Mark all affected separators as changed
     rv = AdjustSeparatorsSyncCounter(bookmark.parentId, bookmark.position, syncChangeDelta);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = AdjustSeparatorsSyncCounter(aNewParent, newIndex, syncChangeDelta);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  bool isChangingTagFolder = bookmark.parentId == mTagsRoot;
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  bool isChangingTagFolder = bookmark.parentId == tagsRootId;
   if (isChangingTagFolder) {
     // Moving a tag folder out of the tags root untags all its bookmarks. This
     // is an odd case, but the tagging service adds an observer to handle it,
     // so we bump the change counter for each untagged item for consistency.
     rv = AddSyncChangesForBookmarksInFolder(bookmark.id, syncChangeDelta);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
@@ -1617,18 +1661,20 @@ NS_IMETHODIMP
 nsNavBookmarks::SetItemDateAdded(int64_t aItemId, PRTime aDateAdded,
                                  uint16_t aSource)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aItemId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
-
-  bool isTagging = bookmark.grandParentId == mTagsRoot;
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  bool isTagging = bookmark.grandParentId == tagsRootId;
   int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
 
   // Round here so that we notify with the right value.
   bookmark.dateAdded = RoundToMilliseconds(aDateAdded);
 
   if (isTagging) {
     // If we're changing a tag, bump the change counter for all tagged
     // bookmarks. We use a separate code path to avoid a transaction for
@@ -1688,17 +1734,20 @@ nsNavBookmarks::SetItemLastModified(int6
                                     uint16_t aSource)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aItemId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  bool isTagging = bookmark.grandParentId == mTagsRoot;
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  bool isTagging = bookmark.grandParentId == tagsRootId;
   int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
 
   // Round here so that we notify with the right value.
   bookmark.lastModified = RoundToMilliseconds(aLastModified);
 
   if (isTagging) {
     // If we're changing a tag, bump the change counter for all tagged
     // bookmarks. We use a separate code path to avoid a transaction for
@@ -1957,17 +2006,20 @@ nsNavBookmarks::SetItemTitle(int64_t aIt
                              uint16_t aSource)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aItemId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  bool isChangingTagFolder = bookmark.parentId == mTagsRoot;
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  bool isChangingTagFolder = bookmark.parentId == tagsRootId;
   int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
 
   nsAutoCString title;
   TruncateTitle(aTitle, title);
 
   if (isChangingTagFolder) {
     // If we're changing the title of a tag folder, bump the change counter
     // for all tagged bookmarks. We use a separate code path to avoid a
@@ -2464,17 +2516,20 @@ nsNavBookmarks::ChangeBookmarkURI(int64_
 
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aBookmarkId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_ARG(bookmark.type == TYPE_BOOKMARK);
 
   mozStorageTransaction transaction(mDB->MainConn(), false);
 
-  bool isTagging = bookmark.grandParentId == mTagsRoot;
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  bool isTagging = bookmark.grandParentId == tagsRootId;
   int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
 
   nsNavHistory* history = nsNavHistory::GetHistoryService();
   NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
   int64_t newPlaceId;
   nsAutoCString newPlaceGuid;
   rv = history->GetOrCreateIdForPage(aNewURI, &newPlaceId, newPlaceGuid);
   NS_ENSURE_SUCCESS(rv, rv);
@@ -2578,19 +2633,22 @@ nsNavBookmarks::GetBookmarkIdsForURITArr
     "JOIN moz_bookmarks t on t.id = b.parent "
     "WHERE b.fk = (SELECT id FROM moz_places WHERE url_hash = hash(:page_url) AND url = :page_url) AND "
     "t.parent IS NOT :tags_root "
     "ORDER BY b.lastModified DESC, b.id DESC "
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
-  nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+  int64_t tagsRootId;
+  nsresult rv = GetTagsFolder(&tagsRootId);
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("tags_root"), mTagsRoot);
+  rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("tags_root"), tagsRootId);
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool more;
   while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&more))) && more) {
     int64_t bookmarkId;
     rv = stmt->GetInt64(0, &bookmarkId);
     NS_ENSURE_SUCCESS(rv, rv);
     NS_ENSURE_TRUE(aResult.AppendElement(bookmarkId), NS_ERROR_OUT_OF_MEMORY);
@@ -2618,24 +2676,28 @@ nsNavBookmarks::GetBookmarksForURI(nsIUR
     "ORDER BY b.lastModified DESC, b.id DESC "
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
   nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  int64_t tagsRootId;
+  rv = GetTagsFolder(&tagsRootId);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   bool more;
   nsAutoString tags;
   while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&more))) && more) {
     // Skip tags.
     int64_t grandParentId;
     nsresult rv = stmt->GetInt64(5, &grandParentId);
     NS_ENSURE_SUCCESS(rv, rv);
-    if (grandParentId == mTagsRoot) {
+    if (grandParentId == tagsRootId) {
       continue;
     }
 
     BookmarkData bookmark;
     bookmark.grandParentId = grandParentId;
     rv = stmt->GetInt64(0, &bookmark.id);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = stmt->GetUTF8String(1, bookmark.guid);
@@ -2901,45 +2963,46 @@ nsNavBookmarks::SetKeywordForBookmark(in
                                      NS_LITERAL_CSTRING("keyword"),
                                      false,
                                      EmptyCString(),
                                      bookmarks[i].lastModified,
                                      TYPE_BOOKMARK,
                                      bookmarks[i].parentId,
                                      bookmarks[i].guid,
                                      bookmarks[i].parentGuid,
-                                     EmptyCString(),
+                                     // Abusing oldVal to pass out the url.
+                                     bookmark.url,
                                      aSource));
     }
 
     return NS_OK;
   }
 
   // A keyword can only be associated to a single URI.  Check if the requested
   // keyword was already associated, in such a case we will need to notify about
   // the change.
+  nsAutoCString oldSpec;
   nsCOMPtr<nsIURI> oldUri;
   {
     nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
       "SELECT url "
       "FROM moz_keywords "
       "JOIN moz_places h ON h.id = place_id "
       "WHERE keyword = :keyword"
     );
     NS_ENSURE_STATE(stmt);
     mozStorageStatementScoper scoper(stmt);
     rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
     NS_ENSURE_SUCCESS(rv, rv);
 
     bool hasMore;
     if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
-      nsAutoCString spec;
-      rv = stmt->GetUTF8String(0, spec);
+      rv = stmt->GetUTF8String(0, oldSpec);
       NS_ENSURE_SUCCESS(rv, rv);
-      rv = NS_NewURI(getter_AddRefs(oldUri), spec);
+      rv = NS_NewURI(getter_AddRefs(oldUri), oldSpec);
       NS_ENSURE_SUCCESS(rv, rv);
     }
   }
 
   // If another uri is using the new keyword, we must update the keyword entry.
   // Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete
   // trigger.
   mozStorageTransaction updateTxn(mDB->MainConn(), false);
@@ -2957,17 +3020,18 @@ nsNavBookmarks::SetKeywordForBookmark(in
                                      NS_LITERAL_CSTRING("keyword"),
                                      false,
                                      EmptyCString(),
                                      bookmarks[i].lastModified,
                                      TYPE_BOOKMARK,
                                      bookmarks[i].parentId,
                                      bookmarks[i].guid,
                                      bookmarks[i].parentGuid,
-                                     EmptyCString(),
+                                     // Abusing oldVal to pass out the url.
+                                     oldSpec,
                                      aSource));
     }
 
     stmt = mDB->GetStatement(
       "UPDATE moz_keywords SET place_id = :place_id WHERE keyword = :keyword"
     );
     NS_ENSURE_STATE(stmt);
   }
@@ -3027,17 +3091,18 @@ nsNavBookmarks::SetKeywordForBookmark(in
                                    NS_LITERAL_CSTRING("keyword"),
                                    false,
                                    NS_ConvertUTF16toUTF8(keyword),
                                    bookmarks[i].lastModified,
                                    TYPE_BOOKMARK,
                                    bookmarks[i].parentId,
                                    bookmarks[i].guid,
                                    bookmarks[i].parentGuid,
-                                   EmptyCString(),
+                                   // Abusing oldVal to pass out the url.
+                                   bookmark.url,
                                    aSource));
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -240,17 +240,17 @@ private:
    * @return true if aFolderId points to live bookmarks, false otherwise.
    */
   bool IsLivemark(int64_t aFolderId);
 
   /**
    * Locates the root items in the bookmarks folder hierarchy assigning folder
    * ids to the root properties that are exposed through the service interface.
    */
-  nsresult ReadRoots();
+  nsresult EnsureRoots();
 
   nsresult AdjustIndices(int64_t aFolder,
                          int32_t aStartIndex,
                          int32_t aEndIndex,
                          int32_t aDelta);
 
   nsresult AdjustSeparatorsSyncCounter(int64_t aFolderId,
                                        int32_t aStartIndex,
@@ -313,16 +313,18 @@ private:
    * This is an handle to the Places database.
    */
   RefPtr<mozilla::places::Database> mDB;
 
   int32_t mItemCount;
 
   nsMaybeWeakPtrArray<nsINavBookmarkObserver> mObservers;
 
+  // These are lazy loaded, so never access them directly, always use the
+  // XPIDL getters.
   int64_t mRoot;
   int64_t mMenuRoot;
   int64_t mTagsRoot;
   int64_t mUnfiledRoot;
   int64_t mToolbarRoot;
   int64_t mMobileRoot;
 
   inline bool IsRoot(int64_t aFolderId) {
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -2215,22 +2215,25 @@ nsNavHistory::GetQueryResults(nsNavHisto
   nsresult rv = ConstructQueryString(aQueries, aOptions, queryString,
                                      paramsPresent, addParams);
   NS_ENSURE_SUCCESS(rv,rv);
 
   // create statement
   nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(queryString);
 #ifdef DEBUG
   if (!statement) {
-    nsAutoCString lastErrorString;
-    (void)mDB->MainConn()->GetLastErrorString(lastErrorString);
-    int32_t lastError = 0;
-    (void)mDB->MainConn()->GetLastError(&lastError);
-    printf("Places failed to create a statement from this query:\n%s\nStorage error (%d): %s\n",
-           queryString.get(), lastError, lastErrorString.get());
+    nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+    if (conn) {
+      nsAutoCString lastErrorString;
+      (void)conn->GetLastErrorString(lastErrorString);
+      int32_t lastError = 0;
+      (void)conn->GetLastError(&lastError);
+      printf("Places failed to create a statement from this query:\n%s\nStorage error (%d): %s\n",
+            queryString.get(), lastError, lastErrorString.get());
+    }
   }
 #endif
   NS_ENSURE_STATE(statement);
   mozStorageStatementScoper scoper(statement);
 
   if (paramsPresent) {
     // bind parameters
     int32_t i;
@@ -2396,17 +2399,21 @@ nsNavHistory::RemovePagesInternal(const 
   if (aPlaceIdsQueryString.IsEmpty())
     return NS_OK;
 
   mozStorageTransaction transaction(mDB->MainConn(), false,
                                     mozIStorageConnection::TRANSACTION_DEFERRED,
                                     true);
 
   // Delete all visits for the specified place ids.
-  nsresult rv = mDB->MainConn()->ExecuteSimpleSQL(
+  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+  if (!conn) {
+    return NS_ERROR_UNEXPECTED;
+  }
+  nsresult rv = conn->ExecuteSimpleSQL(
     NS_LITERAL_CSTRING(
       "DELETE FROM moz_historyvisits WHERE place_id IN (") +
         aPlaceIdsQueryString +
         NS_LITERAL_CSTRING(")")
   );
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = CleanupPlacesOnVisitsDelete(aPlaceIdsQueryString);
@@ -2481,40 +2488,44 @@ nsNavHistory::CleanupPlacesOnVisitsDelet
                        OnDeleteVisits(uri, 0, guid, nsINavHistoryObserver::REASON_DELETED, 0));
     }
   }
 
   // if the entry is not bookmarked and is not a place: uri
   // then we can remove it from moz_places.
   // Note that we do NOT delete favicons. Any unreferenced favicons will be
   // deleted next time the browser is shut down.
-  nsresult rv = mDB->MainConn()->ExecuteSimpleSQL(
+  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+  if (!conn) {
+    return NS_ERROR_UNEXPECTED;
+  }
+  nsresult rv = conn->ExecuteSimpleSQL(
     NS_LITERAL_CSTRING(
       "DELETE FROM moz_places WHERE id IN ( "
         ) + filteredPlaceIds + NS_LITERAL_CSTRING(
       ") "
     )
   );
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Expire orphan icons.
-  rv = mDB->MainConn()->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+  rv = conn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "DELETE FROM moz_pages_w_icons "
     "WHERE page_url_hash NOT IN (SELECT url_hash FROM moz_places) "
   ));
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = mDB->MainConn()->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+  rv = conn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "DELETE FROM moz_icons "
     "WHERE root = 0 AND id NOT IN (SELECT icon_id FROM moz_icons_to_pages) "
   ));
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Hosts accumulated during the places delete are updated through a trigger
   // (see nsPlacesTriggers.h).
-  rv = mDB->MainConn()->ExecuteSimpleSQL(
+  rv = conn->ExecuteSimpleSQL(
     NS_LITERAL_CSTRING("DELETE FROM moz_updatehosts_temp")
   );
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Invalidate frecencies of touched places, since they need recalculation.
   rv = invalidateFrecencies(aPlaceIdsQueryString);
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -2933,22 +2944,25 @@ nsNavHistory::AsyncExecuteLegacyQueries(
   NS_ENSURE_SUCCESS(rv,rv);
 
   nsCOMPtr<mozIStorageAsyncStatement> statement =
     mDB->GetAsyncStatement(queryString);
   NS_ENSURE_STATE(statement);
 
 #ifdef DEBUG
   if (NS_FAILED(rv)) {
-    nsAutoCString lastErrorString;
-    (void)mDB->MainConn()->GetLastErrorString(lastErrorString);
-    int32_t lastError = 0;
-    (void)mDB->MainConn()->GetLastError(&lastError);
-    printf("Places failed to create a statement from this query:\n%s\nStorage error (%d): %s\n",
-           queryString.get(), lastError, lastErrorString.get());
+    nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+    if (conn) {
+      nsAutoCString lastErrorString;
+      (void)mDB->MainConn()->GetLastErrorString(lastErrorString);
+      int32_t lastError = 0;
+      (void)mDB->MainConn()->GetLastError(&lastError);
+      printf("Places failed to create a statement from this query:\n%s\nStorage error (%d): %s\n",
+            queryString.get(), lastError, lastErrorString.get());
+    }
   }
 #endif
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (paramsPresent) {
     // bind parameters
     int32_t i;
     for (i = 0; i < queries.Count(); i++) {
@@ -3129,24 +3143,28 @@ nsNavHistory::DecayFrecency()
   NS_ENSURE_STATE(decayAdaptive);
 
   // Delete any adaptive entries that won't help in ordering anymore.
   nsCOMPtr<mozIStorageAsyncStatement> deleteAdaptive = mDB->GetAsyncStatement(
     "DELETE FROM moz_inputhistory WHERE use_count < .01"
   );
   NS_ENSURE_STATE(deleteAdaptive);
 
+  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+  if (!conn) {
+    return NS_ERROR_UNEXPECTED;
+  }
   mozIStorageBaseStatement *stmts[] = {
     decayFrecency.get(),
     decayAdaptive.get(),
     deleteAdaptive.get()
   };
   nsCOMPtr<mozIStoragePendingStatement> ps;
   RefPtr<DecayFrecencyCallback> cb = new DecayFrecencyCallback();
-  rv = mDB->MainConn()->ExecuteAsync(stmts, ArrayLength(stmts), cb,
+  rv = conn->ExecuteAsync(stmts, ArrayLength(stmts), cb,
                                      getter_AddRefs(ps));
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 
 // Query stuff *****************************************************************
@@ -4378,25 +4396,29 @@ nsNavHistory::UpdateFrecency(int64_t aPl
     "SET hidden = 0 "
     "WHERE id = :page_id AND frecency <> 0"
   );
   NS_ENSURE_STATE(updateHiddenStmt);
   rv = updateHiddenStmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"),
                                          aPlaceId);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
+  if (!conn) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
   mozIStorageBaseStatement *stmts[] = {
     updateFrecencyStmt.get()
   , updateHiddenStmt.get()
   };
-
   RefPtr<AsyncStatementCallbackNotifier> cb =
     new AsyncStatementCallbackNotifier(TOPIC_FRECENCY_UPDATED);
   nsCOMPtr<mozIStoragePendingStatement> ps;
-  rv = mDB->MainConn()->ExecuteAsync(stmts, ArrayLength(stmts), cb,
+  rv = conn->ExecuteAsync(stmts, ArrayLength(stmts), cb,
                                      getter_AddRefs(ps));
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 
 namespace {
--- a/toolkit/components/places/nsPlacesExpiration.js
+++ b/toolkit/components/places/nsPlacesExpiration.js
@@ -836,37 +836,41 @@ nsPlacesExpiration.prototype = {
         diskAvailableBytes * DATABASE_TO_DISK_PERC / 100,
         DATABASE_MAX_SIZE
       );
 
       // Calculate avg size of a URI in the database.
       let db;
       try {
         db = await PlacesUtils.promiseDBConnection();
+        if (db) {
+          let row = (await db.execute(`SELECT * FROM pragma_page_size(),
+                                                pragma_page_count(),
+                                                pragma_freelist_count(),
+                                                (SELECT count(*) FROM moz_places)`))[0];
+          let pageSize = row.getResultByIndex(0);
+          let pageCount = row.getResultByIndex(1);
+          let freelistCount = row.getResultByIndex(2);
+          let uriCount = row.getResultByIndex(3);
+          let dbSize = (pageCount - freelistCount) * pageSize;
+          let avgURISize = Math.ceil(dbSize / uriCount);
+          // For new profiles this value may be too large, due to the Sqlite header,
+          // or Infinity when there are no pages.  Thus we must limit it.
+          if (avgURISize > (URIENTRY_AVG_SIZE * 3)) {
+            avgURISize = URIENTRY_AVG_SIZE;
+          }
+          this._urisLimit = Math.ceil(optimalDatabaseSize / avgURISize);
+        }
       } catch (ex) {
         // We may have been initialized late in the shutdown process, maybe
         // by a call to clear history on shutdown.
         // If we're unable to get a connection clone, we'll just proceed with
         // the default value, it should not be critical at this point in the
         // application life-cycle.
       }
-      if (db) {
-        let pageSize = (await db.execute(`PRAGMA page_size`))[0].getResultByIndex(0);
-        let pageCount = (await db.execute(`PRAGMA page_count`))[0].getResultByIndex(0);
-        let freelistCount = (await db.execute(`PRAGMA freelist_count`))[0].getResultByIndex(0);
-        let dbSize = (pageCount - freelistCount) * pageSize;
-        let uriCount = (await db.execute(`SELECT count(*) FROM moz_places`))[0].getResultByIndex(0);
-        let avgURISize = Math.ceil(dbSize / uriCount);
-        // For new profiles this value may be too large, due to the Sqlite header,
-        // or Infinity when there are no pages.  Thus we must limit it.
-        if (avgURISize > (URIENTRY_AVG_SIZE * 3)) {
-          avgURISize = URIENTRY_AVG_SIZE;
-        }
-        this._urisLimit = Math.ceil(optimalDatabaseSize / avgURISize);
-      }
     }
 
     // Expose the calculated limit to other components.
     this._prefBranch.setIntPref(PREF_READONLY_CALCULATED_MAX_URIS,
                                 this._urisLimit);
 
     // Get the expiration interval value.
     this._interval = this._prefBranch.getIntPref(PREF_INTERVAL_SECONDS,
--- a/toolkit/components/places/tests/bookmarks/test_keywords.js
+++ b/toolkit/components/places/tests/bookmarks/test_keywords.js
@@ -72,160 +72,165 @@ add_task(function test_invalid_input() {
                 /NS_ERROR_ILLEGAL_VALUE/);
   Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(null, "k"),
                 /NS_ERROR_ILLEGAL_VALUE/);
   Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(0, "k"),
                 /NS_ERROR_ILLEGAL_VALUE/);
 });
 
 add_task(async function test_addBookmarkAndKeyword() {
-  check_keyword(URI1, null);
+  await check_keyword(URI1, null);
   let fc = await foreign_count(URI1);
   let observer = expectNotifications();
 
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                          URI1,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
                                          "test");
 
   PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
   let bookmark = await PlacesUtils.bookmarks.fetch({ url: URI1 });
   observer.check([ { name: "onItemChanged",
                      arguments: [ itemId, "keyword", false, "keyword",
                                   bookmark.lastModified * 1000, bookmark.type,
                                   (await PlacesUtils.promiseItemId(bookmark.parentGuid)),
-                                  bookmark.guid, bookmark.parentGuid, "",
+                                  bookmark.guid, bookmark.parentGuid,
+                                  bookmark.url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
   await PlacesTestUtils.promiseAsyncUpdates();
 
-  check_keyword(URI1, "keyword");
+  await check_keyword(URI1, "keyword");
   Assert.equal((await foreign_count(URI1)), fc + 2); // + 1 bookmark + 1 keyword
 
   await PlacesTestUtils.promiseAsyncUpdates();
   await check_orphans();
 });
 
 add_task(async function test_addBookmarkToURIHavingKeyword() {
   // The uri has already a keyword.
-  check_keyword(URI1, "keyword");
+  await check_keyword(URI1, "keyword");
   let fc = await foreign_count(URI1);
 
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                          URI1,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
                                          "test");
-  check_keyword(URI1, "keyword");
+  await check_keyword(URI1, "keyword");
   Assert.equal((await foreign_count(URI1)), fc + 1); // + 1 bookmark
 
   PlacesUtils.bookmarks.removeItem(itemId);
   await PlacesTestUtils.promiseAsyncUpdates();
-  check_orphans();
+  await check_orphans();
 });
 
 add_task(async function test_sameKeywordDifferentURI() {
   let fc1 = await foreign_count(URI1);
   let fc2 = await foreign_count(URI2);
   let observer = expectNotifications();
 
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                          URI2,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
                                          "test2");
-  check_keyword(URI1, "keyword");
-  check_keyword(URI2, null);
+  await check_keyword(URI1, "keyword");
+  await check_keyword(URI2, null);
 
   PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "kEyWoRd");
 
   let bookmark1 = await PlacesUtils.bookmarks.fetch({ url: URI1 });
   let bookmark2 = await PlacesUtils.bookmarks.fetch({ url: URI2 });
   observer.check([ { name: "onItemChanged",
                      arguments: [ (await PlacesUtils.promiseItemId(bookmark1.guid)),
                                   "keyword", false, "",
                                   bookmark1.lastModified * 1000, bookmark1.type,
                                   (await PlacesUtils.promiseItemId(bookmark1.parentGuid)),
-                                  bookmark1.guid, bookmark1.parentGuid, "",
+                                  bookmark1.guid, bookmark1.parentGuid,
+                                  bookmark1.url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                     { name: "onItemChanged",
                      arguments: [ itemId, "keyword", false, "keyword",
                                   bookmark2.lastModified * 1000, bookmark2.type,
                                   (await PlacesUtils.promiseItemId(bookmark2.parentGuid)),
-                                  bookmark2.guid, bookmark2.parentGuid, "",
+                                  bookmark2.guid, bookmark2.parentGuid,
+                                  bookmark2.url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
   await PlacesTestUtils.promiseAsyncUpdates();
 
   // The keyword should have been "moved" to the new URI.
-  check_keyword(URI1, null);
+  await check_keyword(URI1, null);
   Assert.equal((await foreign_count(URI1)), fc1 - 1); // - 1 keyword
-  check_keyword(URI2, "keyword");
+  await check_keyword(URI2, "keyword");
   Assert.equal((await foreign_count(URI2)), fc2 + 2); // + 1 bookmark + 1 keyword
 
   await PlacesTestUtils.promiseAsyncUpdates();
-  check_orphans();
+  await check_orphans();
 });
 
 add_task(async function test_sameURIDifferentKeyword() {
   let fc = await foreign_count(URI2);
   let observer = expectNotifications();
 
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                          URI2,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
                                          "test2");
-  check_keyword(URI2, "keyword");
+  await check_keyword(URI2, "keyword");
 
   PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword2");
 
   let bookmarks = [];
   await PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark));
   observer.check([ { name: "onItemChanged",
                      arguments: [ (await PlacesUtils.promiseItemId(bookmarks[0].guid)),
                                   "keyword", false, "keyword2",
                                   bookmarks[0].lastModified * 1000, bookmarks[0].type,
                                   (await PlacesUtils.promiseItemId(bookmarks[0].parentGuid)),
-                                  bookmarks[0].guid, bookmarks[0].parentGuid, "",
+                                  bookmarks[0].guid, bookmarks[0].parentGuid,
+                                  bookmarks[0].url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                     { name: "onItemChanged",
                      arguments: [ (await PlacesUtils.promiseItemId(bookmarks[1].guid)),
                                   "keyword", false, "keyword2",
                                   bookmarks[1].lastModified * 1000, bookmarks[1].type,
                                   (await PlacesUtils.promiseItemId(bookmarks[1].parentGuid)),
-                                  bookmarks[1].guid, bookmarks[1].parentGuid, "",
+                                  bookmarks[1].guid, bookmarks[1].parentGuid,
+                                  bookmarks[0].url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
   await PlacesTestUtils.promiseAsyncUpdates();
 
-  check_keyword(URI2, "keyword2");
+  await check_keyword(URI2, "keyword2");
   Assert.equal((await foreign_count(URI2)), fc + 2); // + 1 bookmark + 1 keyword
 
   await PlacesTestUtils.promiseAsyncUpdates();
-  check_orphans();
+  await check_orphans();
 });
 
 add_task(async function test_removeBookmarkWithKeyword() {
   let fc = await foreign_count(URI2);
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                          URI2,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
                                          "test");
 
    // The keyword should not be removed, since there are other bookmarks yet.
    PlacesUtils.bookmarks.removeItem(itemId);
 
-  check_keyword(URI2, "keyword2");
+  await check_keyword(URI2, "keyword2");
   Assert.equal((await foreign_count(URI2)), fc); // + 1 bookmark - 1 bookmark
 
   await PlacesTestUtils.promiseAsyncUpdates();
-  check_orphans();
+  await check_orphans();
 });
 
 add_task(async function test_unsetKeyword() {
   let fc = await foreign_count(URI2);
   let observer = expectNotifications();
 
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
@@ -239,40 +244,43 @@ add_task(async function test_unsetKeywor
   let bookmarks = [];
   await PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark));
   do_print(bookmarks.length);
   observer.check([ { name: "onItemChanged",
                      arguments: [ (await PlacesUtils.promiseItemId(bookmarks[0].guid)),
                                   "keyword", false, "",
                                   bookmarks[0].lastModified * 1000, bookmarks[0].type,
                                   (await PlacesUtils.promiseItemId(bookmarks[0].parentGuid)),
-                                  bookmarks[0].guid, bookmarks[0].parentGuid, "",
+                                  bookmarks[0].guid, bookmarks[0].parentGuid,
+                                  bookmarks[0].url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                     { name: "onItemChanged",
                      arguments: [ (await PlacesUtils.promiseItemId(bookmarks[1].guid)),
                                   "keyword", false, "",
                                   bookmarks[1].lastModified * 1000, bookmarks[1].type,
                                   (await PlacesUtils.promiseItemId(bookmarks[1].parentGuid)),
-                                  bookmarks[1].guid, bookmarks[1].parentGuid, "",
+                                  bookmarks[1].guid, bookmarks[1].parentGuid,
+                                  bookmarks[1].url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                     { name: "onItemChanged",
                      arguments: [ (await PlacesUtils.promiseItemId(bookmarks[2].guid)),
                                   "keyword", false, "",
                                   bookmarks[2].lastModified * 1000, bookmarks[2].type,
                                   (await PlacesUtils.promiseItemId(bookmarks[2].parentGuid)),
-                                  bookmarks[2].guid, bookmarks[2].parentGuid, "",
+                                  bookmarks[2].guid, bookmarks[2].parentGuid,
+                                  bookmarks[2].url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
 
-  check_keyword(URI1, null);
-  check_keyword(URI2, null);
+  await check_keyword(URI1, null);
+  await check_keyword(URI2, null);
   Assert.equal((await foreign_count(URI2)), fc - 1); // + 1 bookmark - 2 keyword
 
   await PlacesTestUtils.promiseAsyncUpdates();
-  check_orphans();
+  await check_orphans();
 });
 
 add_task(async function test_addRemoveBookmark() {
   let observer = expectNotifications();
 
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                          URI3,
@@ -284,19 +292,20 @@ add_task(async function test_addRemoveBo
   let parentId = await PlacesUtils.promiseItemId(bookmark.parentGuid);
   PlacesUtils.bookmarks.removeItem(itemId);
 
   observer.check([ { name: "onItemChanged",
                      arguments: [ itemId,
                                   "keyword", false, "keyword",
                                   bookmark.lastModified * 1000, bookmark.type,
                                   parentId,
-                                  bookmark.guid, bookmark.parentGuid, "",
+                                  bookmark.guid, bookmark.parentGuid,
+                                  bookmark.url.href,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
 
-  check_keyword(URI3, null);
+  await check_keyword(URI3, null);
   // Don't check the foreign count since the process is async.
   // The new test_keywords.js in unit is checking this though.
 
   await PlacesTestUtils.promiseAsyncUpdates();
-  check_orphans();
+  await check_orphans();
 });
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -45,16 +45,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
                                   "resource://testing-common/PlacesTestUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
                                   "resource://gre/modules/PlacesTransactions.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
                                   "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TestUtils",
+                                  "resource://testing-common/TestUtils.jsm");
 
 // This imports various other objects in addition to PlacesUtils.
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() {
   return NetUtil.newURI(
          "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
          "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==");
deleted file mode 100644
--- a/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-add_task(async function test_lazyBookmarksObservers() {
-  const TEST_URI = Services.io.newURI("http://moz.org/");
-
-  let promise = PromiseUtils.defer();
-
-  let observer = {
-    QueryInterface: XPCOMUtils.generateQI([
-      Ci.nsINavBookmarkObserver,
-    ]),
-
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI) {
-      do_check_true(aURI.equals(TEST_URI));
-      PlacesUtils.removeLazyBookmarkObserver(this);
-      promise.resolve();
-    },
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
-  };
-
-  // Check registration and removal with uninitialized bookmarks service.
-  PlacesUtils.addLazyBookmarkObserver(observer);
-  PlacesUtils.removeLazyBookmarkObserver(observer);
-
-  // Add a proper lazy observer we will test.
-  PlacesUtils.addLazyBookmarkObserver(observer);
-
-  // Check that we don't leak when adding and removing an observer while the
-  // bookmarks service is instantiated but no change happened (bug 721319).
-  PlacesUtils.bookmarks;
-  PlacesUtils.addLazyBookmarkObserver(observer);
-  PlacesUtils.removeLazyBookmarkObserver(observer);
-  try {
-    PlacesUtils.bookmarks.removeObserver(observer);
-    do_throw("Trying to remove a nonexisting observer should throw!");
-  } catch (ex) {}
-
-  await PlacesUtils.bookmarks.insert({
-    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-    url: TEST_URI,
-    title: "Bookmark title"
-  });
-
-  await promise;
-});
--- a/toolkit/components/places/tests/unit/test_history_catobs.js
+++ b/toolkit/components/places/tests/unit/test_history_catobs.js
@@ -1,50 +1,42 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 add_task(async function() {
   do_load_manifest("nsDummyObserver.manifest");
 
-  let dummyCreated = false;
-  let dummyReceivedOnVisit = false;
-
-  Services.obs.addObserver(function created() {
-    Services.obs.removeObserver(created, "dummy-observer-created");
-    dummyCreated = true;
-  }, "dummy-observer-created");
-  Services.obs.addObserver(function visited() {
-    Services.obs.removeObserver(visited, "dummy-observer-visited");
-    dummyReceivedOnVisit = true;
-  }, "dummy-observer-visited");
+  let promises = [];
+  let resolved = 0;
+  promises.push(TestUtils.topicObserved("dummy-observer-created", () => ++resolved));
+  promises.push(TestUtils.topicObserved("dummy-observer-visited", () => ++resolved));
 
   let initialObservers = PlacesUtils.history.getObservers();
 
   // Add a common observer, it should be invoked after the category observer.
-  let notificationsPromised = new Promise((resolve, reject) => {
-    PlacesUtils.history.addObserver({
-      __proto__: NavHistoryObserver.prototype,
-      onVisit() {
-        let observers = PlacesUtils.history.getObservers();
-        Assert.equal(observers.length, initialObservers.length + 1);
+  promises.push(new Promise(resolve => {
+    let observer = new NavHistoryObserver();
+    observer.onVisit = uri => {
+      do_print("Got visit for " + uri.spec);
+      let observers = PlacesUtils.history.getObservers();
+      let observersCount = observers.length;
+      Assert.ok(observersCount > initialObservers.length);
 
-        // Check the common observer is the last one.
-        for (let i = 0; i < initialObservers.length; ++i) {
-          Assert.equal(initialObservers[i], observers[i]);
-        }
+      // Check the common observer is the last one.
+      for (let i = 0; i < initialObservers.length; ++i) {
+        Assert.equal(initialObservers[i], observers[i]);
+      }
 
-        PlacesUtils.history.removeObserver(this);
-        observers = PlacesUtils.history.getObservers();
-        Assert.equal(observers.length, initialObservers.length);
+      PlacesUtils.history.removeObserver(observer);
+      observers = PlacesUtils.history.getObservers();
+      Assert.ok(observers.length < observersCount);
 
-        // Check the category observer has been invoked before this one.
-        Assert.ok(dummyCreated);
-        Assert.ok(dummyReceivedOnVisit);
-        resolve();
-      }
-    });
-  });
+      // Check the category observer has been invoked before this one.
+      Assert.equal(resolved, 2);
+      resolve();
+    };
+    PlacesUtils.history.addObserver(observer);
+  }));
 
-  // Add a visit.
+  do_print("Add a visit");
   await PlacesTestUtils.addVisits(uri("http://typed.mozilla.org"));
-
-  await notificationsPromised;
+  await Promise.all(promises);
 });
--- a/toolkit/components/places/tests/unit/test_history_clear.js
+++ b/toolkit/components/places/tests/unit/test_history_clear.js
@@ -26,34 +26,17 @@ function promiseOnClearHistoryObserved()
       QueryInterface: XPCOMUtils.generateQI([
         Ci.nsINavHistoryObserver,
       ])
     }
     PlacesUtils.history.addObserver(historyObserver);
   });
 }
 
-// This global variable is a promise object, initialized in run_test and waited
-// upon in the first asynchronous test.  It is resolved when the
-// "places-init-complete" notification is received. We cannot initialize it in
-// the asynchronous test, because then it's too late to register the observer.
-var promiseInit;
-
-function run_test() {
-  // places-init-complete is notified after run_test, and it will
-  // run a first frecency fix through async statements.
-  // To avoid random failures we have to run after all of this.
-  promiseInit = promiseTopicObserved(PlacesUtils.TOPIC_INIT_COMPLETE);
-
-  run_next_test();
-}
-
 add_task(async function test_history_clear() {
-  await promiseInit;
-
   await PlacesTestUtils.addVisits([
     { uri: uri("http://typed.mozilla.org/"),
       transition: TRANSITION_TYPED },
     { uri: uri("http://link.mozilla.org/"),
       transition: TRANSITION_LINK },
     { uri: uri("http://download.mozilla.org/"),
       transition: TRANSITION_DOWNLOAD },
     { uri: uri("http://redir_temp.mozilla.org/"),
--- a/toolkit/components/places/tests/unit/test_history_notifications.js
+++ b/toolkit/components/places/tests/unit/test_history_notifications.js
@@ -1,38 +1,44 @@
 const NS_PLACES_INIT_COMPLETE_TOPIC = "places-init-complete";
-const NS_PLACES_DATABASE_LOCKED_TOPIC = "places-database-locked";
+let gLockedConn;
 
-add_task(async function() {
+add_task(async function setup() {
   // Create a dummy places.sqlite and open an unshared connection on it
   let db = Services.dirsvc.get("ProfD", Ci.nsIFile);
   db.append("places.sqlite");
-  let dbConn = Services.storage.openUnsharedDatabase(db);
+  gLockedConn = Services.storage.openUnsharedDatabase(db);
   Assert.ok(db.exists(), "The database should have been created");
 
   // We need an exclusive lock on the db
-  dbConn.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
+  gLockedConn.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
   // Exclusive locking is lazy applied, we need to make a write to activate it
-  dbConn.executeSimpleSQL("PRAGMA USER_VERSION = 1");
+  gLockedConn.executeSimpleSQL("PRAGMA USER_VERSION = 1");
+});
 
-  // Try to create history service while the db is locked
-  let promiseLocked = promiseTopicObserved(NS_PLACES_DATABASE_LOCKED_TOPIC);
-  Assert.throws(() => Cc["@mozilla.org/browser/nav-history-service;1"]
-                        .getService(Ci.nsINavHistoryService),
-                /NS_ERROR_XPC_GS_RETURNED_FAILURE/);
-  await promiseLocked;
+add_task(async function locked() {
+  // Try to create history service while the db is locked.
+  // It should be possible to create the service, but any method using the
+  // database will fail.
+  let resolved = false;
+  let promiseComplete = promiseTopicObserved(NS_PLACES_INIT_COMPLETE_TOPIC)
+                          .then(() => resolved = true);
+  let history = Cc["@mozilla.org/browser/nav-history-service;1"]
+                  .createInstance(Ci.nsINavHistoryService);
+  // The notification shouldn't happen until something tries to use the database.
+  await new Promise(resolve => do_timeout(100, resolve));
+  Assert.equal(resolved, false, "The notification should not have been fired yet");
+  // This will initialize the database.
+  Assert.equal(history.databaseStatus, history.DATABASE_STATUS_LOCKED);
+  await promiseComplete;
 
   // Close our connection and try to cleanup the file (could fail on Windows)
-  dbConn.close();
+  gLockedConn.close();
+  let db = Services.dirsvc.get("ProfD", Ci.nsIFile);
+  db.append("places.sqlite");
   if (db.exists()) {
     try {
       db.remove(false);
     } catch (e) {
       do_print("Unable to remove dummy places.sqlite");
     }
   }
-
-  // Create history service correctly
-  let promiseComplete = promiseTopicObserved(NS_PLACES_INIT_COMPLETE_TOPIC);
-  Cc["@mozilla.org/browser/nav-history-service;1"]
-    .getService(Ci.nsINavHistoryService);
-  await promiseComplete;
 });
--- a/toolkit/components/places/tests/unit/test_keywords.js
+++ b/toolkit/components/places/tests/unit/test_keywords.js
@@ -78,40 +78,16 @@ function expectBookmarkNotifications() {
         return target[name];
       return undefined;
     }
   });
   PlacesUtils.bookmarks.addObserver(observer);
   return observer;
 }
 
-// This test must be the first one, since it creates the keywords cache.
-add_task(async function test_invalidURL() {
-  await PlacesTestUtils.addVisits("http://test.com/");
-  // Change to url to an invalid one, there's no API for that, so we must do
-  // that manually.
-  await PlacesUtils.withConnectionWrapper("test_invalidURL", async function(db) {
-    await db.execute(
-      `UPDATE moz_places SET url = :broken, url_hash = hash(:broken)
-       WHERE id = (SELECT id FROM moz_places WHERE url_hash = hash(:url))`,
-      { url: "http://test.com/", broken: "<invalid url>" });
-
-    await db.execute(
-      `INSERT INTO moz_keywords (keyword, place_id)
-       VALUES (:kw, (SELECT id FROM moz_places WHERE url_hash = hash(:broken)))`,
-      { broken: "<invalid url>", kw: "keyword" });
-  });
-  await check_keyword(false, "http://broken.com/", "keyword");
-  await check_keyword(false, null, "keyword");
-  await PlacesUtils.withConnectionWrapper("test_invalidURL", async function(db) {
-    let rows = await db.execute(`SELECT * FROM moz_keywords`);
-    Assert.equal(rows.length, 0, "The broken keyword should have been removed");
-  });
-});
-
 add_task(async function test_invalid_input() {
   Assert.throws(() => PlacesUtils.keywords.fetch(null),
                 /Invalid keyword/);
   Assert.throws(() => PlacesUtils.keywords.fetch(5),
                 /Invalid keyword/);
   Assert.throws(() => PlacesUtils.keywords.fetch(undefined),
                 /Invalid keyword/);
   Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: null }),
@@ -433,17 +409,17 @@ add_task(async function test_sameURIDiff
 
   // Now remove the bookmark.
   await PlacesUtils.bookmarks.remove(bookmark);
   while ((await foreign_count("http://example.com/")));
   await check_keyword(false, "http://example.com/", "keyword");
   await check_keyword(false, "http://example.com/", "keyword2");
   await check_keyword(false, "http://example.com/", "keyword3");
 
-  check_no_orphans();
+  await check_no_orphans();
 });
 
 add_task(async function test_deleteKeywordMultipleBookmarks() {
   let fc = await foreign_count("http://example.com/");
 
   let observer = expectBookmarkNotifications();
   let bookmark1 = await PlacesUtils.bookmarks.insert({ url: "http://example.com/",
                                                        parentGuid: PlacesUtils.bookmarks.unfiledGuid });
@@ -487,45 +463,45 @@ add_task(async function test_deleteKeywo
                                  bookmark1.guid, bookmark1.parentGuid, "",
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
 
   // Now remove the bookmarks.
   await PlacesUtils.bookmarks.remove(bookmark1);
   await PlacesUtils.bookmarks.remove(bookmark2);
   Assert.equal((await foreign_count("http://example.com/")), fc); // -2 bookmarks
 
-  check_no_orphans();
+  await check_no_orphans();
 });
 
 add_task(async function test_multipleKeywordsSamePostData() {
   await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", postData: "postData1" });
   await check_keyword(true, "http://example.com/", "keyword", "postData1");
   // Add another keyword with same postData, should fail.
   await Assert.rejects(PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", postData: "postData1" }),
                        /constraint failed/);
   await check_keyword(false, "http://example.com/", "keyword2", "postData1");
 
   await PlacesUtils.keywords.remove("keyword");
 
-  check_no_orphans();
+  await check_no_orphans();
 });
 
 add_task(async function test_oldPostDataAPI() {
   let bookmark = await PlacesUtils.bookmarks.insert({ url: "http://example.com/",
                                                       parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
   let itemId = await PlacesUtils.promiseItemId(bookmark.guid);
   await PlacesUtils.setPostDataForBookmark(itemId, "postData");
   await check_keyword(true, "http://example.com/", "keyword", "postData");
   Assert.equal(PlacesUtils.getPostDataForBookmark(itemId), "postData");
 
   await PlacesUtils.keywords.remove("keyword");
   await PlacesUtils.bookmarks.remove(bookmark);
 
-  check_no_orphans();
+  await check_no_orphans();
 });
 
 add_task(async function test_oldKeywordsAPI() {
   let bookmark = await PlacesUtils.bookmarks.insert({ url: "http://example.com/",
                                                     parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   await check_keyword(false, "http://example.com/", "keyword");
   let itemId = await PlacesUtils.promiseItemId(bookmark.guid);
 
@@ -539,17 +515,17 @@ add_task(async function test_oldKeywords
   await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com" });
   Assert.equal(PlacesUtils.bookmarks.getKeywordForBookmark(itemId), "keyword");
 
   let entry = await PlacesUtils.keywords.fetch("keyword");
   Assert.equal(entry.url, "http://example.com/");
 
   await PlacesUtils.bookmarks.remove(bookmark);
 
-  check_no_orphans();
+  await check_no_orphans();
 });
 
 add_task(async function test_bookmarkURLChange() {
   let fc1 = await foreign_count("http://example1.com/");
   let fc2 = await foreign_count("http://example2.com/");
   let bookmark = await PlacesUtils.bookmarks.insert({ url: "http://example1.com/",
                                                       parentGuid: PlacesUtils.bookmarks.unfiledGuid });
   await PlacesUtils.keywords.insert({ keyword: "keyword",
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -106,17 +106,16 @@ skip-if = (os == "win" && os_version == 
 skip-if = true
 [test_null_interfaces.js]
 [test_onItemChanged_tags.js]
 [test_pageGuid_bookmarkGuid.js]
 [test_frecency_observers.js]
 [test_placeURIs.js]
 [test_PlacesSearchAutocompleteProvider.js]
 [test_PlacesUtils_invalidateCachedGuidFor.js]
-[test_PlacesUtils_lazyobservers.js]
 [test_placesTxn.js]
 [test_preventive_maintenance.js]
 [test_preventive_maintenance_checkAndFixDatabase.js]
 [test_preventive_maintenance_runTasks.js]
 [test_promiseBookmarksTree.js]
 [test_resolveNullBookmarkTitles.js]
 [test_result_sort.js]
 [test_resultsAsVisit_details.js]
--- a/toolkit/components/places/toolkitplaces.manifest
+++ b/toolkit/components/places/toolkitplaces.manifest
@@ -12,17 +12,17 @@ contract @mozilla.org/autocomplete/searc
 component {705a423f-2f69-42f3-b9fe-1517e0dee56f} nsPlacesExpiration.js
 contract @mozilla.org/places/expiration;1 {705a423f-2f69-42f3-b9fe-1517e0dee56f}
 category history-observers nsPlacesExpiration @mozilla.org/places/expiration;1
 
 # PlacesCategoriesStarter.js
 component {803938d5-e26d-4453-bf46-ad4b26e41114} PlacesCategoriesStarter.js
 contract @mozilla.org/places/categoriesStarter;1 {803938d5-e26d-4453-bf46-ad4b26e41114}
 category idle-daily PlacesCategoriesStarter @mozilla.org/places/categoriesStarter;1
-category bookmark-observers PlacesCategoriesStarter @mozilla.org/places/categoriesStarter;1
+category places-init-complete PlacesCategoriesStarter @mozilla.org/places/categoriesStarter;1
 
 # ColorAnalyzer.js
 component {d056186c-28a0-494e-aacc-9e433772b143} ColorAnalyzer.js
 contract @mozilla.org/places/colorAnalyzer;1 {d056186c-28a0-494e-aacc-9e433772b143}
 
 # UnifiedComplete.js
 component {f964a319-397a-4d21-8be6-5cdd1ee3e3ae} UnifiedComplete.js
 contract @mozilla.org/autocomplete/search;1?name=unifiedcomplete {f964a319-397a-4d21-8be6-5cdd1ee3e3ae}